| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206142071420814209142101421114212142131421414215142161421714218142191422014221142221422314224142251422614227142281422914230142311423214233142341423514236142371423814239142401424114242142431424414245142461424714248142491425014251142521425314254142551425614257142581425914260142611426214263142641426514266142671426814269142701427114272142731427414275142761427714278142791428014281142821428314284142851428614287142881428914290142911429214293142941429514296142971429814299143001430114302143031430414305143061430714308143091431014311143121431314314143151431614317143181431914320143211432214323143241432514326143271432814329143301433114332143331433414335143361433714338143391434014341143421434314344143451434614347143481434914350143511435214353143541435514356143571435814359143601436114362143631436414365143661436714368143691437014371143721437314374143751437614377143781437914380143811438214383143841438514386143871438814389143901439114392143931439414395143961439714398143991440014401144021440314404144051440614407144081440914410144111441214413144141441514416144171441814419144201442114422144231442414425144261442714428144291443014431144321443314434144351443614437144381443914440144411444214443144441444514446144471444814449144501445114452144531445414455144561445714458144591446014461144621446314464144651446614467144681446914470144711447214473144741447514476144771447814479144801448114482144831448414485144861448714488144891449014491144921449314494144951449614497144981449914500145011450214503145041450514506145071450814509145101451114512145131451414515145161451714518145191452014521145221452314524145251452614527145281452914530145311453214533145341453514536145371453814539145401454114542145431454414545145461454714548145491455014551145521455314554145551455614557145581455914560145611456214563145641456514566145671456814569145701457114572145731457414575145761457714578145791458014581145821458314584145851458614587145881458914590145911459214593145941459514596145971459814599146001460114602146031460414605146061460714608146091461014611146121461314614146151461614617146181461914620146211462214623146241462514626146271462814629146301463114632146331463414635146361463714638146391464014641146421464314644146451464614647146481464914650146511465214653146541465514656146571465814659146601466114662146631466414665146661466714668146691467014671146721467314674146751467614677146781467914680146811468214683146841468514686146871468814689146901469114692146931469414695146961469714698146991470014701147021470314704147051470614707147081470914710147111471214713147141471514716147171471814719147201472114722147231472414725147261472714728147291473014731147321473314734147351473614737147381473914740147411474214743147441474514746147471474814749147501475114752147531475414755147561475714758147591476014761147621476314764147651476614767147681476914770147711477214773147741477514776147771477814779147801478114782147831478414785147861478714788147891479014791147921479314794147951479614797147981479914800148011480214803148041480514806148071480814809148101481114812148131481414815148161481714818148191482014821148221482314824148251482614827148281482914830148311483214833148341483514836148371483814839148401484114842148431484414845148461484714848148491485014851148521485314854148551485614857148581485914860148611486214863148641486514866148671486814869148701487114872148731487414875148761487714878148791488014881148821488314884148851488614887148881488914890148911489214893148941489514896148971489814899149001490114902149031490414905149061490714908149091491014911149121491314914149151491614917149181491914920149211492214923149241492514926149271492814929149301493114932149331493414935149361493714938149391494014941149421494314944149451494614947149481494914950149511495214953149541495514956149571495814959149601496114962149631496414965149661496714968149691497014971149721497314974149751497614977149781497914980149811498214983149841498514986149871498814989149901499114992149931499414995149961499714998149991500015001150021500315004150051500615007150081500915010150111501215013150141501515016150171501815019150201502115022150231502415025150261502715028150291503015031150321503315034150351503615037150381503915040150411504215043150441504515046150471504815049150501505115052150531505415055150561505715058150591506015061150621506315064150651506615067150681506915070150711507215073150741507515076150771507815079150801508115082150831508415085150861508715088150891509015091150921509315094150951509615097150981509915100151011510215103151041510515106151071510815109151101511115112151131511415115151161511715118151191512015121151221512315124151251512615127151281512915130151311513215133151341513515136151371513815139151401514115142151431514415145151461514715148151491515015151151521515315154151551515615157151581515915160151611516215163151641516515166151671516815169151701517115172151731517415175151761517715178151791518015181151821518315184151851518615187151881518915190151911519215193151941519515196151971519815199152001520115202152031520415205152061520715208152091521015211152121521315214152151521615217152181521915220152211522215223152241522515226152271522815229152301523115232152331523415235152361523715238152391524015241152421524315244152451524615247152481524915250152511525215253152541525515256152571525815259152601526115262152631526415265152661526715268152691527015271152721527315274152751527615277152781527915280152811528215283152841528515286152871528815289152901529115292152931529415295152961529715298152991530015301153021530315304153051530615307153081530915310153111531215313153141531515316153171531815319153201532115322153231532415325153261532715328153291533015331153321533315334153351533615337153381533915340153411534215343153441534515346153471534815349153501535115352153531535415355153561535715358153591536015361153621536315364153651536615367153681536915370153711537215373153741537515376153771537815379153801538115382153831538415385153861538715388153891539015391153921539315394153951539615397153981539915400154011540215403154041540515406154071540815409154101541115412154131541415415154161541715418154191542015421154221542315424154251542615427154281542915430154311543215433154341543515436154371543815439154401544115442154431544415445154461544715448154491545015451154521545315454154551545615457154581545915460154611546215463154641546515466154671546815469154701547115472154731547415475154761547715478154791548015481154821548315484154851548615487154881548915490154911549215493154941549515496154971549815499155001550115502155031550415505155061550715508155091551015511155121551315514155151551615517155181551915520155211552215523155241552515526155271552815529155301553115532155331553415535155361553715538155391554015541155421554315544155451554615547155481554915550155511555215553155541555515556155571555815559155601556115562155631556415565155661556715568155691557015571155721557315574155751557615577155781557915580155811558215583155841558515586155871558815589155901559115592155931559415595155961559715598155991560015601156021560315604156051560615607156081560915610156111561215613156141561515616156171561815619156201562115622156231562415625156261562715628156291563015631156321563315634156351563615637156381563915640156411564215643156441564515646156471564815649156501565115652156531565415655156561565715658156591566015661156621566315664156651566615667156681566915670156711567215673156741567515676156771567815679156801568115682156831568415685156861568715688156891569015691156921569315694156951569615697156981569915700157011570215703157041570515706157071570815709157101571115712157131571415715157161571715718157191572015721157221572315724157251572615727157281572915730157311573215733157341573515736157371573815739157401574115742157431574415745157461574715748157491575015751157521575315754157551575615757157581575915760157611576215763157641576515766157671576815769157701577115772157731577415775157761577715778157791578015781157821578315784157851578615787157881578915790157911579215793157941579515796157971579815799158001580115802158031580415805158061580715808158091581015811158121581315814158151581615817158181581915820158211582215823158241582515826158271582815829158301583115832158331583415835158361583715838158391584015841158421584315844158451584615847158481584915850158511585215853158541585515856158571585815859158601586115862158631586415865158661586715868158691587015871158721587315874158751587615877158781587915880158811588215883158841588515886158871588815889158901589115892158931589415895158961589715898158991590015901159021590315904159051590615907159081590915910159111591215913159141591515916159171591815919159201592115922159231592415925159261592715928159291593015931159321593315934159351593615937159381593915940159411594215943159441594515946159471594815949159501595115952159531595415955159561595715958159591596015961159621596315964159651596615967159681596915970159711597215973159741597515976159771597815979159801598115982159831598415985159861598715988159891599015991159921599315994159951599615997159981599916000160011600216003160041600516006160071600816009160101601116012160131601416015160161601716018160191602016021160221602316024160251602616027160281602916030160311603216033160341603516036160371603816039160401604116042160431604416045160461604716048160491605016051160521605316054160551605616057160581605916060160611606216063160641606516066160671606816069160701607116072160731607416075160761607716078160791608016081160821608316084160851608616087160881608916090160911609216093160941609516096160971609816099161001610116102161031610416105161061610716108161091611016111161121611316114161151611616117161181611916120161211612216123161241612516126161271612816129161301613116132161331613416135161361613716138161391614016141161421614316144161451614616147161481614916150161511615216153161541615516156161571615816159161601616116162161631616416165161661616716168161691617016171161721617316174161751617616177161781617916180161811618216183161841618516186161871618816189161901619116192161931619416195161961619716198161991620016201162021620316204162051620616207162081620916210162111621216213162141621516216162171621816219162201622116222162231622416225162261622716228162291623016231162321623316234162351623616237162381623916240162411624216243162441624516246162471624816249162501625116252162531625416255162561625716258162591626016261162621626316264162651626616267162681626916270162711627216273162741627516276162771627816279162801628116282162831628416285162861628716288162891629016291162921629316294162951629616297162981629916300163011630216303163041630516306163071630816309163101631116312163131631416315163161631716318163191632016321163221632316324163251632616327163281632916330163311633216333163341633516336163371633816339163401634116342163431634416345163461634716348163491635016351163521635316354163551635616357163581635916360163611636216363163641636516366163671636816369163701637116372163731637416375163761637716378163791638016381163821638316384163851638616387163881638916390163911639216393163941639516396163971639816399164001640116402164031640416405164061640716408164091641016411164121641316414164151641616417164181641916420164211642216423164241642516426164271642816429164301643116432164331643416435164361643716438164391644016441164421644316444164451644616447164481644916450164511645216453164541645516456164571645816459164601646116462164631646416465164661646716468164691647016471164721647316474164751647616477164781647916480164811648216483164841648516486164871648816489164901649116492164931649416495164961649716498164991650016501165021650316504165051650616507165081650916510165111651216513165141651516516165171651816519165201652116522165231652416525165261652716528165291653016531165321653316534165351653616537165381653916540165411654216543165441654516546165471654816549165501655116552165531655416555165561655716558165591656016561165621656316564165651656616567165681656916570165711657216573165741657516576165771657816579165801658116582165831658416585165861658716588165891659016591165921659316594165951659616597165981659916600166011660216603166041660516606166071660816609166101661116612166131661416615166161661716618166191662016621166221662316624166251662616627166281662916630166311663216633166341663516636166371663816639166401664116642166431664416645166461664716648166491665016651166521665316654166551665616657166581665916660166611666216663166641666516666166671666816669166701667116672166731667416675166761667716678166791668016681166821668316684166851668616687166881668916690166911669216693166941669516696166971669816699167001670116702167031670416705167061670716708167091671016711167121671316714167151671616717167181671916720167211672216723167241672516726167271672816729167301673116732167331673416735167361673716738167391674016741167421674316744167451674616747167481674916750167511675216753167541675516756167571675816759167601676116762167631676416765167661676716768167691677016771167721677316774167751677616777167781677916780167811678216783167841678516786167871678816789167901679116792167931679416795167961679716798167991680016801168021680316804168051680616807168081680916810168111681216813168141681516816168171681816819168201682116822168231682416825168261682716828168291683016831168321683316834168351683616837168381683916840168411684216843168441684516846168471684816849168501685116852168531685416855168561685716858168591686016861168621686316864168651686616867168681686916870168711687216873168741687516876168771687816879168801688116882168831688416885168861688716888168891689016891168921689316894168951689616897168981689916900169011690216903169041690516906169071690816909169101691116912169131691416915169161691716918169191692016921169221692316924169251692616927169281692916930169311693216933169341693516936169371693816939169401694116942169431694416945169461694716948169491695016951169521695316954169551695616957169581695916960169611696216963169641696516966169671696816969169701697116972169731697416975169761697716978169791698016981169821698316984169851698616987169881698916990169911699216993169941699516996169971699816999170001700117002170031700417005170061700717008170091701017011170121701317014170151701617017170181701917020170211702217023170241702517026170271702817029170301703117032170331703417035170361703717038170391704017041170421704317044170451704617047170481704917050170511705217053170541705517056170571705817059170601706117062170631706417065170661706717068170691707017071170721707317074170751707617077170781707917080170811708217083170841708517086170871708817089170901709117092170931709417095170961709717098170991710017101171021710317104171051710617107171081710917110171111711217113171141711517116171171711817119171201712117122171231712417125171261712717128171291713017131171321713317134171351713617137171381713917140171411714217143171441714517146171471714817149171501715117152171531715417155171561715717158171591716017161171621716317164171651716617167171681716917170171711717217173171741717517176171771717817179171801718117182171831718417185171861718717188171891719017191171921719317194171951719617197171981719917200172011720217203172041720517206172071720817209172101721117212172131721417215172161721717218172191722017221172221722317224172251722617227172281722917230172311723217233172341723517236172371723817239172401724117242172431724417245172461724717248172491725017251172521725317254172551725617257172581725917260172611726217263172641726517266172671726817269172701727117272172731727417275172761727717278172791728017281172821728317284172851728617287172881728917290172911729217293172941729517296172971729817299173001730117302173031730417305173061730717308173091731017311173121731317314173151731617317173181731917320173211732217323173241732517326173271732817329173301733117332173331733417335173361733717338173391734017341173421734317344173451734617347173481734917350173511735217353173541735517356173571735817359173601736117362173631736417365173661736717368173691737017371173721737317374173751737617377173781737917380173811738217383173841738517386173871738817389173901739117392173931739417395173961739717398173991740017401174021740317404174051740617407174081740917410174111741217413174141741517416174171741817419174201742117422174231742417425174261742717428174291743017431174321743317434174351743617437174381743917440174411744217443174441744517446174471744817449174501745117452174531745417455174561745717458174591746017461174621746317464174651746617467174681746917470174711747217473174741747517476174771747817479174801748117482174831748417485174861748717488174891749017491174921749317494174951749617497174981749917500175011750217503175041750517506175071750817509175101751117512175131751417515175161751717518175191752017521175221752317524175251752617527175281752917530175311753217533175341753517536175371753817539175401754117542175431754417545175461754717548175491755017551175521755317554175551755617557175581755917560175611756217563175641756517566175671756817569175701757117572175731757417575175761757717578175791758017581175821758317584175851758617587175881758917590175911759217593175941759517596175971759817599176001760117602176031760417605176061760717608176091761017611176121761317614176151761617617176181761917620176211762217623176241762517626176271762817629176301763117632176331763417635176361763717638176391764017641176421764317644176451764617647176481764917650176511765217653176541765517656176571765817659176601766117662176631766417665176661766717668176691767017671176721767317674176751767617677176781767917680176811768217683176841768517686176871768817689176901769117692176931769417695176961769717698176991770017701177021770317704177051770617707177081770917710177111771217713177141771517716177171771817719177201772117722177231772417725177261772717728177291773017731177321773317734177351773617737177381773917740177411774217743177441774517746177471774817749177501775117752177531775417755177561775717758177591776017761177621776317764177651776617767177681776917770177711777217773177741777517776177771777817779177801778117782177831778417785177861778717788177891779017791177921779317794177951779617797177981779917800178011780217803178041780517806178071780817809178101781117812178131781417815178161781717818178191782017821178221782317824178251782617827178281782917830178311783217833178341783517836178371783817839178401784117842178431784417845178461784717848178491785017851178521785317854178551785617857178581785917860178611786217863178641786517866178671786817869178701787117872178731787417875178761787717878178791788017881178821788317884178851788617887178881788917890178911789217893178941789517896178971789817899179001790117902179031790417905179061790717908179091791017911179121791317914179151791617917179181791917920179211792217923179241792517926179271792817929179301793117932179331793417935179361793717938179391794017941179421794317944179451794617947179481794917950179511795217953179541795517956179571795817959179601796117962179631796417965179661796717968179691797017971179721797317974179751797617977179781797917980179811798217983179841798517986179871798817989179901799117992179931799417995179961799717998179991800018001180021800318004180051800618007180081800918010180111801218013180141801518016180171801818019180201802118022180231802418025180261802718028180291803018031180321803318034180351803618037180381803918040180411804218043180441804518046180471804818049180501805118052180531805418055180561805718058180591806018061180621806318064180651806618067180681806918070180711807218073180741807518076180771807818079180801808118082180831808418085180861808718088180891809018091180921809318094180951809618097180981809918100181011810218103181041810518106181071810818109181101811118112181131811418115181161811718118181191812018121181221812318124181251812618127181281812918130181311813218133181341813518136181371813818139181401814118142181431814418145181461814718148181491815018151181521815318154181551815618157181581815918160181611816218163181641816518166181671816818169181701817118172181731817418175181761817718178181791818018181181821818318184181851818618187181881818918190181911819218193181941819518196181971819818199182001820118202182031820418205182061820718208182091821018211182121821318214182151821618217182181821918220182211822218223182241822518226182271822818229182301823118232182331823418235182361823718238182391824018241182421824318244182451824618247182481824918250182511825218253182541825518256182571825818259182601826118262182631826418265182661826718268182691827018271182721827318274182751827618277182781827918280182811828218283182841828518286182871828818289182901829118292182931829418295182961829718298182991830018301183021830318304183051830618307183081830918310183111831218313183141831518316183171831818319183201832118322183231832418325183261832718328183291833018331183321833318334183351833618337183381833918340183411834218343183441834518346183471834818349183501835118352183531835418355183561835718358183591836018361183621836318364183651836618367183681836918370183711837218373183741837518376183771837818379183801838118382183831838418385183861838718388183891839018391183921839318394183951839618397183981839918400184011840218403184041840518406184071840818409184101841118412184131841418415184161841718418184191842018421184221842318424184251842618427184281842918430184311843218433184341843518436184371843818439184401844118442184431844418445184461844718448184491845018451184521845318454184551845618457184581845918460184611846218463184641846518466184671846818469184701847118472184731847418475184761847718478184791848018481184821848318484184851848618487184881848918490184911849218493184941849518496184971849818499185001850118502185031850418505185061850718508185091851018511185121851318514185151851618517185181851918520185211852218523185241852518526185271852818529185301853118532185331853418535185361853718538185391854018541185421854318544185451854618547185481854918550185511855218553185541855518556185571855818559185601856118562185631856418565185661856718568185691857018571185721857318574185751857618577185781857918580185811858218583185841858518586185871858818589185901859118592185931859418595185961859718598185991860018601186021860318604186051860618607186081860918610186111861218613186141861518616186171861818619186201862118622186231862418625186261862718628186291863018631186321863318634186351863618637186381863918640186411864218643186441864518646186471864818649186501865118652186531865418655186561865718658186591866018661186621866318664186651866618667186681866918670186711867218673186741867518676186771867818679186801868118682186831868418685186861868718688186891869018691186921869318694186951869618697186981869918700187011870218703187041870518706187071870818709187101871118712187131871418715187161871718718187191872018721187221872318724187251872618727187281872918730187311873218733187341873518736187371873818739187401874118742187431874418745187461874718748187491875018751187521875318754187551875618757187581875918760187611876218763187641876518766187671876818769187701877118772187731877418775187761877718778187791878018781187821878318784187851878618787187881878918790187911879218793187941879518796187971879818799188001880118802188031880418805188061880718808188091881018811188121881318814188151881618817188181881918820188211882218823188241882518826188271882818829188301883118832188331883418835188361883718838188391884018841188421884318844188451884618847188481884918850188511885218853188541885518856188571885818859188601886118862188631886418865188661886718868188691887018871188721887318874188751887618877188781887918880188811888218883188841888518886188871888818889188901889118892188931889418895188961889718898188991890018901189021890318904189051890618907189081890918910189111891218913189141891518916189171891818919189201892118922189231892418925189261892718928189291893018931189321893318934189351893618937189381893918940189411894218943189441894518946189471894818949189501895118952189531895418955189561895718958189591896018961189621896318964189651896618967189681896918970189711897218973189741897518976189771897818979189801898118982189831898418985189861898718988189891899018991189921899318994189951899618997189981899919000190011900219003190041900519006190071900819009190101901119012190131901419015190161901719018190191902019021190221902319024190251902619027190281902919030190311903219033190341903519036190371903819039190401904119042190431904419045190461904719048190491905019051190521905319054190551905619057190581905919060190611906219063190641906519066190671906819069190701907119072190731907419075190761907719078190791908019081190821908319084190851908619087190881908919090190911909219093190941909519096190971909819099191001910119102191031910419105191061910719108191091911019111191121911319114191151911619117191181911919120191211912219123191241912519126191271912819129191301913119132191331913419135191361913719138191391914019141191421914319144191451914619147191481914919150191511915219153191541915519156191571915819159191601916119162191631916419165191661916719168191691917019171191721917319174191751917619177191781917919180191811918219183191841918519186191871918819189191901919119192191931919419195191961919719198191991920019201192021920319204192051920619207192081920919210192111921219213192141921519216192171921819219192201922119222192231922419225192261922719228192291923019231192321923319234192351923619237192381923919240192411924219243192441924519246192471924819249192501925119252192531925419255192561925719258192591926019261192621926319264192651926619267192681926919270192711927219273192741927519276192771927819279192801928119282192831928419285192861928719288192891929019291192921929319294192951929619297192981929919300193011930219303193041930519306193071930819309193101931119312193131931419315193161931719318193191932019321193221932319324193251932619327193281932919330193311933219333193341933519336193371933819339193401934119342193431934419345193461934719348193491935019351193521935319354193551935619357193581935919360193611936219363193641936519366193671936819369193701937119372193731937419375193761937719378193791938019381193821938319384193851938619387193881938919390193911939219393193941939519396193971939819399194001940119402194031940419405194061940719408194091941019411194121941319414194151941619417194181941919420194211942219423194241942519426194271942819429194301943119432194331943419435194361943719438194391944019441194421944319444194451944619447194481944919450194511945219453194541945519456194571945819459194601946119462194631946419465194661946719468194691947019471194721947319474194751947619477194781947919480194811948219483194841948519486194871948819489194901949119492194931949419495194961949719498194991950019501195021950319504195051950619507195081950919510195111951219513195141951519516195171951819519195201952119522195231952419525195261952719528195291953019531195321953319534195351953619537195381953919540195411954219543195441954519546195471954819549195501955119552195531955419555195561955719558195591956019561195621956319564195651956619567195681956919570195711957219573195741957519576195771957819579195801958119582195831958419585195861958719588195891959019591195921959319594195951959619597195981959919600196011960219603196041960519606196071960819609196101961119612196131961419615196161961719618196191962019621196221962319624196251962619627196281962919630196311963219633196341963519636196371963819639196401964119642196431964419645196461964719648196491965019651196521965319654196551965619657196581965919660196611966219663196641966519666196671966819669196701967119672196731967419675196761967719678196791968019681196821968319684196851968619687196881968919690196911969219693196941969519696196971969819699197001970119702197031970419705197061970719708197091971019711197121971319714197151971619717197181971919720197211972219723197241972519726197271972819729197301973119732197331973419735197361973719738197391974019741197421974319744197451974619747197481974919750197511975219753197541975519756197571975819759197601976119762197631976419765197661976719768197691977019771197721977319774197751977619777197781977919780197811978219783197841978519786197871978819789197901979119792197931979419795197961979719798197991980019801198021980319804198051980619807198081980919810198111981219813198141981519816198171981819819198201982119822198231982419825198261982719828198291983019831198321983319834198351983619837198381983919840198411984219843198441984519846198471984819849198501985119852198531985419855198561985719858198591986019861198621986319864198651986619867198681986919870198711987219873198741987519876198771987819879198801988119882198831988419885198861988719888198891989019891198921989319894198951989619897198981989919900199011990219903199041990519906199071990819909199101991119912199131991419915199161991719918199191992019921199221992319924199251992619927199281992919930199311993219933199341993519936199371993819939199401994119942199431994419945199461994719948199491995019951199521995319954199551995619957199581995919960199611996219963199641996519966199671996819969199701997119972199731997419975199761997719978199791998019981199821998319984199851998619987199881998919990199911999219993199941999519996199971999819999200002000120002200032000420005200062000720008200092001020011200122001320014200152001620017200182001920020200212002220023200242002520026200272002820029200302003120032200332003420035200362003720038200392004020041200422004320044200452004620047200482004920050200512005220053200542005520056200572005820059200602006120062200632006420065200662006720068200692007020071200722007320074200752007620077200782007920080200812008220083200842008520086200872008820089200902009120092200932009420095200962009720098200992010020101201022010320104201052010620107201082010920110201112011220113201142011520116201172011820119201202012120122201232012420125201262012720128201292013020131201322013320134201352013620137201382013920140201412014220143201442014520146201472014820149201502015120152201532015420155201562015720158201592016020161201622016320164201652016620167201682016920170201712017220173201742017520176201772017820179201802018120182201832018420185201862018720188201892019020191201922019320194201952019620197201982019920200202012020220203202042020520206202072020820209202102021120212202132021420215202162021720218202192022020221202222022320224202252022620227202282022920230202312023220233202342023520236202372023820239202402024120242202432024420245202462024720248202492025020251202522025320254202552025620257202582025920260202612026220263202642026520266202672026820269202702027120272202732027420275202762027720278202792028020281202822028320284202852028620287202882028920290202912029220293202942029520296202972029820299203002030120302203032030420305203062030720308203092031020311203122031320314203152031620317203182031920320203212032220323203242032520326203272032820329203302033120332203332033420335203362033720338203392034020341203422034320344203452034620347203482034920350203512035220353203542035520356203572035820359203602036120362203632036420365203662036720368203692037020371203722037320374203752037620377203782037920380203812038220383203842038520386203872038820389203902039120392203932039420395203962039720398203992040020401204022040320404204052040620407204082040920410204112041220413204142041520416204172041820419204202042120422204232042420425204262042720428204292043020431204322043320434204352043620437204382043920440204412044220443204442044520446204472044820449204502045120452204532045420455204562045720458204592046020461204622046320464204652046620467204682046920470204712047220473204742047520476204772047820479204802048120482204832048420485204862048720488204892049020491204922049320494204952049620497204982049920500205012050220503205042050520506205072050820509205102051120512205132051420515205162051720518205192052020521205222052320524205252052620527205282052920530205312053220533205342053520536205372053820539205402054120542205432054420545205462054720548205492055020551205522055320554205552055620557205582055920560205612056220563205642056520566205672056820569205702057120572205732057420575205762057720578205792058020581205822058320584205852058620587205882058920590205912059220593205942059520596205972059820599206002060120602206032060420605206062060720608206092061020611206122061320614206152061620617206182061920620206212062220623206242062520626206272062820629206302063120632206332063420635206362063720638206392064020641206422064320644206452064620647206482064920650206512065220653206542065520656206572065820659206602066120662206632066420665206662066720668206692067020671206722067320674206752067620677206782067920680206812068220683206842068520686206872068820689206902069120692206932069420695206962069720698206992070020701207022070320704207052070620707207082070920710207112071220713207142071520716207172071820719207202072120722207232072420725207262072720728207292073020731207322073320734207352073620737207382073920740207412074220743207442074520746207472074820749207502075120752207532075420755207562075720758207592076020761207622076320764207652076620767207682076920770207712077220773207742077520776207772077820779207802078120782207832078420785207862078720788207892079020791207922079320794207952079620797207982079920800208012080220803208042080520806208072080820809208102081120812208132081420815208162081720818208192082020821208222082320824208252082620827208282082920830208312083220833208342083520836208372083820839208402084120842208432084420845208462084720848208492085020851208522085320854208552085620857208582085920860208612086220863208642086520866208672086820869208702087120872208732087420875208762087720878208792088020881208822088320884208852088620887208882088920890208912089220893208942089520896208972089820899209002090120902209032090420905209062090720908209092091020911209122091320914209152091620917209182091920920209212092220923209242092520926209272092820929209302093120932209332093420935209362093720938209392094020941209422094320944209452094620947209482094920950209512095220953209542095520956209572095820959209602096120962209632096420965209662096720968209692097020971209722097320974209752097620977209782097920980209812098220983209842098520986209872098820989209902099120992209932099420995209962099720998209992100021001210022100321004210052100621007210082100921010210112101221013210142101521016210172101821019210202102121022210232102421025210262102721028210292103021031210322103321034210352103621037210382103921040210412104221043210442104521046210472104821049210502105121052210532105421055210562105721058210592106021061210622106321064210652106621067210682106921070210712107221073210742107521076210772107821079210802108121082210832108421085210862108721088210892109021091210922109321094210952109621097210982109921100211012110221103211042110521106211072110821109211102111121112211132111421115211162111721118211192112021121211222112321124211252112621127211282112921130211312113221133211342113521136211372113821139211402114121142211432114421145211462114721148211492115021151211522115321154211552115621157211582115921160211612116221163211642116521166211672116821169211702117121172211732117421175211762117721178211792118021181211822118321184211852118621187211882118921190211912119221193211942119521196211972119821199212002120121202212032120421205212062120721208212092121021211212122121321214212152121621217212182121921220212212122221223212242122521226212272122821229212302123121232212332123421235212362123721238212392124021241212422124321244212452124621247212482124921250212512125221253212542125521256212572125821259212602126121262212632126421265212662126721268212692127021271212722127321274212752127621277212782127921280212812128221283212842128521286212872128821289212902129121292212932129421295212962129721298212992130021301213022130321304213052130621307213082130921310213112131221313213142131521316213172131821319213202132121322213232132421325213262132721328213292133021331213322133321334213352133621337213382133921340213412134221343213442134521346213472134821349213502135121352213532135421355213562135721358213592136021361213622136321364213652136621367213682136921370213712137221373213742137521376213772137821379213802138121382213832138421385213862138721388213892139021391213922139321394213952139621397213982139921400214012140221403214042140521406214072140821409214102141121412214132141421415214162141721418214192142021421214222142321424214252142621427214282142921430214312143221433214342143521436214372143821439214402144121442214432144421445214462144721448214492145021451214522145321454214552145621457214582145921460214612146221463214642146521466214672146821469214702147121472214732147421475214762147721478214792148021481214822148321484214852148621487214882148921490214912149221493214942149521496214972149821499215002150121502215032150421505215062150721508215092151021511215122151321514215152151621517215182151921520215212152221523215242152521526215272152821529215302153121532215332153421535215362153721538215392154021541215422154321544215452154621547215482154921550215512155221553215542155521556215572155821559215602156121562215632156421565215662156721568215692157021571215722157321574215752157621577215782157921580215812158221583215842158521586215872158821589215902159121592215932159421595215962159721598215992160021601216022160321604216052160621607216082160921610216112161221613216142161521616216172161821619216202162121622216232162421625216262162721628216292163021631216322163321634216352163621637216382163921640216412164221643216442164521646216472164821649216502165121652216532165421655216562165721658216592166021661216622166321664216652166621667216682166921670216712167221673216742167521676216772167821679216802168121682216832168421685216862168721688216892169021691216922169321694216952169621697216982169921700217012170221703217042170521706217072170821709217102171121712217132171421715217162171721718217192172021721217222172321724217252172621727217282172921730217312173221733217342173521736217372173821739217402174121742217432174421745217462174721748217492175021751217522175321754217552175621757217582175921760217612176221763217642176521766217672176821769217702177121772217732177421775217762177721778217792178021781217822178321784217852178621787217882178921790217912179221793217942179521796217972179821799218002180121802218032180421805218062180721808218092181021811218122181321814218152181621817218182181921820218212182221823218242182521826218272182821829218302183121832218332183421835218362183721838218392184021841218422184321844218452184621847218482184921850218512185221853218542185521856218572185821859218602186121862218632186421865218662186721868218692187021871218722187321874218752187621877218782187921880218812188221883218842188521886218872188821889218902189121892218932189421895218962189721898218992190021901219022190321904219052190621907219082190921910219112191221913219142191521916219172191821919219202192121922219232192421925219262192721928219292193021931219322193321934219352193621937219382193921940219412194221943219442194521946219472194821949219502195121952219532195421955219562195721958219592196021961219622196321964219652196621967219682196921970219712197221973219742197521976219772197821979219802198121982219832198421985219862198721988219892199021991219922199321994219952199621997219982199922000220012200222003220042200522006220072200822009220102201122012220132201422015220162201722018220192202022021220222202322024220252202622027220282202922030220312203222033220342203522036220372203822039220402204122042220432204422045220462204722048220492205022051220522205322054220552205622057220582205922060220612206222063220642206522066220672206822069220702207122072220732207422075220762207722078220792208022081220822208322084220852208622087220882208922090220912209222093220942209522096220972209822099221002210122102221032210422105221062210722108221092211022111221122211322114221152211622117221182211922120221212212222123221242212522126221272212822129221302213122132221332213422135221362213722138221392214022141221422214322144221452214622147221482214922150221512215222153221542215522156221572215822159221602216122162221632216422165221662216722168221692217022171221722217322174221752217622177221782217922180221812218222183221842218522186221872218822189221902219122192221932219422195221962219722198221992220022201222022220322204222052220622207222082220922210222112221222213222142221522216222172221822219222202222122222222232222422225222262222722228222292223022231222322223322234222352223622237222382223922240222412224222243222442224522246222472224822249222502225122252222532225422255222562225722258222592226022261222622226322264222652226622267222682226922270222712227222273222742227522276222772227822279222802228122282222832228422285222862228722288222892229022291222922229322294222952229622297222982229922300223012230222303223042230522306223072230822309223102231122312223132231422315223162231722318223192232022321223222232322324223252232622327223282232922330223312233222333223342233522336223372233822339223402234122342223432234422345223462234722348223492235022351223522235322354223552235622357223582235922360223612236222363223642236522366223672236822369223702237122372223732237422375223762237722378223792238022381223822238322384223852238622387223882238922390223912239222393223942239522396223972239822399224002240122402224032240422405224062240722408224092241022411224122241322414224152241622417224182241922420224212242222423224242242522426224272242822429224302243122432224332243422435224362243722438224392244022441224422244322444224452244622447224482244922450224512245222453224542245522456224572245822459224602246122462224632246422465224662246722468224692247022471224722247322474224752247622477224782247922480224812248222483224842248522486224872248822489224902249122492224932249422495224962249722498224992250022501225022250322504225052250622507225082250922510225112251222513225142251522516225172251822519225202252122522225232252422525225262252722528225292253022531225322253322534225352253622537225382253922540225412254222543225442254522546225472254822549225502255122552225532255422555225562255722558225592256022561225622256322564225652256622567225682256922570225712257222573225742257522576225772257822579225802258122582225832258422585225862258722588225892259022591225922259322594225952259622597225982259922600226012260222603226042260522606226072260822609226102261122612226132261422615226162261722618226192262022621226222262322624226252262622627226282262922630226312263222633226342263522636226372263822639226402264122642226432264422645226462264722648226492265022651226522265322654226552265622657226582265922660226612266222663226642266522666226672266822669226702267122672226732267422675226762267722678226792268022681226822268322684226852268622687226882268922690226912269222693226942269522696226972269822699227002270122702227032270422705227062270722708227092271022711227122271322714227152271622717227182271922720227212272222723227242272522726227272272822729227302273122732227332273422735227362273722738227392274022741227422274322744227452274622747227482274922750227512275222753227542275522756227572275822759227602276122762227632276422765227662276722768227692277022771227722277322774227752277622777227782277922780227812278222783227842278522786227872278822789227902279122792227932279422795227962279722798227992280022801228022280322804228052280622807228082280922810228112281222813228142281522816228172281822819228202282122822228232282422825228262282722828228292283022831228322283322834228352283622837228382283922840228412284222843228442284522846228472284822849228502285122852228532285422855228562285722858228592286022861228622286322864228652286622867228682286922870228712287222873228742287522876228772287822879228802288122882228832288422885228862288722888228892289022891228922289322894228952289622897228982289922900229012290222903229042290522906229072290822909229102291122912229132291422915229162291722918229192292022921229222292322924229252292622927229282292922930229312293222933229342293522936229372293822939229402294122942229432294422945229462294722948229492295022951229522295322954229552295622957229582295922960229612296222963229642296522966229672296822969229702297122972229732297422975229762297722978229792298022981229822298322984229852298622987229882298922990229912299222993229942299522996229972299822999230002300123002230032300423005230062300723008230092301023011230122301323014230152301623017230182301923020230212302223023230242302523026230272302823029230302303123032230332303423035230362303723038230392304023041230422304323044230452304623047230482304923050230512305223053230542305523056230572305823059230602306123062230632306423065230662306723068230692307023071230722307323074230752307623077230782307923080230812308223083230842308523086230872308823089230902309123092230932309423095230962309723098230992310023101231022310323104231052310623107231082310923110231112311223113231142311523116231172311823119231202312123122231232312423125231262312723128231292313023131231322313323134231352313623137231382313923140231412314223143231442314523146231472314823149231502315123152231532315423155231562315723158231592316023161231622316323164231652316623167231682316923170231712317223173231742317523176231772317823179231802318123182231832318423185231862318723188231892319023191231922319323194231952319623197231982319923200232012320223203232042320523206232072320823209232102321123212232132321423215232162321723218232192322023221232222322323224232252322623227232282322923230232312323223233232342323523236232372323823239232402324123242232432324423245232462324723248232492325023251232522325323254232552325623257232582325923260232612326223263232642326523266232672326823269232702327123272232732327423275232762327723278232792328023281232822328323284232852328623287232882328923290232912329223293232942329523296232972329823299233002330123302233032330423305233062330723308233092331023311233122331323314233152331623317233182331923320233212332223323233242332523326233272332823329233302333123332233332333423335233362333723338233392334023341233422334323344233452334623347233482334923350233512335223353233542335523356233572335823359233602336123362233632336423365233662336723368233692337023371233722337323374233752337623377233782337923380233812338223383233842338523386233872338823389233902339123392233932339423395233962339723398233992340023401234022340323404234052340623407234082340923410234112341223413234142341523416234172341823419234202342123422234232342423425234262342723428234292343023431234322343323434234352343623437234382343923440234412344223443234442344523446234472344823449234502345123452234532345423455234562345723458234592346023461234622346323464234652346623467234682346923470234712347223473234742347523476234772347823479234802348123482234832348423485234862348723488234892349023491234922349323494234952349623497234982349923500235012350223503235042350523506235072350823509235102351123512235132351423515235162351723518235192352023521235222352323524235252352623527235282352923530235312353223533235342353523536235372353823539235402354123542235432354423545235462354723548235492355023551235522355323554235552355623557235582355923560235612356223563235642356523566235672356823569235702357123572235732357423575235762357723578235792358023581235822358323584235852358623587235882358923590235912359223593235942359523596235972359823599236002360123602236032360423605236062360723608236092361023611236122361323614236152361623617236182361923620236212362223623236242362523626236272362823629236302363123632236332363423635236362363723638236392364023641236422364323644236452364623647236482364923650236512365223653236542365523656236572365823659236602366123662236632366423665236662366723668236692367023671236722367323674236752367623677236782367923680236812368223683236842368523686236872368823689236902369123692236932369423695236962369723698236992370023701237022370323704237052370623707237082370923710237112371223713237142371523716237172371823719237202372123722237232372423725237262372723728237292373023731237322373323734237352373623737237382373923740237412374223743237442374523746237472374823749237502375123752237532375423755237562375723758237592376023761237622376323764237652376623767237682376923770237712377223773237742377523776237772377823779237802378123782237832378423785237862378723788237892379023791237922379323794237952379623797237982379923800238012380223803238042380523806238072380823809238102381123812238132381423815238162381723818238192382023821238222382323824238252382623827238282382923830238312383223833238342383523836238372383823839238402384123842238432384423845238462384723848238492385023851238522385323854238552385623857238582385923860238612386223863238642386523866238672386823869238702387123872238732387423875238762387723878238792388023881238822388323884238852388623887238882388923890238912389223893238942389523896238972389823899239002390123902239032390423905239062390723908239092391023911239122391323914239152391623917239182391923920239212392223923239242392523926239272392823929239302393123932239332393423935239362393723938239392394023941239422394323944239452394623947239482394923950239512395223953239542395523956239572395823959239602396123962239632396423965239662396723968239692397023971239722397323974239752397623977239782397923980239812398223983239842398523986239872398823989239902399123992239932399423995239962399723998239992400024001240022400324004240052400624007240082400924010240112401224013240142401524016240172401824019240202402124022240232402424025240262402724028240292403024031240322403324034240352403624037240382403924040240412404224043240442404524046240472404824049240502405124052240532405424055240562405724058240592406024061240622406324064240652406624067240682406924070240712407224073240742407524076240772407824079240802408124082240832408424085240862408724088240892409024091240922409324094240952409624097240982409924100241012410224103241042410524106241072410824109241102411124112241132411424115241162411724118241192412024121241222412324124241252412624127241282412924130241312413224133241342413524136241372413824139241402414124142241432414424145241462414724148241492415024151241522415324154241552415624157241582415924160241612416224163241642416524166241672416824169241702417124172241732417424175241762417724178241792418024181241822418324184241852418624187241882418924190241912419224193241942419524196241972419824199242002420124202242032420424205242062420724208242092421024211242122421324214242152421624217242182421924220242212422224223242242422524226242272422824229242302423124232242332423424235242362423724238242392424024241242422424324244242452424624247242482424924250242512425224253242542425524256242572425824259242602426124262242632426424265242662426724268242692427024271242722427324274242752427624277242782427924280242812428224283242842428524286242872428824289242902429124292242932429424295242962429724298242992430024301243022430324304243052430624307243082430924310243112431224313243142431524316243172431824319243202432124322243232432424325243262432724328243292433024331243322433324334243352433624337243382433924340243412434224343243442434524346243472434824349243502435124352243532435424355243562435724358243592436024361243622436324364243652436624367243682436924370243712437224373243742437524376243772437824379243802438124382243832438424385243862438724388243892439024391243922439324394243952439624397243982439924400244012440224403244042440524406244072440824409244102441124412244132441424415244162441724418244192442024421244222442324424244252442624427244282442924430244312443224433244342443524436244372443824439244402444124442244432444424445244462444724448244492445024451244522445324454244552445624457244582445924460244612446224463244642446524466244672446824469244702447124472244732447424475244762447724478244792448024481244822448324484244852448624487244882448924490244912449224493244942449524496244972449824499245002450124502245032450424505245062450724508245092451024511245122451324514245152451624517245182451924520245212452224523245242452524526245272452824529245302453124532245332453424535245362453724538245392454024541245422454324544245452454624547245482454924550245512455224553245542455524556245572455824559245602456124562245632456424565245662456724568245692457024571245722457324574245752457624577245782457924580245812458224583245842458524586245872458824589245902459124592245932459424595245962459724598245992460024601246022460324604246052460624607246082460924610246112461224613246142461524616246172461824619246202462124622246232462424625246262462724628246292463024631246322463324634246352463624637246382463924640246412464224643246442464524646246472464824649246502465124652246532465424655246562465724658246592466024661246622466324664246652466624667246682466924670246712467224673246742467524676246772467824679246802468124682246832468424685246862468724688246892469024691246922469324694246952469624697246982469924700247012470224703247042470524706247072470824709247102471124712247132471424715247162471724718247192472024721247222472324724247252472624727247282472924730247312473224733247342473524736247372473824739247402474124742247432474424745247462474724748247492475024751247522475324754247552475624757247582475924760247612476224763247642476524766247672476824769247702477124772247732477424775247762477724778247792478024781247822478324784247852478624787247882478924790247912479224793247942479524796247972479824799248002480124802248032480424805248062480724808248092481024811248122481324814248152481624817248182481924820248212482224823248242482524826248272482824829248302483124832248332483424835248362483724838248392484024841248422484324844248452484624847248482484924850248512485224853248542485524856248572485824859248602486124862248632486424865248662486724868248692487024871248722487324874248752487624877248782487924880248812488224883248842488524886248872488824889248902489124892248932489424895248962489724898248992490024901249022490324904249052490624907249082490924910249112491224913249142491524916249172491824919249202492124922249232492424925249262492724928249292493024931249322493324934249352493624937249382493924940249412494224943249442494524946249472494824949249502495124952249532495424955249562495724958249592496024961249622496324964249652496624967249682496924970249712497224973249742497524976249772497824979249802498124982249832498424985249862498724988249892499024991249922499324994249952499624997249982499925000250012500225003250042500525006250072500825009250102501125012250132501425015250162501725018250192502025021250222502325024250252502625027250282502925030250312503225033250342503525036250372503825039250402504125042250432504425045250462504725048250492505025051250522505325054250552505625057250582505925060250612506225063250642506525066250672506825069250702507125072250732507425075250762507725078250792508025081250822508325084250852508625087250882508925090250912509225093250942509525096250972509825099251002510125102251032510425105251062510725108251092511025111251122511325114251152511625117251182511925120251212512225123251242512525126251272512825129251302513125132251332513425135251362513725138251392514025141251422514325144251452514625147251482514925150251512515225153251542515525156251572515825159251602516125162251632516425165251662516725168251692517025171251722517325174251752517625177251782517925180251812518225183251842518525186251872518825189251902519125192251932519425195251962519725198251992520025201252022520325204252052520625207252082520925210252112521225213252142521525216252172521825219252202522125222252232522425225252262522725228252292523025231252322523325234252352523625237252382523925240252412524225243252442524525246252472524825249252502525125252252532525425255252562525725258252592526025261252622526325264252652526625267252682526925270252712527225273252742527525276252772527825279252802528125282252832528425285252862528725288252892529025291252922529325294252952529625297252982529925300253012530225303253042530525306253072530825309253102531125312253132531425315253162531725318253192532025321253222532325324253252532625327253282532925330253312533225333253342533525336253372533825339253402534125342253432534425345253462534725348253492535025351253522535325354253552535625357253582535925360253612536225363253642536525366253672536825369253702537125372253732537425375253762537725378253792538025381253822538325384253852538625387253882538925390253912539225393253942539525396253972539825399254002540125402254032540425405254062540725408254092541025411254122541325414254152541625417254182541925420254212542225423254242542525426254272542825429254302543125432254332543425435254362543725438254392544025441254422544325444254452544625447254482544925450254512545225453254542545525456254572545825459254602546125462254632546425465254662546725468254692547025471254722547325474254752547625477254782547925480254812548225483254842548525486254872548825489254902549125492254932549425495254962549725498254992550025501255022550325504255052550625507255082550925510255112551225513255142551525516255172551825519255202552125522255232552425525255262552725528255292553025531255322553325534255352553625537255382553925540255412554225543255442554525546255472554825549255502555125552255532555425555255562555725558255592556025561255622556325564255652556625567255682556925570255712557225573255742557525576255772557825579255802558125582255832558425585255862558725588255892559025591255922559325594255952559625597255982559925600256012560225603256042560525606256072560825609256102561125612256132561425615256162561725618256192562025621256222562325624256252562625627256282562925630256312563225633256342563525636256372563825639256402564125642256432564425645256462564725648256492565025651256522565325654256552565625657256582565925660256612566225663256642566525666256672566825669256702567125672256732567425675256762567725678256792568025681256822568325684256852568625687256882568925690256912569225693256942569525696256972569825699257002570125702257032570425705257062570725708257092571025711257122571325714257152571625717257182571925720257212572225723257242572525726257272572825729257302573125732257332573425735257362573725738257392574025741257422574325744257452574625747257482574925750257512575225753257542575525756257572575825759257602576125762257632576425765257662576725768257692577025771257722577325774257752577625777257782577925780257812578225783257842578525786257872578825789257902579125792257932579425795257962579725798257992580025801258022580325804258052580625807258082580925810258112581225813258142581525816258172581825819258202582125822258232582425825258262582725828258292583025831258322583325834258352583625837258382583925840258412584225843258442584525846258472584825849258502585125852258532585425855258562585725858258592586025861258622586325864258652586625867258682586925870258712587225873258742587525876258772587825879258802588125882258832588425885258862588725888258892589025891258922589325894258952589625897258982589925900259012590225903259042590525906259072590825909259102591125912259132591425915259162591725918259192592025921259222592325924259252592625927259282592925930259312593225933259342593525936259372593825939259402594125942259432594425945259462594725948259492595025951259522595325954259552595625957259582595925960259612596225963259642596525966259672596825969259702597125972259732597425975259762597725978259792598025981259822598325984259852598625987259882598925990259912599225993259942599525996259972599825999260002600126002260032600426005260062600726008260092601026011260122601326014260152601626017260182601926020260212602226023260242602526026260272602826029260302603126032260332603426035260362603726038260392604026041260422604326044260452604626047260482604926050260512605226053260542605526056260572605826059260602606126062260632606426065260662606726068260692607026071260722607326074260752607626077260782607926080260812608226083260842608526086260872608826089260902609126092260932609426095260962609726098260992610026101261022610326104261052610626107261082610926110261112611226113261142611526116261172611826119261202612126122261232612426125261262612726128261292613026131261322613326134261352613626137261382613926140261412614226143261442614526146261472614826149261502615126152261532615426155261562615726158261592616026161261622616326164261652616626167261682616926170261712617226173261742617526176261772617826179261802618126182261832618426185261862618726188261892619026191261922619326194261952619626197261982619926200262012620226203262042620526206262072620826209262102621126212262132621426215262162621726218262192622026221262222622326224262252622626227262282622926230262312623226233262342623526236262372623826239262402624126242262432624426245262462624726248262492625026251262522625326254262552625626257262582625926260262612626226263262642626526266262672626826269262702627126272262732627426275262762627726278262792628026281262822628326284262852628626287262882628926290262912629226293262942629526296262972629826299263002630126302263032630426305263062630726308263092631026311263122631326314263152631626317263182631926320263212632226323263242632526326263272632826329263302633126332263332633426335263362633726338263392634026341263422634326344263452634626347263482634926350263512635226353263542635526356263572635826359263602636126362263632636426365263662636726368263692637026371263722637326374263752637626377263782637926380263812638226383263842638526386263872638826389263902639126392263932639426395263962639726398263992640026401264022640326404264052640626407264082640926410264112641226413264142641526416264172641826419264202642126422264232642426425264262642726428264292643026431264322643326434264352643626437264382643926440264412644226443264442644526446264472644826449264502645126452264532645426455264562645726458264592646026461264622646326464264652646626467264682646926470264712647226473264742647526476264772647826479264802648126482264832648426485264862648726488264892649026491264922649326494264952649626497264982649926500265012650226503265042650526506265072650826509265102651126512265132651426515265162651726518265192652026521265222652326524265252652626527265282652926530265312653226533265342653526536265372653826539265402654126542265432654426545265462654726548265492655026551265522655326554265552655626557265582655926560265612656226563265642656526566265672656826569265702657126572265732657426575265762657726578265792658026581265822658326584265852658626587265882658926590265912659226593265942659526596265972659826599266002660126602266032660426605266062660726608266092661026611266122661326614266152661626617266182661926620266212662226623266242662526626266272662826629266302663126632266332663426635266362663726638266392664026641266422664326644266452664626647266482664926650266512665226653266542665526656266572665826659266602666126662266632666426665266662666726668266692667026671266722667326674266752667626677266782667926680266812668226683266842668526686266872668826689266902669126692266932669426695266962669726698266992670026701267022670326704267052670626707267082670926710267112671226713267142671526716267172671826719267202672126722267232672426725267262672726728267292673026731267322673326734267352673626737267382673926740267412674226743267442674526746267472674826749267502675126752267532675426755267562675726758267592676026761267622676326764267652676626767267682676926770267712677226773267742677526776267772677826779267802678126782267832678426785267862678726788267892679026791267922679326794267952679626797267982679926800268012680226803268042680526806268072680826809268102681126812268132681426815268162681726818268192682026821268222682326824268252682626827268282682926830268312683226833268342683526836268372683826839268402684126842268432684426845268462684726848268492685026851268522685326854268552685626857268582685926860268612686226863268642686526866268672686826869268702687126872268732687426875268762687726878268792688026881268822688326884268852688626887268882688926890268912689226893268942689526896268972689826899269002690126902269032690426905269062690726908269092691026911269122691326914269152691626917269182691926920269212692226923269242692526926269272692826929269302693126932269332693426935269362693726938269392694026941269422694326944269452694626947269482694926950269512695226953269542695526956269572695826959269602696126962269632696426965269662696726968269692697026971269722697326974269752697626977269782697926980269812698226983269842698526986269872698826989269902699126992269932699426995269962699726998269992700027001270022700327004270052700627007270082700927010270112701227013270142701527016270172701827019270202702127022270232702427025270262702727028270292703027031270322703327034270352703627037270382703927040270412704227043270442704527046270472704827049270502705127052270532705427055270562705727058270592706027061270622706327064270652706627067270682706927070270712707227073270742707527076270772707827079270802708127082270832708427085270862708727088270892709027091270922709327094270952709627097270982709927100271012710227103271042710527106271072710827109271102711127112271132711427115271162711727118271192712027121271222712327124271252712627127271282712927130271312713227133271342713527136271372713827139271402714127142271432714427145271462714727148271492715027151271522715327154271552715627157271582715927160271612716227163271642716527166271672716827169271702717127172271732717427175271762717727178271792718027181271822718327184271852718627187271882718927190271912719227193271942719527196271972719827199272002720127202272032720427205272062720727208272092721027211272122721327214272152721627217272182721927220272212722227223272242722527226272272722827229272302723127232272332723427235272362723727238272392724027241272422724327244272452724627247272482724927250272512725227253272542725527256272572725827259272602726127262272632726427265272662726727268272692727027271272722727327274272752727627277272782727927280272812728227283272842728527286272872728827289272902729127292272932729427295272962729727298272992730027301273022730327304273052730627307273082730927310273112731227313273142731527316273172731827319273202732127322273232732427325273262732727328273292733027331273322733327334273352733627337273382733927340273412734227343273442734527346273472734827349273502735127352273532735427355273562735727358273592736027361273622736327364273652736627367273682736927370273712737227373273742737527376273772737827379273802738127382273832738427385273862738727388273892739027391273922739327394273952739627397273982739927400274012740227403274042740527406274072740827409274102741127412274132741427415274162741727418274192742027421274222742327424274252742627427274282742927430274312743227433274342743527436274372743827439274402744127442274432744427445274462744727448274492745027451274522745327454274552745627457274582745927460274612746227463274642746527466274672746827469274702747127472274732747427475274762747727478274792748027481274822748327484274852748627487274882748927490274912749227493274942749527496274972749827499275002750127502275032750427505275062750727508275092751027511275122751327514275152751627517275182751927520275212752227523275242752527526275272752827529275302753127532275332753427535275362753727538275392754027541275422754327544275452754627547275482754927550275512755227553275542755527556275572755827559275602756127562275632756427565275662756727568275692757027571275722757327574275752757627577275782757927580275812758227583275842758527586275872758827589275902759127592275932759427595275962759727598275992760027601276022760327604276052760627607276082760927610276112761227613276142761527616276172761827619276202762127622276232762427625276262762727628276292763027631276322763327634276352763627637276382763927640276412764227643276442764527646276472764827649276502765127652276532765427655276562765727658276592766027661276622766327664276652766627667276682766927670276712767227673276742767527676276772767827679276802768127682276832768427685276862768727688276892769027691276922769327694276952769627697276982769927700277012770227703277042770527706277072770827709277102771127712277132771427715277162771727718277192772027721277222772327724277252772627727277282772927730277312773227733277342773527736277372773827739277402774127742277432774427745277462774727748277492775027751277522775327754277552775627757277582775927760277612776227763277642776527766277672776827769277702777127772277732777427775277762777727778277792778027781277822778327784277852778627787277882778927790277912779227793277942779527796277972779827799278002780127802278032780427805278062780727808278092781027811278122781327814278152781627817278182781927820278212782227823278242782527826278272782827829278302783127832278332783427835278362783727838278392784027841278422784327844278452784627847278482784927850278512785227853278542785527856278572785827859278602786127862278632786427865278662786727868278692787027871278722787327874278752787627877278782787927880278812788227883278842788527886278872788827889278902789127892278932789427895278962789727898278992790027901279022790327904279052790627907279082790927910279112791227913279142791527916279172791827919279202792127922279232792427925279262792727928279292793027931279322793327934279352793627937279382793927940279412794227943279442794527946279472794827949279502795127952279532795427955279562795727958279592796027961279622796327964279652796627967279682796927970279712797227973279742797527976279772797827979279802798127982279832798427985279862798727988279892799027991279922799327994279952799627997279982799928000280012800228003280042800528006280072800828009280102801128012280132801428015280162801728018280192802028021280222802328024280252802628027280282802928030280312803228033280342803528036280372803828039280402804128042280432804428045280462804728048280492805028051280522805328054280552805628057280582805928060280612806228063280642806528066280672806828069280702807128072280732807428075280762807728078280792808028081280822808328084280852808628087280882808928090280912809228093280942809528096280972809828099281002810128102281032810428105281062810728108281092811028111281122811328114281152811628117281182811928120281212812228123281242812528126281272812828129281302813128132281332813428135281362813728138281392814028141281422814328144281452814628147281482814928150281512815228153281542815528156281572815828159281602816128162281632816428165281662816728168281692817028171281722817328174281752817628177281782817928180281812818228183281842818528186281872818828189281902819128192281932819428195281962819728198281992820028201282022820328204282052820628207282082820928210282112821228213282142821528216282172821828219282202822128222282232822428225282262822728228282292823028231282322823328234282352823628237282382823928240282412824228243282442824528246282472824828249282502825128252282532825428255282562825728258282592826028261282622826328264282652826628267282682826928270282712827228273282742827528276282772827828279282802828128282282832828428285282862828728288282892829028291282922829328294282952829628297282982829928300283012830228303283042830528306283072830828309283102831128312283132831428315283162831728318283192832028321283222832328324283252832628327283282832928330283312833228333283342833528336283372833828339283402834128342283432834428345283462834728348283492835028351283522835328354283552835628357283582835928360283612836228363283642836528366283672836828369283702837128372283732837428375283762837728378283792838028381283822838328384283852838628387283882838928390283912839228393283942839528396283972839828399284002840128402284032840428405284062840728408284092841028411284122841328414284152841628417284182841928420284212842228423284242842528426284272842828429284302843128432284332843428435284362843728438284392844028441284422844328444284452844628447284482844928450284512845228453284542845528456284572845828459284602846128462284632846428465284662846728468284692847028471284722847328474284752847628477284782847928480284812848228483284842848528486284872848828489284902849128492284932849428495284962849728498284992850028501285022850328504285052850628507285082850928510285112851228513285142851528516285172851828519285202852128522285232852428525285262852728528285292853028531285322853328534285352853628537285382853928540285412854228543285442854528546285472854828549285502855128552285532855428555285562855728558285592856028561285622856328564285652856628567285682856928570285712857228573285742857528576285772857828579285802858128582285832858428585285862858728588285892859028591285922859328594285952859628597285982859928600286012860228603286042860528606286072860828609286102861128612286132861428615286162861728618286192862028621286222862328624286252862628627286282862928630286312863228633286342863528636286372863828639286402864128642286432864428645286462864728648286492865028651286522865328654286552865628657286582865928660286612866228663286642866528666286672866828669286702867128672286732867428675286762867728678286792868028681286822868328684286852868628687286882868928690286912869228693286942869528696286972869828699287002870128702287032870428705287062870728708287092871028711287122871328714287152871628717287182871928720287212872228723287242872528726287272872828729287302873128732287332873428735287362873728738287392874028741287422874328744287452874628747287482874928750287512875228753287542875528756287572875828759287602876128762287632876428765287662876728768287692877028771287722877328774287752877628777287782877928780287812878228783287842878528786287872878828789287902879128792287932879428795287962879728798287992880028801288022880328804288052880628807288082880928810288112881228813288142881528816288172881828819288202882128822288232882428825288262882728828288292883028831288322883328834288352883628837288382883928840288412884228843288442884528846288472884828849288502885128852288532885428855288562885728858288592886028861288622886328864288652886628867288682886928870288712887228873288742887528876288772887828879288802888128882288832888428885288862888728888288892889028891288922889328894288952889628897288982889928900289012890228903289042890528906289072890828909289102891128912289132891428915289162891728918289192892028921289222892328924289252892628927289282892928930289312893228933289342893528936289372893828939289402894128942289432894428945289462894728948289492895028951289522895328954289552895628957289582895928960289612896228963289642896528966289672896828969289702897128972289732897428975289762897728978289792898028981289822898328984289852898628987289882898928990289912899228993289942899528996289972899828999290002900129002290032900429005290062900729008290092901029011290122901329014290152901629017290182901929020290212902229023290242902529026290272902829029290302903129032290332903429035290362903729038290392904029041290422904329044290452904629047290482904929050290512905229053290542905529056290572905829059290602906129062290632906429065290662906729068290692907029071290722907329074290752907629077290782907929080290812908229083290842908529086290872908829089290902909129092290932909429095290962909729098290992910029101291022910329104291052910629107291082910929110291112911229113291142911529116291172911829119291202912129122291232912429125291262912729128291292913029131291322913329134291352913629137291382913929140291412914229143291442914529146291472914829149291502915129152291532915429155291562915729158291592916029161291622916329164291652916629167291682916929170291712917229173291742917529176291772917829179291802918129182291832918429185291862918729188291892919029191291922919329194291952919629197291982919929200292012920229203292042920529206292072920829209292102921129212292132921429215292162921729218292192922029221292222922329224292252922629227292282922929230292312923229233292342923529236292372923829239292402924129242292432924429245292462924729248292492925029251292522925329254292552925629257292582925929260292612926229263292642926529266292672926829269292702927129272292732927429275292762927729278292792928029281292822928329284292852928629287292882928929290292912929229293292942929529296292972929829299293002930129302293032930429305293062930729308293092931029311293122931329314293152931629317293182931929320293212932229323293242932529326293272932829329293302933129332293332933429335293362933729338293392934029341293422934329344293452934629347293482934929350293512935229353293542935529356293572935829359293602936129362293632936429365293662936729368293692937029371293722937329374293752937629377293782937929380293812938229383293842938529386293872938829389293902939129392293932939429395293962939729398293992940029401294022940329404294052940629407294082940929410294112941229413294142941529416294172941829419294202942129422294232942429425294262942729428294292943029431294322943329434294352943629437294382943929440294412944229443294442944529446294472944829449294502945129452294532945429455294562945729458294592946029461294622946329464294652946629467294682946929470294712947229473294742947529476294772947829479294802948129482294832948429485294862948729488294892949029491294922949329494294952949629497294982949929500295012950229503295042950529506295072950829509295102951129512295132951429515295162951729518295192952029521295222952329524295252952629527295282952929530295312953229533295342953529536295372953829539295402954129542295432954429545295462954729548295492955029551295522955329554295552955629557295582955929560295612956229563295642956529566295672956829569295702957129572295732957429575295762957729578295792958029581295822958329584295852958629587295882958929590295912959229593295942959529596295972959829599296002960129602296032960429605296062960729608296092961029611296122961329614296152961629617296182961929620296212962229623296242962529626296272962829629296302963129632296332963429635296362963729638296392964029641296422964329644296452964629647296482964929650296512965229653296542965529656296572965829659296602966129662296632966429665296662966729668296692967029671296722967329674296752967629677296782967929680296812968229683296842968529686296872968829689296902969129692296932969429695296962969729698296992970029701297022970329704297052970629707297082970929710297112971229713297142971529716297172971829719297202972129722297232972429725297262972729728297292973029731297322973329734297352973629737297382973929740297412974229743297442974529746297472974829749297502975129752297532975429755297562975729758297592976029761297622976329764297652976629767297682976929770297712977229773297742977529776297772977829779297802978129782297832978429785297862978729788297892979029791297922979329794297952979629797297982979929800298012980229803298042980529806298072980829809298102981129812298132981429815298162981729818298192982029821298222982329824298252982629827298282982929830298312983229833298342983529836298372983829839298402984129842298432984429845298462984729848298492985029851298522985329854298552985629857298582985929860298612986229863298642986529866298672986829869298702987129872298732987429875298762987729878298792988029881298822988329884298852988629887298882988929890298912989229893298942989529896298972989829899299002990129902299032990429905299062990729908299092991029911299122991329914299152991629917299182991929920299212992229923299242992529926299272992829929299302993129932299332993429935299362993729938299392994029941299422994329944299452994629947299482994929950299512995229953299542995529956299572995829959299602996129962299632996429965299662996729968299692997029971299722997329974299752997629977299782997929980299812998229983299842998529986299872998829989299902999129992299932999429995299962999729998299993000030001300023000330004300053000630007300083000930010300113001230013300143001530016300173001830019300203002130022300233002430025300263002730028300293003030031300323003330034300353003630037300383003930040300413004230043300443004530046300473004830049300503005130052300533005430055300563005730058300593006030061300623006330064300653006630067300683006930070300713007230073300743007530076300773007830079300803008130082300833008430085300863008730088300893009030091300923009330094300953009630097300983009930100301013010230103301043010530106301073010830109301103011130112301133011430115301163011730118301193012030121301223012330124301253012630127301283012930130301313013230133301343013530136301373013830139301403014130142301433014430145301463014730148301493015030151301523015330154301553015630157301583015930160301613016230163301643016530166301673016830169301703017130172301733017430175301763017730178301793018030181301823018330184301853018630187301883018930190301913019230193301943019530196301973019830199302003020130202302033020430205302063020730208302093021030211302123021330214302153021630217302183021930220302213022230223302243022530226302273022830229302303023130232302333023430235302363023730238302393024030241302423024330244302453024630247302483024930250302513025230253302543025530256302573025830259302603026130262302633026430265302663026730268302693027030271302723027330274302753027630277302783027930280302813028230283302843028530286302873028830289302903029130292302933029430295302963029730298302993030030301303023030330304303053030630307303083030930310303113031230313303143031530316303173031830319303203032130322303233032430325303263032730328303293033030331303323033330334303353033630337303383033930340303413034230343303443034530346303473034830349303503035130352303533035430355303563035730358303593036030361303623036330364303653036630367303683036930370303713037230373303743037530376303773037830379303803038130382303833038430385303863038730388303893039030391303923039330394303953039630397303983039930400304013040230403304043040530406304073040830409304103041130412304133041430415304163041730418304193042030421304223042330424304253042630427304283042930430304313043230433304343043530436304373043830439304403044130442304433044430445304463044730448304493045030451304523045330454304553045630457304583045930460304613046230463304643046530466304673046830469304703047130472304733047430475304763047730478304793048030481304823048330484304853048630487304883048930490304913049230493304943049530496304973049830499305003050130502305033050430505305063050730508305093051030511305123051330514305153051630517305183051930520305213052230523305243052530526305273052830529305303053130532305333053430535305363053730538305393054030541305423054330544305453054630547305483054930550305513055230553305543055530556305573055830559305603056130562305633056430565305663056730568305693057030571305723057330574305753057630577305783057930580305813058230583305843058530586305873058830589305903059130592305933059430595305963059730598305993060030601306023060330604306053060630607306083060930610306113061230613306143061530616306173061830619306203062130622306233062430625306263062730628306293063030631306323063330634306353063630637306383063930640306413064230643306443064530646306473064830649306503065130652306533065430655306563065730658306593066030661306623066330664306653066630667306683066930670306713067230673306743067530676306773067830679306803068130682306833068430685306863068730688306893069030691306923069330694306953069630697306983069930700307013070230703307043070530706307073070830709307103071130712307133071430715307163071730718307193072030721307223072330724307253072630727307283072930730307313073230733307343073530736307373073830739307403074130742307433074430745307463074730748307493075030751307523075330754307553075630757307583075930760307613076230763307643076530766307673076830769307703077130772307733077430775307763077730778307793078030781307823078330784307853078630787307883078930790307913079230793307943079530796307973079830799308003080130802308033080430805308063080730808308093081030811308123081330814308153081630817308183081930820308213082230823308243082530826308273082830829308303083130832308333083430835308363083730838308393084030841308423084330844308453084630847308483084930850308513085230853308543085530856308573085830859308603086130862308633086430865308663086730868308693087030871308723087330874308753087630877308783087930880308813088230883308843088530886308873088830889308903089130892308933089430895308963089730898308993090030901309023090330904309053090630907309083090930910309113091230913309143091530916309173091830919309203092130922309233092430925309263092730928309293093030931309323093330934309353093630937309383093930940309413094230943309443094530946309473094830949309503095130952309533095430955309563095730958309593096030961309623096330964309653096630967309683096930970309713097230973309743097530976309773097830979309803098130982309833098430985309863098730988309893099030991309923099330994309953099630997309983099931000310013100231003310043100531006310073100831009310103101131012310133101431015310163101731018310193102031021310223102331024310253102631027310283102931030310313103231033310343103531036310373103831039310403104131042310433104431045310463104731048310493105031051310523105331054310553105631057310583105931060310613106231063310643106531066310673106831069310703107131072310733107431075310763107731078310793108031081310823108331084310853108631087310883108931090310913109231093310943109531096310973109831099311003110131102311033110431105311063110731108311093111031111311123111331114311153111631117311183111931120311213112231123311243112531126311273112831129311303113131132311333113431135311363113731138311393114031141311423114331144311453114631147311483114931150311513115231153311543115531156311573115831159311603116131162311633116431165311663116731168311693117031171311723117331174311753117631177311783117931180311813118231183311843118531186311873118831189311903119131192311933119431195311963119731198311993120031201312023120331204312053120631207312083120931210312113121231213312143121531216312173121831219312203122131222312233122431225312263122731228312293123031231312323123331234312353123631237312383123931240312413124231243312443124531246312473124831249312503125131252312533125431255312563125731258312593126031261312623126331264312653126631267312683126931270312713127231273312743127531276312773127831279312803128131282312833128431285312863128731288312893129031291312923129331294312953129631297312983129931300313013130231303313043130531306313073130831309313103131131312313133131431315313163131731318313193132031321313223132331324313253132631327313283132931330313313133231333313343133531336313373133831339313403134131342313433134431345313463134731348313493135031351313523135331354313553135631357313583135931360313613136231363313643136531366313673136831369313703137131372313733137431375313763137731378313793138031381313823138331384313853138631387313883138931390313913139231393313943139531396313973139831399314003140131402314033140431405314063140731408314093141031411314123141331414314153141631417314183141931420314213142231423314243142531426314273142831429314303143131432314333143431435314363143731438314393144031441314423144331444314453144631447314483144931450314513145231453314543145531456314573145831459314603146131462314633146431465314663146731468314693147031471314723147331474314753147631477314783147931480314813148231483314843148531486314873148831489314903149131492314933149431495314963149731498314993150031501315023150331504315053150631507315083150931510315113151231513315143151531516315173151831519315203152131522315233152431525315263152731528315293153031531315323153331534315353153631537315383153931540315413154231543315443154531546315473154831549315503155131552315533155431555315563155731558315593156031561315623156331564315653156631567315683156931570315713157231573315743157531576315773157831579315803158131582315833158431585315863158731588315893159031591315923159331594315953159631597315983159931600316013160231603316043160531606316073160831609316103161131612316133161431615316163161731618316193162031621316223162331624316253162631627316283162931630316313163231633316343163531636316373163831639316403164131642316433164431645316463164731648316493165031651316523165331654316553165631657316583165931660316613166231663316643166531666316673166831669316703167131672316733167431675316763167731678316793168031681316823168331684316853168631687316883168931690316913169231693316943169531696316973169831699317003170131702317033170431705317063170731708317093171031711317123171331714317153171631717317183171931720317213172231723317243172531726317273172831729317303173131732317333173431735317363173731738317393174031741317423174331744317453174631747317483174931750317513175231753317543175531756317573175831759317603176131762317633176431765 |
- <template>
- <div class="chat-container">
- <!-- 最左侧边栏 -->
- <Sidebar />
- <!-- 中间历史记录区域 -->
- <div class="history-sidebar" :class="{ 'disabled': isProcessing }">
- <div class="history-header">
- <span class="section-title">历史记录</span>
- <img src="@/assets/Chat/2.png" alt="新建任务" class="new-chat-btn" @click="handleNewChatClick"
- :class="{ 'disabled': isProcessing }">
- <!-- 测试按钮 -->
- <!-- <button @click="isProcessing = !isProcessing" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">
- 测试切换: {{ isProcessing ? '处理中' : '空闲' }}
- </button> -->
- </div>
- <div class="history-list">
- <!-- 历史记录加载状态 -->
- <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
- <div class="loading-spinner"></div>
- <div class="loading-text">正在加载历史记录...</div>
- </div>
- <!-- 有历史记录时显示 -->
- <div v-else-if="historyTotal > 0" v-for="(item, index) in historyData" :key="index"
- :class="['history-item', { active: item.isActive, disabled: isProcessing }]"
- @click="() => handleHistoryItemClick(item, index)"
- :style="{ cursor: (item.isActive || isProcessing) ? 'default' : 'pointer' }">
- <div class="history-icon">
- <img :src="getHistoryImage(item)" alt="培训图标" class="history-icon-img">
- </div>
- <div class="history-content">
- <div class="history-title">{{ item.title }}</div>
- <div class="history-meta">
- <span class="history-time">{{ item.time }}</span>
- <!-- <span class="history-pages">{{ item.pages }}页</span> -->
- </div>
- </div>
- <div class="delete-btn" @click.stop="deleteHistoryItem(item, index)"
- :class="{ 'always-visible': item.isActive }">
- <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
- </div>
- </div>
- <!-- 无历史记录时显示空状态 -->
- <div v-else class="empty-history">
- <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
- <div class="empty-text">暂无数据</div>
- </div>
- </div>
- </div>
- <!-- 右侧工作区域 -->
- <div class="main-work">
- <!-- 头部 -->
- <div class="work-header">
- <h2>安全培训</h2>
- </div>
- <!-- 工作内容区域 -->
- <div class="work-content">
- <!-- 步骤一:AI聊天界面 -->
- <div v-if="currentStep === 'step1'" class="step1-content">
- <!-- 初始状态:AI助手介绍和功能卡片 -->
- <div v-if="!showChat" class="initial-content">
- <!-- AI助手介绍 -->
- <div class="ai-intro">
- <div class="ai-avatar">
- <img src="@/assets/Safety/5.png" alt="AI头像" class="ai-avatar-img">
- </div>
- <div class="ai-greeting">
- <h3>快速生成专业安全培训材料</h3>
- <p>输入培训主题,一键生成培训大纲与PPT模板</p>
- </div>
- </div>
- <!-- 功能卡片 -->
- <div class="function-cards">
- <div v-for="(card, index) in functionCards" :key="card.id || index" class="function-card"
- @click="handleFunctionCard(card.function_title)">
- <div class="card-header">
- <div class="card-icon">
- <img :src="getFunctionCardIcon(card.function_title)" :alt="card.function_title"
- class="card-icon-img">
- </div>
- <h4>{{ card.function_title }}</h4>
- </div>
- <div class="card-description">
- <p>{{ card.function_content }}</p>
- </div>
- </div>
- <!-- 如果没有数据,显示默认卡片 -->
- <div v-if="functionCards.length === 0" class="function-card"
- @click="handleFunctionCard('safety-training')">
- <div class="card-header">
- <div class="card-icon">
- <img src="@/assets/Safety/4.png" alt="安全培训课程" class="card-icon-img">
- </div>
- <h4>安全培训课程</h4>
- </div>
- <div class="card-description">
- <p>施工安全培训,操作规范学习</p>
- </div>
- </div>
- <div v-if="functionCards.length === 0" class="function-card"
- @click="handleFunctionCard('safety-assessment')">
- <div class="card-header">
- <div class="card-icon">
- <img src="@/assets/Safety/3.png" alt="安全评估" class="card-icon-img">
- </div>
- <h4>安全评估测试</h4>
- </div>
- <div class="card-description">
- <p>安全知识测评,能力水平评估</p>
- </div>
- </div>
- <div v-if="functionCards.length === 0" class="function-card"
- @click="handleFunctionCard('safety-regulations')">
- <div class="card-header">
- <div class="card-icon">
- <img src="@/assets/Safety/2.png" alt="安全法规" class="card-icon-img">
- </div>
- <h4>安全法规查询</h4>
- </div>
- <div class="card-description">
- <p>安全法律法规,标准规范查询</p>
- </div>
- </div>
- <div v-if="functionCards.length === 0" class="function-card"
- @click="handleFunctionCard('emergency-procedures')">
- <div class="card-header">
- <div class="card-icon">
- <img src="@/assets/Safety/1.png" alt="应急程序" class="card-icon-img">
- </div>
- <h4>应急处理程序</h4>
- </div>
- <div class="card-description">
- <p>事故应急预案,处理流程指导</p>
- </div>
- </div>
- </div>
- </div>
- <!-- 聊天对话区域 -->
- <div v-else class="chat-content">
- <div class="chat-messages">
- <div v-for="(message, index) in chatMessages" :key="index" :class="['message-item', message.type]">
- <!-- 用户消息 -->
- <div v-if="message.type === 'user'" class="user-message">
- <div class="message-content">
- <!-- 文件显示 -->
- <div v-if="message.file" class="message-file">
- <div class="file-display">
- <div class="file-icon">
- <img v-if="message.file.type === '.doc' || message.file.type === '.docx'" :src="message.file.icon" alt="文档图标" class="file-icon-img">
- <span v-else>{{ message.file.icon }}</span>
- </div>
- <div class="file-details">
- <div class="file-name">{{ message.file.name }}</div>
- <div class="file-size">{{ formatFileSize(message.file.size) }}</div>
- </div>
- </div>
- </div>
- <!-- 文本内容 -->
- <div v-if="message.content" class="message-text">{{ message.content }}</div>
- </div>
- <div class="message-actions">
- <button class="action-btn copy-btn" @click="copyUserMessage(message)">
- <img src="@/assets/AIWriting/5.png" alt="复制" class="action-icon">
- 复制
- </button>
- <button class="action-btn edit-btn">
- <img src="@/assets/AIWriting/6.png" alt="编辑" class="action-icon">
- 编辑
- </button>
- </div>
- </div>
- <!-- AI消息 -->
- <div v-else-if="message.type === 'ai'" class="ai-message">
- <div class="ai-avatar-small">
- <img src="@/assets/Safety/5.png" alt="AI" class="ai-icon">
- </div>
- <div class="message-content" :data-message-index="index">
- <div class="ai-text">
- <div v-if="message.displayContent.length === 0" class="typing-indicator">
- <div class="thinking-animation">
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- </div>
- <span>AI正在思考中...</span>
- </div>
- <div v-else v-html="message.displayContent" class="ai-content"></div>
- </div>
- <!-- 移除底部按钮,因为用户看不到这些按钮,AI回复完成后直接跳转 -->
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 步骤二:培训大纲界面 -->
- <div v-if="currentStep === 'step2'" class="step2-content">
- <!-- 加载状态 -->
- <div v-if="isLoadingHistory" class="loading-overlay">
- <div class="loading-content">
- <div class="loading-spinner"></div>
- <div class="loading-text">正在加载历史记录</div>
- <div class="loading-subtitle">请稍候,正在为您准备数据...</div>
- </div>
- </div>
- <div class="outline-container">
- <!-- 左侧大纲内容 -->
- <div class="outline-main">
- <div class="outline-header">
- <div class="outline-title-container">
- <h3 v-if="editingType !== 'title'" class="outline-title"
- @click="!isGeneratingOutline && !isGeneratingExam && startEditing(null, 'title', null, outlineTitle || '安全培训大纲')"
- :class="{ 'disabled': isGeneratingOutline || isGeneratingExam }">
- {{ outlineTitle || '安全培训大纲' }}
- </h3>
- <div v-else class="edit-input-container">
- <textarea v-model="editingContent" class="edit-textarea title-edit-textarea" @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="titleEditInput" autofocus></textarea>
- </div>
- </div>
- <div class="outline-actions">
- <button class="action-btn" @click="copyEntireOutline" :disabled="isGeneratingOutline">
- <img src="@/assets/AIWriting/12.png" alt="复制" class="action-icon">
- 复制
- </button>
- <button class="action-btn" @click="downloadOutlineAsWord" :disabled="isGeneratingOutline">
- <img src="@/assets/AIWriting/13.png" alt="下载" class="action-icon">
- 下载
- </button>
- </div>
- </div>
- <div class="outline-content" :class="{ 'disabled': isGeneratingOutline }">
- <!-- 生成中遮罩层 -->
- <div v-if="isGeneratingOutline" class="generating-overlay">
- <div class="generating-content">
- <p>AI正在生成新大纲,请稍候...</p>
- </div>
- </div>
- <div v-if="isGeneratingExam" class="generating-overlay">
- <div class="generating-content">
- <p>AI正在生成考试题目,请稍候...</p>
- </div>
- </div>
- <!-- <div class="zoom-controls">
- <button class="zoom-btn">
- <img src="@/assets/Safety/7.png" alt="编辑" class="zoom-icon">
- </button>
- <button class="zoom-btn">
- <img src="@/assets/Safety/8.png" alt="新增" class="zoom-icon">
- </button>
- <button class="zoom-btn">
- <img src="@/assets/Safety/9.png" alt="删除" class="zoom-icon">
- </button>
- </div> -->
- <!-- 动态大纲内容 -->
- <div v-if="outlineData && outlineData.length > 0" class="outline-content-scrollable">
- <template v-for="(chapter, chapterIndex) in outlineData" :key="chapterIndex">
- <div v-if="chapter && chapter.sections" class="outline-chapter" :class="{
- 'dragging': draggedChapterIndex === chapterIndex,
- 'drag-over': dragOverChapterIndex === chapterIndex && draggedChapterIndex !== chapterIndex
- }" draggable="true" @dragstart="handleDragStart($event, chapterIndex)" @dragend="handleDragEnd"
- @dragover="handleDragOver($event, chapterIndex)" @dragleave="handleDragLeave"
- @drop="handleDrop($event, chapterIndex)">
- <div class="chapter-header">
- <h4 v-if="editingType !== 'chapter' || editingIndex !== chapterIndex" class="chapter-title"
- @click="!isGeneratingExam && startEditing(chapter, 'chapter', chapterIndex, getCleanChapterTitle(chapter.title))"
- :class="{ 'disabled': isGeneratingExam }">
- {{ getDisplayChapterTitle(chapter.title, chapterIndex) }}
- </h4>
- <div v-else class="edit-input-container">
- <div class="edit-input-wrapper">
- <textarea v-model="editingContent" class="edit-textarea chapter-edit-textarea"
- @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="chapterEditInput"
- autofocus></textarea>
- <!-- 编辑选项 - 显示在输入框内 -->
- <div class="edit-options-inline">
- <button class="edit-option-btn" @click="addNewItem('section', chapterIndex)">
- <img src="@/assets/Safety/8.png" alt="添加小节" class="edit-icon">
- </button>
- <button v-if="outlineData.length > 2" class="edit-option-btn delete-btn"
- @click="deleteItem('chapter', chapterIndex)">
- <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
- </button>
- </div>
- </div>
- </div>
- </div>
- <div class="outline-section">
- <template v-for="(section, sectionIndex) in chapter.sections" :key="sectionIndex">
- <div v-if="section &&
- section.title !== '内容要点' &&
- section.title !== '概述' &&
- section.title !== '内容详情'" class="section-container">
- <div class="section-header">
- <div
- v-if="editingType !== 'section' || editingIndex !== `${chapterIndex}-${sectionIndex}`"
- class="section-title"
- @click="!isGeneratingExam && startEditing(section, 'section', `${chapterIndex}-${sectionIndex}`, section.title)"
- :class="{ 'disabled': isGeneratingExam }">
- {{ section.title }}
- </div>
- <div v-else class="edit-input-container">
- <div class="edit-input-wrapper">
- <textarea v-model="editingContent" class="edit-textarea section-edit-textarea"
- @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit"
- ref="sectionEditInput" autofocus></textarea>
- <!-- 编辑选项 - 显示在输入框内 -->
- <div class="edit-options-inline">
- <button class="edit-option-btn"
- @click="addNewItem('subsection', `${chapterIndex}-${sectionIndex}`)">
- <img src="@/assets/Safety/8.png" alt="添加子标题" class="edit-icon">
- </button>
- <button v-if="chapter.sections.length > 1" class="edit-option-btn delete-btn"
- @click="deleteItem('section', `${chapterIndex}-${sectionIndex}`)">
- <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 子小节单独渲染,使用不同的类名 -->
- <div v-if="section && section.subsections && section.subsections.length > 0"
- class="section-subsection">
- <template v-for="(subsection, subsectionIndex) in section.subsections"
- :key="subsectionIndex">
- <div v-if="subsection &&
- subsection.title !== '内容要点' &&
- subsection.title !== '概述' &&
- subsection.title !== '内容详情' &&
- !subsection.title.includes('总章节数') &&
- !subsection.title.includes('总小节数') &&
- !subsection.title.includes('预计PPT页数') &&
- !subsection.title.includes('预计讲解时长')" class="subsection-container">
- <div class="subsection-header">
- <div
- v-if="editingType !== 'subsection' || editingIndex !== `${chapterIndex}-${sectionIndex}-${subsectionIndex}`"
- class="subsection-title"
- @click="!isGeneratingExam && startEditing(subsection, 'subsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}`, subsection.title)"
- :class="{ 'disabled': isGeneratingExam }">
- {{ subsection.title }}
- </div>
- <div v-else class="edit-input-container">
- <div class="edit-input-wrapper">
- <textarea v-model="editingContent" class="edit-textarea subsection-edit-textarea"
- @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit"
- ref="subsectionEditInput" autofocus></textarea>
- <!-- 编辑选项 - 显示在输入框内 -->
- <div class="edit-options-inline">
- <button class="edit-option-btn delete-btn"
- @click="deleteItem('subsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}`)">
- <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 具体内容要点(-开头) -->
- <div v-if="subsection.subsubsections && subsection.subsubsections.length > 0"
- class="subsubsection-container">
- <template v-for="(subsubsection, subsubsectionIndex) in subsection.subsubsections"
- :key="subsubsectionIndex">
- <div class="subsubsection-item">
- <div class="subsubsection-header">
- <div
- v-if="editingType !== 'subsubsection' || editingIndex !== `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${subsubsectionIndex}`"
- class="subsubsection-title"
- @click="!isGeneratingExam && startEditing(subsubsection, 'subsubsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${subsubsectionIndex}`, subsubsection.title)"
- :class="{ 'disabled': isGeneratingExam }">
- {{ subsubsection.title }}
- </div>
- <div v-else class="edit-input-container">
- <div class="edit-input-wrapper">
- <textarea v-model="editingContent"
- class="edit-textarea subsubsection-edit-textarea"
- @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="subsubsectionEditInput"
- autofocus></textarea>
- <!-- 编辑选项 - 显示在输入框内 -->
- <div class="edit-options-inline">
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- <!-- 添加新章节按钮 -->
- <div v-if="outlineData.length < 6" class="add-chapter-container">
- <button class="add-chapter-btn" @click="addNewItem('chapter', null)">
- <img src="@/assets/Safety/8.png" alt="添加章节" class="add-icon">
- <span>添加新章节</span>
- </button>
- </div>
- </div>
- <!-- 默认大纲内容(当没有AI生成内容时显示) -->
- <div v-else>
- <div class="outline-chapter">
- <h4>第一章 安全生产基本原则</h4>
- <div class="outline-section">
- <div class="section-item">1.1 安全生产的重要性</div>
- <div class="section-item">1.2 安全生产相关法规</div>
- <div class="section-subsection">
- <div class="subsection-item">1.2.1 《中华人民共和国安全生产法》解读</div>
- <div class="subsection-item">1.2.2 建筑工程安全管理规范</div>
- </div>
- </div>
- </div>
- <div class="outline-chapter">
- <h4>第二章 施工现场安全管理</h4>
- <div class="outline-section">
- <div class="section-item">2.1 安全责任制度</div>
- <div class="section-item">2.2 安全教育培训</div>
- <div class="section-item">2.3 安全检查与隐患排查</div>
- </div>
- </div>
- <div class="outline-chapter">
- <h4>第三章 常见安全隐患及防范措施</h4>
- <div class="outline-section">
- <div class="section-item">3.1 高空作业安全</div>
- <div class="section-subsection">
- <div class="subsection-item">3.1.1 脚手架搭设及使用安全规范</div>
- </div>
- <div class="section-item">3.2 用电安全</div>
- <div class="section-item">3.3 消防安全</div>
- </div>
- </div>
- <div class="outline-chapter">
- <h4>第四章 安全事故案例分析</h4>
- <div class="outline-section">
- <div class="section-item">4.1 典型事故分析与教训</div>
- </div>
- </div>
- <div class="outline-chapter">
- <h4>第五章 总结与展望</h4>
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧统计信息面板 -->
- <div class="outline-sidebar">
- <!-- 大纲统计信息 -->
- <div class="sidebar-section">
- <div class="section-header">
- <img src="@/assets/Safety/10.png" alt="统计" class="section-icon">
- <h5>大纲统计信息</h5>
- </div>
- <div class="section-content">
- <div class="stat-item">
- <span class="stat-label">总章节数:</span>
- <span class="stat-value">{{ outlineStats.totalChapters || 0 }}章</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">总小节数:</span>
- <span class="stat-value">{{ outlineStats.totalSections || 0 }}小节</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">预计PPT页数:</span>
- <span class="stat-value">{{ outlineStats.estimatedPages || '0页' }}</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">预计讲解时长:</span>
- <span class="stat-value">{{ outlineStats.estimatedTime || '0分钟' }}</span>
- </div>
- </div>
- </div>
- <!-- 大纲编辑提示 -->
- <div class="sidebar-section">
- <div class="section-header">
- <img src="@/assets/Safety/11.png" alt="提示" class="section-icon">
- <h5>大纲编辑提示</h5>
- </div>
- <div class="section-content">
- <ul class="tip-list">
- <li>点击标题文本可直接在白色区域编辑内容</li>
- <li>悬停在章节上将显示编辑选项</li>
- <li>编辑完成后系统会自动保存更改</li>
- <li>如果内容较多,滚动白色区域可浏览更多大纲内容</li>
- <li>章节顺序可以通过拖拽调整</li>
- </ul>
- </div>
- </div>
- <!-- 大纲评价 -->
- <div class="sidebar-section">
- <div class="section-header">
- <h5>大纲评价</h5>
- </div>
- <div class="section-content">
- <p class="evaluation-question">这个大纲对您的需求满意度如何?</p>
- <div class="evaluation-buttons">
- <button class="eval-btn satisfied" :class="{ active: getEvaluationStatus() === 'satisfied' }"
- @click="setEvaluation('satisfied')" :disabled="isGeneratingOutline">
- <img src="@/assets/AIWriting/10.png" alt="满意" class="eval-icon">
- 满意
- </button>
- <button class="eval-btn unsatisfied" :class="{ active: getEvaluationStatus() === 'unsatisfied' }"
- @click="setEvaluation('unsatisfied')" :disabled="isGeneratingOutline">
- <img src="@/assets/AIWriting/11.png" alt="不满意" class="eval-icon">
- 不满意
- </button>
- </div>
- </div>
- </div>
- <!-- 操作按钮 -->
- <div class="sidebar-actions">
- <button class="action-btn secondary" @click="generateNewOutline"
- :disabled="isGeneratingOutline || isGeneratingExam">
- <img src="@/assets/Safety/12.png" alt="刷新" class="action-icon"
- :class="{ 'rotating': isGeneratingOutline }">
- 生成新大纲
- </button>
- <button class="action-btn primary" @click="openWPS" :disabled="isGeneratingOutline || isGeneratingExam"
- v-if="!currentPPTInfo">
- 继续创作
- <img src="@/assets/Safety/13.png" alt="箭头" class="action-icon">
- </button>
- <!-- <button class="action-btn wps" @click="goToStep3" :disabled="isGeneratingOutline">
- <img src="@/assets/Safety/28.png" alt="WPS AI PPT" class="action-icon">
- WPS AI PPT
- </button> -->
- <!-- 动态生成的打开PPT按钮 -->
- <button v-if="showOpenPPTButton && currentPPTInfo" class="action-btn primary" @click="openTestPPT"
- style="background: #EA580C; color: #fff;">
- <!-- <img src="@/assets/Safety/28.png" alt="打开PPT" class="action-icon"> -->
- 修改PPT模板
- <img src="@/assets/Safety/13.png" alt="箭头" class="action-icon">
- </button>
- <!-- 测试按钮 - 直接显示用于测试 -->
- <!-- <button class="action-btn test-btn" @click="openTestPPT"
- style="background: linear-gradient(135deg, #ff7875, #ff9c6e); margin-left: 10px; font-size: 12px;">
- 测试PPT查看
- </button> -->
- </div>
- </div>
- </div>
- </div>
- <!-- 步骤三:PPT模板选择界面 -->
- <div v-if="currentStep === 'step3'" class="step3-content" :class="{ 'disabled': isApplyingTemplate }">
- <!-- 加载状态 -->
- <div v-if="isLoadingHistory" class="loading-overlay">
- <div class="loading-content">
- <div class="loading-spinner"></div>
- <div class="loading-text">正在加载历史记录</div>
- <div class="loading-subtitle">请稍候,正在为您准备数据...</div>
- </div>
- </div>
- <div class="template-container">
- <!-- 应用模板中遮罩层 -->
- <div v-if="isApplyingTemplate" class="applying-overlay">
- <div class="applying-content">
- <p>AI正在填充内容并应用模板,请稍候...</p>
- </div>
- </div>
- <!-- 左侧PPT预览区域 -->
- <div class="template-preview">
- <div class="preview-container">
- <div class="preview-header">
- <h3 class="preview-title">预览效果</h3>
- <span class="save-status">已保存于 {{ currentTime }}</span>
- </div>
- <!-- 大图轮播区域 -->
- <div class="main-carousel">
- <!-- 预览模式:显示轮播图 -->
- <div v-if="!showDownloadOptions">
- <button class="carousel-btn prev" @click="prevSlide">
- <img src="@/assets/Safety/23.png" alt="上一页" class="carousel-icon">
- </button>
- <div class="main-slide">
- <!-- 如果有AI生成的PPT数据,显示AI内容 -->
- <div v-if="generatedPPT && generatedPPT.length > 0" class="ai-generated-slide">
- <div class="slide-content" :style="{ background: getCurrentPPTSlideBackground() }">
- <div v-for="(element, index) in getCurrentPPTSlide().elements" :key="element.id"
- class="slide-element" :style="getElementStyle(element)">
- <div v-if="element.type === 'text'" v-html="element.content"></div>
- <img v-else-if="element.type === 'image' && element.src" :src="element.src"
- :alt="element.id">
- </div>
- </div>
- </div>
- <!-- 否则显示默认图片 -->
- <img v-else :src="currentSlideImage" :alt="`第${currentSlideIndex + 1}页`" class="slide-image">
- </div>
- <button class="carousel-btn next" @click="nextSlide">
- <img src="@/assets/Safety/24.png" alt="下一页" class="carousel-icon">
- </button>
- </div>
- <!-- PPT编辑工具台 -->
- <div v-if="showDownloadOptions" class="ppt-editor-workspace">
- <div class="editor-canvas">
- <!-- PPT预览模式 -->
- <div v-if="showDownloadOptions && generatedPPT.length > 0" class="slide-preview" :style="{
- transform: `scale(${zoom})`,
- background: getCurrentPPTSlideBackground()
- }">
- <!-- 调试信息 -->
- <div
- 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;">
- {{ generatedPPT.length }}页, 当前第{{ currentPPTSlideIndex + 1 }}页
- </div>
- <!-- PPT预览提示 -->
- <div class="ppt-preview-tip">
- <p>💡 点击背景图片、素材图片可更换</p>
- </div>
- <!-- 显示PPT元素 -->
- <div v-for="(element, index) in getCurrentPPTSlideForMainView().elements" :key="element.id"
- class="preview-element" :class="{ selected: selectedPPTElementIndex === index }"
- :style="getElementStyle(element)" @click.stop="selectPPTElement(index)"
- @dblclick="handleDoubleClickDisabled(index)" @mousedown="startPPTDrag($event, index)">
- <!-- 缩放手柄 -->
- <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-nw"
- @mousedown.stop="startPPTResize($event, index, 'nw')"></div>
- <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-ne"
- @mousedown.stop="startPPTResize($event, index, 'ne')"></div>
- <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-sw"
- @mousedown.stop="startPPTResize($event, index, 'sw')"></div>
- <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-se"
- @mousedown.stop="startPPTResize($event, index, 'se')"></div>
- <!-- 拖拽手柄 -->
- <div v-if="selectedPPTElementIndex === index" class="drag-handle"
- @mousedown.stop="startPPTDrag($event, index)">
- {{ getElementTypeName(element.type) }}
- </div>
- <!-- 图片元素 -->
- <img v-if="element.type === 'image' && element.src" :src="element.src" :alt="element.id"
- :style="{ opacity: element.opacity || 1 }" @click="changePPTImage(index)" @mousedown.stop />
- <!-- 文本元素 -->
- <template v-else-if="element.type === 'text'">
- <!-- 内联编辑态 -->
- <div v-if="editingPPTElementIndex === index" class="inline-editor" contenteditable="true"
- v-html="editingPPTHtml" :style="{
- color: element.defaultColor,
- fontFamily: element.defaultFontName,
- opacity: element.opacity || 1
- }" @input="onPPTInlineInput" @blur="savePPTInlineEdit(index)" @mousedown.stop
- @keydown.stop></div>
- <!-- 普通显示态 -->
- <div v-else v-html="element.content" :style="{
- color: element.defaultColor,
- fontFamily: element.defaultFontName,
- opacity: element.opacity || 1
- }"></div>
- </template>
- <!-- 形状元素 -->
- <div v-else-if="element.type === 'shape'" class="shape" :style="getShapeStyle(element)"></div>
- </div>
- </div>
- <!-- 编辑模式:显示编辑界面 -->
- <div v-else-if="showDownloadOptions && generatedPPT.length === 0" class="edit-mode">
- <div class="slide-editor">
- <div class="slide-content" contenteditable="true" @input="updateSlideContent">
- <h2 class="slide-title">{{ currentEditingSlide.title || '点击编辑标题' }}</h2>
- <div class="slide-body">
- <p>{{ currentEditingSlide.content || '点击编辑内容' }}</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 缩略图导航 -->
- <div class="thumbnail-nav">
- <!-- 模板预览模式:显示模板缩略图 -->
- <div v-if="!showDownloadOptions">
- <div class="slide-counter">
- 第{{ currentSlideIndex + 1 }}页/共{{ slideImages.length }}页
- <div class="progress-dots">
- <div v-for="(slide, index) in slideImages" :key="index"
- :class="['progress-dot', { active: index === currentSlideIndex }]" @click="goToSlide(index)">
- </div>
- </div>
- </div>
- <div class="thumbnail-strip" @wheel="handleThumbnailWheel" ref="thumbnailStrip">
- <div v-for="(slide, index) in slideImages" :key="index"
- :class="['thumbnail-item', { active: index === currentSlideIndex }]" @click="goToSlide(index)">
- <img :src="slide" :alt="`第${index + 1}页`" class="thumbnail-image">
- <div class="thumbnail-number">{{ index + 1 }}</div>
- </div>
- </div>
- </div>
- <!-- PPT预览模式:显示PPT缩略图 -->
- <div v-if="showDownloadOptions && generatedPPT.length > 0">
- <div class="slide-counter">
- 第{{ currentPPTSlideIndex + 1 }}页/共{{ generatedPPT.length }}页
- <div class="progress-dots">
- <div v-for="(slide, index) in generatedPPT" :key="index"
- :class="['progress-dot', { active: index === currentPPTSlideIndex }]"
- @click="goToPPTSlide(index)">
- </div>
- </div>
- </div>
- <div class="thumbnail-strip" @wheel="handleThumbnailWheel" ref="thumbnailStrip">
- <div v-for="(slide, index) in generatedPPT" :key="index"
- :class="['thumbnail-item', { active: index === currentPPTSlideIndex }]"
- @click="goToPPTSlide(index)">
- <div class="thumbnail-preview" :style="{ background: getSlideBackground(slide) }">
- <div class="thumbnail-content">
- <div v-for="element in slide.elements" :key="element.id" class="thumbnail-element"
- :style="getThumbnailElementStyle(element)">
- <div v-if="element.type === 'text'" v-html="element.content"></div>
- <img v-else-if="element.type === 'image' && element.src" :src="element.src"
- :alt="element.id">
- </div>
- </div>
- </div>
- <div class="thumbnail-number">{{ index + 1 }}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧侧边栏 -->
- <div class="template-sidebar">
- <!-- 模板样式选择 -->
- <div v-if="!showDownloadOptions" class="template-content" :class="{ 'disabled': isApplyingTemplate }">
- <h4 class="sidebar-title">模板样式 ({{ templateStyles.length }})</h4>
- <div class="template-list">
- <div v-for="(template, index) in templateStyles" :key="index"
- :class="['template-item', { active: selectedTemplate === index }]"
- @click="selectTemplate(index); resetSlidePosition()">
- <div class="template-thumbnail">
- <img :src="template.thumbnail" :alt="template.title" class="template-img">
- <!-- <div v-if="template.type === 'dynamic'" class="dynamic-badge">动态</div> -->
- </div>
- <div class="template-info">
- <h5 class="template-title">{{ template.title }}</h5>
- <div class="template-meta">
- <span class="update-time">{{ template.updateTime }}</span>
- <span class="page-count">{{ template.pageCount }}页</span>
- </div>
- <div v-if="template.description" class="template-description">
- {{ template.description }}
- </div>
- </div>
- </div>
- </div>
- <!-- 动态模板预览信息 -->
- <!-- <div v-if="isDynamicTemplate && templatePreview" class="dynamic-preview">
- <h5 class="preview-title">📊 大纲分析</h5>
- <div class="preview-stats">
- <div class="stat-item">
- <span class="stat-label">章节数:</span>
- <span class="stat-value">{{ templatePreview.chapterCount }}</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">小节数:</span>
- <span class="stat-value">{{ templatePreview.totalSections }}</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">复杂度:</span>
- <span class="stat-value" :class="'complexity-' + templatePreview.complexity">
- {{ templatePreview.complexity === 'simple' ? '简单' :
- templatePreview.complexity === 'medium' ? '中等' : '复杂' }}
- </span>
- </div>
- <div class="stat-item">
- <span class="stat-label">预计页数:</span>
- <span class="stat-value">{{ templatePreview.estimatedSlides }}</span>
- </div>
- </div>
-
- <div v-if="templatePreview.recommendations && templatePreview.recommendations.length > 0" class="recommendations">
- <h6 class="recommendations-title">💡 优化建议</h6>
- <ul class="recommendations-list">
- <li v-for="(rec, index) in templatePreview.recommendations" :key="index">
- {{ rec }}
- </li>
- </ul>
- </div>
-
- <div v-if="templatePreview.validation && templatePreview.validation.warnings && templatePreview.validation.warnings.length > 0" class="warnings">
- <h6 class="warnings-title">⚠️ 注意事项</h6>
- <ul class="warnings-list">
- <li v-for="(warning, index) in templatePreview.validation.warnings" :key="index">
- {{ warning }}
- </li>
- </ul>
- </div>
- </div> -->
- <!-- 应用模板中遮罩层 -->
- <div v-if="isApplyingTemplate" class="applying-overlay">
- <div class="applying-content">
- <p>AI正在填充内容并应用模板,请稍候...</p>
- </div>
- </div>
- </div>
- <!-- 下载选项 -->
- <div v-if="showDownloadOptions" class="download-content"
- :class="{ 'disabled': isGeneratingTrainingMaterial || isGeneratingExam }">
- <h4 class="sidebar-title">下载选项</h4>
- <!-- 培训讲义生成中遮罩层 -->
- <div v-if="isGeneratingTrainingMaterial" class="applying-overlay">
- <div class="applying-content">
- <p>AI正在生成培训讲义,请稍候...</p>
- </div>
- </div>
- <!-- 考试工坊生成中遮罩层 -->
- <div v-if="isGeneratingExam" class="applying-overlay">
- <div class="applying-content">
- <p>AI正在生成考试题目,请稍候...</p>
- </div>
- </div>
- <div class="download-options">
- <div v-for="(option, index) in downloadOptions" :key="index"
- :class="['download-option', { active: selectedDownloadOption === index }]"
- @click="selectDownloadOption(index)">
- <div class="option-icon">
- <img :src="option.icon" :alt="option.title" class="option-img">
- </div>
- <div class="option-info">
- <h5 class="option-title">{{ option.title }}</h5>
- <p class="option-description">{{ option.description }}</p>
- </div>
- <div v-if="selectedDownloadOption === index" class="option-check">
- <img src="@/assets/Safety/29.png" alt="选中" class="check-icon">
- </div>
- </div>
- </div>
- <div class="download-actions">
- <button class="action-btn secondary" @click="goBackToTemplate">
- 重新挑选模板
- </button>
- <button class="action-btn primary" @click="exportPPTX" :disabled="isDownloading">
- <img src="@/assets/Safety/30.png" alt="下载" class="download-icon">
- {{ isDownloading ? '生成中...' : '立即下载' }}
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 操作按钮 -->
- <div class="preview-actions" v-if="!showDownloadOptions">
- <button class="action-btn secondary" @click="goToStep2">
- 返回编辑大纲
- </button>
- <!-- Mock数据选择器 -->
- <!-- <div class="mock-data-selector" style="margin-bottom: 16px;">
- <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">选择测试数据:</label>
- <select v-model="selectedMockData" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; background: white;">
- <option v-for="(option, index) in mockDataOptions" :key="option.id" :value="index">
- {{ option.title }} - {{ option.description }}
- </option>
- </select>
- </div> -->
- <!-- 使用用户大纲选项 -->
- <!-- <div class="user-outline-option" style="margin-bottom: 16px; padding: 16px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;">
- <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">使用我之前生成的大纲:</label>
- <div style="display: flex; align-items: center; gap: 8px;">
- <input
- type="checkbox"
- id="useUserOutline"
- v-model="useUserOutline"
- style="margin: 0;"
- />
- <label for="useUserOutline" style="margin: 0; font-weight: 500; color: #4b5563;">
- 使用当前大纲数据 ({{ outlineData.length > 0 ? `${outlineData.length}个章节` : '无大纲数据' }})
- </label>
- </div>
- <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;">
- ⚠️ 当前没有大纲数据,请先生成大纲后再使用此选项
- </div>
- <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;">
- ✅ 检测到 {{ outlineData.length }} 个章节的大纲数据,可以用于测试
- </div>
- </div> -->
- <!-- 测试动态模板按钮 -->
- <!-- <button class="action-btn test" @click="testDynamicTemplateFill" style="background: #10b981; margin-right: 8px;">
- 测试动态模板
- </button> -->
- <button class="action-btn primary" @click="applyTemplate" :disabled="isApplyingTemplate">
- <span v-if="!isApplyingTemplate">应用此模板</span>
- <span v-else>正在处理中...</span>
- </button>
- <!-- <button class="action-btn secondary" @click="testOutlineToAIPPT" style="margin-left: 10px;">
- 转换并应用模板5
- </button> -->
- </div>
- </div>
- </div>
- <!-- 推荐问题 -->
- <div v-if="currentStep === 'step1' && !showChat && !selectedFile" class="recommended-questions">
- <div v-for="(question, index) in hotQuestions" :key="question.id || index" class="question-tag"
- @click="handleRecommendedQuestion(question.question)">
- <img :src="getQuestionIcon(question.question)" alt="问题" class="question-icon">
- {{ question.question }}
- </div>
- <!-- 如果没有数据,显示默认问题 -->
- <div v-if="hotQuestions.length === 0" class="question-tag"
- @click="handleRecommendedQuestion('施工现场安全培训的主要内容有哪些?')">
- <img src="@/assets/Chat/12.png" alt="问题" class="question-icon">
- 施工现场安全培训的主要内容有哪些?
- </div>
- <div v-if="hotQuestions.length === 0" class="question-tag"
- @click="handleRecommendedQuestion('高空作业安全防护措施有哪些要求?')">
- <img src="@/assets/Chat/10.png" alt="问题" class="question-icon">
- 高空作业安全防护措施有哪些要求?
- </div>
- <div v-if="hotQuestions.length === 0" class="question-tag" @click="handleRecommendedQuestion('《建设工程安全生产管理条例》')">
- <img src="@/assets/Chat/11.png" alt="文档" class="question-icon">
- 《建设工程安全生产管理条例》
- </div>
- </div>
- <!-- 底部输入区域 -->
- <div v-if="currentStep === 'step1'" class="chat-input-section">
- <div class="input-container">
- <!-- 文件预览区域 -->
- <div v-if="selectedFile" class="file-preview-section">
- <div class="file-preview">
- <div class="file-icon">
- <img v-if="selectedFile.type === '.doc' || selectedFile.type === '.docx'" :src="selectedFile.icon" alt="文档图标" class="file-icon-img">
- <span v-else>{{ selectedFile.icon }}</span>
- </div>
- <div class="file-info">
- <div class="file-name">{{ selectedFile.name }}</div>
- <div class="file-size">{{ formatFileSize(selectedFile.size) }}</div>
- </div>
- <button class="remove-file-btn" @click="removeSelectedFile">
- <span class="remove-icon">×</span>
- </button>
- </div>
- </div>
- <div class="input-box">
- <button class="attach-btn" @click="triggerFileUpload" :disabled="isSending || hasTypingMessage">
- <div class="icon-container">
- <img src="@/assets/Chat/9.png" alt="附件" class="action-icon"
- style="width: 20px; height: 20px; max-width: 20px; max-height: 20px;">
- </div>
- </button>
- <input type="text" placeholder="请在此处发送消息 (Enter键可立即发送)" class="message-input" v-model="messageText"
- @keyup.enter="sendMessage" @input="handleInput" :disabled="isSending || hasTypingMessage"
- maxlength="2000">
- <button class="voice-btn" @click="handleVoiceClick" :disabled="isSending || hasTypingMessage"
- :class="{ 'recording': isListening }">
- <div class="icon-container">
- <img src="@/assets/Chat/18.png" alt="语音" class="action-icon"
- style="width: 20px; height: 20px; max-width: 20px; max-height: 20px;">
- <div v-if="isListening" class="recording-indicator"></div>
- </div>
- </button>
- <div class="divider"></div>
- <button class="send-btn" @click="sendMessage"
- :disabled="isSending || hasTypingMessage || !messageText.trim()">
- <img :src="messageText.trim() && !isSending && !hasTypingMessage ? sendIconFilled : sendIconEmpty"
- alt="发送" class="send-icon">
- </button>
- </div>
- </div>
- </div>
- <!-- 隐藏的文件输入框 -->
- <input ref="fileInput" type="file" accept=".docx" style="display: none" @change="handleFileSelect" />
- <!-- 隐藏的图片输入框 -->
- <input ref="imageInput" type="file" accept="image/*" style="display: none" @change="handleImageSelect" />
- <!-- 删除确认弹窗 -->
- <DeleteConfirmModal :visible="showDeleteModal" title="删除历史记录" :message="deleteConfirmMessage"
- @confirm="confirmDeleteHistory" @cancel="cancelDeleteHistory" @close="cancelDeleteHistory" />
- <!-- 自定义居中提示 -->
- <div v-if="showCopyToast" class="copy-toast-overlay"
- 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;">
- <div class="copy-toast"
- 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);">
- 已复制大纲
- </div>
- </div>
- <!-- WPS AI PPT集成弹窗 -->
- <div v-if="showWPSModal" class="wps-modal-overlay" @click="showWPSModal = false"
- 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;">
- <div class="wps-modal" @click.stop
- 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;">
- <div class="wps-modal-header"
- style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background: #f8f9fa; border-bottom: 1px solid #e9ecef;">
- <h3 style="margin: 0; font-size: 18px; font-weight: 600; color: #333;">
- <!-- {{ currentPPTInfo && selectedWPSUrl !== 'https://aippt.wps.cn/aippt/' ? `
- ${currentPPTInfo.file_name}` :
- '蜀安 AI PPT' }} -->
- </h3>
- <button class="wps-modal-close" @click="showWPSModal = false"
- 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>
- </div>
- <div class="wps-modal-content" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
- <!-- 下载监听插件控制 -->
- <!-- <div style="padding: 8px 24px; flex-shrink: 0; background: #f8f9fa; border-bottom: 1px solid #e9ecef;">
- <label style="font-size: 12px; display: flex; align-items: center; gap: 4px;">
- <input type="checkbox" v-model="isDownloadListenerActive" @change="toggleDownloadListener"
- style="margin: 0;" />
- 启用下载监听插件
- </label>
- </div> -->
- <div class="wps-iframe-container" style="flex: 1; min-height: 0; position: relative;">
- <iframe :src="selectedWPSUrl" frameborder="0" allowfullscreen class="wps-iframe" title="WPS AI PPT"
- @error="handleIframeError" style="width: 100%; height: calc(100% - 30px); border: none;"></iframe>
- <!-- 遮罩层:根据PPT创建状态显示不同遮罩 -->
- <!-- PPT创建前:显示"蜀安AI PPT"遮罩 -->
- <div v-if="!currentPPTInfo" class="wps-iframe-mask-top-right"
- 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">
- 蜀安AI PPT
- </div>
- <!-- PPT创建后:延迟3秒显示另一个遮罩 -->
- <!-- <div
- v-if="currentPPTInfo && showSecondMask"
- class="wps-iframe-mask-top-center"
- 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">
- </div> -->
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted, onUnmounted, reactive, nextTick, watch } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import Sidebar from '@/components/Sidebar.vue'
- import DeleteConfirmModal from '@/components/DeleteConfirmModal.vue'
- import { apis } from '@/request/apis.js'
- // ===== 已删除:getUserId - 不再需要,改用token =====
- // import { getUserId } from '@/utils/userManager.js'
- // 导入语音识别组件
- import { useSpeechRecognition } from '@/composables/useSpeechRecognition'
- // 导入发送按钮图标
- import sendIconEmpty from '@/assets/Chat/15.png'
- import sendIconFilled from '@/assets/Chat/16.png'
- // 导入其他图片资源
- import safetyTrainingIcon from '@/assets/Safety/4.png'
- import safetyAssessmentIcon from '@/assets/Safety/3.png'
- import safetyRegulationsIcon from '@/assets/Safety/2.png'
- import emergencyProceduresIcon from '@/assets/Safety/1.png'
- import defaultHistoryIcon from '@/assets/Safety/6.png'
- import copyIcon from '@/assets/AIWriting/5.png'
- import editIcon from '@/assets/AIWriting/6.png'
- import regenerateIcon from '@/assets/AIWriting/7.png'
- import deleteIcon from '@/assets/AIWriting/8.png'
- import voiceIcon from '@/assets/AIWriting/9.png'
- import likeIcon from '@/assets/AIWriting/10.png'
- import dislikeIcon from '@/assets/AIWriting/11.png'
- import questionIcon1 from '@/assets/Chat/12.png'
- import questionIcon2 from '@/assets/Chat/10.png'
- import questionIcon3 from '@/assets/Chat/11.png'
- // 导入template5图片
- import template5Slide1 from '@/assets/template5/template5-slide-1.png'
- import template5Slide2 from '@/assets/template5/template5-slide-2.png'
- import template5Slide3 from '@/assets/template5/template5-slide-3.png'
- import template5Slide4 from '@/assets/template5/template5-slide-4.png'
- import template5Slide5 from '@/assets/template5/template5-slide-5.png'
- // 导入template7图片
- import template7Slide1 from '@/assets/template7/template7-slide-1.png'
- import template7Slide2 from '@/assets/template7/template7-slide-2.png'
- import template7Slide3 from '@/assets/template7/template7-slide-3.png'
- import template7Slide4 from '@/assets/template7/template7-slide-4.png'
- import template7Slide5 from '@/assets/template7/template7-slide-5.png'
- // 导入template8图片
- import template8Slide1 from '@/assets/template8/template8-slide-1.png'
- import template8Slide2 from '@/assets/template8/template8-slide-2.png'
- import template8Slide3 from '@/assets/template8/template8-slide-3.png'
- import template8Slide4 from '@/assets/template8/template8-slide-4.png'
- import template8Slide5 from '@/assets/template8/template8-slide-5.png'
- // 导入JSON文件
- import template5JsonData from '@/assets/mocks/template_5.json'
- import template7JsonData from '@/assets/mocks/template_7.json'
- import template8JsonData from '@/assets/mocks/template_8.json'
- import aipptJsonData from '@/assets/mocks/AIPPT.json'
- // 导入动态模板生成器
- import {
- generateDynamicTemplate,
- selectOptimalTemplate,
- validateOutlineForDynamicTemplate,
- getAvailableTemplateStyles
- } from '@/utils/dynamicTemplateGenerator.js'
- import {
- matchOutlineAndGeneratePPT,
- previewOutlineMatch
- } from '@/utils/outlineMatchingAlgorithm.js'
- // 导入Element Plus组件
- import { ElMessage, ElMessageBox } from 'element-plus'
- // 路由
- const router = useRouter()
- const route = useRoute()
- // 响应式数据
- const messageText = ref('')
- const selectedHistoryItem = ref(null) // 选中的历史记录项
- const ai_conversation_id = ref(0) // 对话ID
- // 保存状态管理
- const lastSavedOutlineData = ref(null) // 上次保存的大纲数据
- const lastSavedPPTData = ref(null) // 上次保存的PPT数据
- const isSaving = ref(false) // 是否正在保存中
- const isSwitchingHistory = ref(false) // 是否正在切换历史记录
- // 删除相关状态
- const showDeleteModal = ref(false) // 控制是否显示删除确认弹窗
- const deleteTargetItem = ref(null) // 要删除的目标项
- const currentStep = ref('step1') // 当前步骤:step1-步骤一,step2-步骤二,step3-步骤三
- const showOutline = ref(false) // 控制是否显示大纲
- const evaluation = ref('') // 大纲评价状态
- const outlineFeedback = ref(null) // 大纲的用户反馈状态(从后端获取)
- const currentAiMessageId = ref(null) // 当前AI消息的ID
- const outlineId = ref(null) // 大纲的ID
- const isGeneratingOutline = ref(false) // 控制是否正在生成新大纲
- const isGeneratingExam = ref(false) // 控制是否正在生成考题
- const isApplyingTemplate = ref(false) // 控制是否正在应用模板
- const isLoadingHistory = ref(false) // 控制是否正在加载历史记录
- const isGeneratingTrainingMaterial = ref(false) // 控制是否正在生成培训讲义
- const isProcessing = ref(false) // 控制是否正在处理中(生成PPT或下载时禁用其他操作)
- // 功能卡片和热点问题数据
- const functionCards = ref([])
- const hotQuestions = ref([])
- // 聊天相关状态
- const showChat = ref(false) // 控制是否显示聊天界面
- const chatMessages = ref([]) // 聊天消息历史
- const isSending = ref(false) // 控制发送状态
- const showWPSModal = ref(false) // 控制是否显示WPS AI PPT集成弹窗
- const showCopyToast = ref(false) // 控制是否显示复制大纲提示
- const selectedWPSUrl = ref('https://aippt.wps.cn/aippt/') // 当前选择的WPS URL,默认进入WPS AI PPT
- // 下载监听插件相关
- const isDownloadListenerActive = ref(true) // 是否启用下载监听,默认选中
- const downloadHistory = ref([]) // 下载历史记录
- const lastDownloadTime = ref(null) // 最后一次下载时间
- // PPT打开功能相关
- const currentPPTInfo = ref(null) // 当前检测到的PPT信息
- const showOpenPPTButton = ref(false) // 是否显示打开PPT按钮
- const showSecondMask = ref(false) // 是否显示第二个遮罩(延迟3秒)
- // 文件上传相关
- const selectedFile = ref(null)
- const isUploadingFile = ref(false)
- const fileContent = ref('') // 存储文件内容
- // 文件处理配置
- const fileConfig = reactive({
- maxSize: 20 * 1024 * 1024, // 20MB
- allowedTypes: ['.docx'] // 只允许.docx格式的Word文档
- })
- // OSS上传配置
- const uploadData = reactive({
- OssAccessKeyId: '',
- policy: '',
- Signature: '',
- host: '',
- dir: '',
- key: ''
- })
- // 语音功能
- const {
- isSupported: speechSupported,
- isListening,
- isSpeaking: speechIsSpeaking,
- transcript,
- error: speechError,
- startListening,
- stopListening,
- speakText,
- stopSpeaking
- } = useSpeechRecognition()
- // 记录正在朗读的消息ID
- const speakingMessageId = ref(null)
- // 计算属性 - 是否有正在打字的AI消息
- const hasTypingMessage = computed(() => {
- return chatMessages.value.some(message => message.type === 'ai' && message.isTyping)
- })
- // 删除确认消息
- const deleteConfirmMessage = computed(() => {
- const title = deleteTargetItem.value?.item?.title || ''
- return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`
- })
- // PPT生成相关数据
- const outlineData = ref(null) // 大纲数据
- const outlineStats = ref({}) // 大纲统计信息
- const outlineTitle = ref('') // 大纲标题
- // 计算大纲统计信息
- const calculateOutlineStats = (chapters) => {
- if (!chapters || !Array.isArray(chapters)) {
- return {
- totalChapters: 0,
- totalSections: 0,
- estimatedPages: '0页',
- estimatedTime: '0分钟'
- }
- }
- const totalChapters = chapters.length
- let totalSections = 0
- // 计算总小节数
- chapters.forEach((chapter, index) => {
- if (chapter.sections && Array.isArray(chapter.sections)) {
- totalSections += chapter.sections.length
- }
- })
- // 根据章节和小节数预估PPT页数和讲解时长
- const estimatedPages = estimatePPTPages(chapters, totalSections)
- const estimatedTime = estimatePresentationTime(chapters, totalSections)
- return {
- totalChapters,
- totalSections,
- estimatedPages,
- estimatedTime
- }
- }
- // 预估PPT页数
- const estimatePPTPages = (chapters, sections) => {
- // 基础页面:封面(1) + 目录(1) + 结束页(1) = 3页
- let basePages = 3
- // 每个章节:过渡页(1) + 内容页(根据小节数计算)
- let contentPages = 0
- chapters.forEach(chapter => {
- if (chapter.sections && chapter.sections.length > 0) {
- // 每个章节至少1个过渡页
- contentPages += 1
- // 每个小节1-2页内容页
- chapter.sections.forEach(section => {
- if (section.subsections && section.subsections.length > 0) {
- // 根据子小节数量决定页数
- const subsectionCount = section.subsections.length
- if (subsectionCount <= 2) {
- contentPages += 1 // 1-2个子小节用1页
- } else if (subsectionCount <= 4) {
- contentPages += 2 // 3-4个子小节用2页
- } else {
- contentPages += 3 // 5个以上子小节用3页
- }
- } else {
- contentPages += 1 // 没有子小节,默认1页
- }
- })
- } else {
- // 章节没有小节,默认1页过渡页 + 1页内容页
- contentPages += 2
- }
- })
- const totalPages = basePages + contentPages
- return `${totalPages}页`
- }
- // 预估讲解时长
- const estimatePresentationTime = (chapters, sections) => {
- // 基础时间:开场(2分钟) + 结束(3分钟) = 5分钟
- let baseTime = 5
- // 每个章节的讲解时间
- let contentTime = 0
- chapters.forEach(chapter => {
- if (chapter.sections && chapter.sections.length > 0) {
- // 每个章节过渡页:1-2分钟
- contentTime += 2
- // 每个小节:3-5分钟
- chapter.sections.forEach(section => {
- if (section.subsections && section.subsections.length > 0) {
- const subsectionCount = section.subsections.length
- if (subsectionCount <= 2) {
- contentTime += 3 // 1-2个子小节用3分钟
- } else if (subsectionCount <= 4) {
- contentTime += 5 // 3-4个子小节用5分钟
- } else {
- contentTime += 7 // 5个以上子小节用7分钟
- }
- } else {
- contentTime += 4 // 没有子小节,默认4分钟
- }
- })
- } else {
- // 章节没有小节,默认2分钟过渡 + 4分钟内容
- contentTime += 6
- }
- })
- const totalTime = baseTime + contentTime
- return `${totalTime}分钟`
- }
- // 更新大纲统计信息
- const updateOutlineStats = () => {
- if (outlineData.value) {
- outlineStats.value = calculateOutlineStats(outlineData.value)
- }
- }
- // 编辑相关状态
- const editingItem = ref(null) // 当前正在编辑的项目
- const editingType = ref('') // 编辑类型:'title', 'chapter', 'section', 'subsection'
- const editingIndex = ref(null) // 编辑项目的索引
- const editingContent = ref('') // 编辑内容
- // 拖拽相关状态
- const draggedChapterIndex = ref(null) // 正在拖拽的章节索引
- const dragOverChapterIndex = ref(null) // 拖拽悬停的章节索引
- const showEditOptions = ref(null) // 显示编辑选项的项目ID
- // 步骤三相关数据
- const currentSlideIndex = ref(0) // 当前幻灯片索引
- const selectedTemplate = ref(0) // 选中的模板索引
- // PPT编辑相关数据
- const currentEditingSlide = ref({ title: '', content: '' }) // 当前编辑的幻灯片内容
- const pptSlides = ref([]) // PPT幻灯片数据
- // 步骤四相关数据
- const selectedDownloadOption = ref(0) // 选中的下载选项索引
- const showDownloadOptions = ref(false) // 控制是否显示下载选项
- // 预览模式相关数据
- const previewMode = ref('edit') // 预览模式:'edit' - 编辑模式, 'preview' - 预览模式
- // PPT预览相关数据
- const isPPTPreviewMode = ref(false) // 是否处于PPT预览模式
- const generatedPPT = ref([]) // 生成的PPT数据
- const currentPPTSlideIndex = ref(0) // 当前PPT幻灯片索引
- const selectedPPTElementIndex = ref(-1) // 选中的PPT元素索引
- const editingPPTElementIndex = ref(-1) // 正在编辑的PPT元素索引
- const editingPPTHtml = ref('') // 正在编辑的PPT HTML内容
- const zoom = ref(1) // 缩放比例
- const selectedImageIndex = ref(null) // 当前选中的图片索引
- // 幻灯片图片数组
- const slideImages = ref([
- template5Slide1, // 第1页
- template5Slide2, // 第2页
- template5Slide3, // 第3页
- template5Slide4, // 第4页
- template5Slide5 // 第5页
- ])
- // 模板样式数据
- const templateStyles = ref([
- // {
- // thumbnail: template5Slide1,
- // title: '通用类PPT',
- // updateTime: '2025-09-01 14:23 更新',
- // pageCount: 5,
- // type: 'static'
- // },
- {
- thumbnail: template5Slide1,
- title: '通用类PPT',
- updateTime: '2025-01-15 10:00 更新',
- pageCount: '5',
- type: 'dynamic', // 恢复dynamic类型
- // description: '支持2-6章节,2-4小节的灵活组合',
- style: 'default'
- },
- {
- thumbnail: template7Slide1, // 使用相同的缩略图,实际会显示红色主题
- title: '红色主题PPT',
- updateTime: '2025-01-15 10:00 更新',
- pageCount: '5',
- type: 'static', // 改回static类型,使用我们设计的template_7.json
- // description: '红色主题的PPT模板',
- style: 'red', // 恢复红色主题样式
- templateData: template7JsonData // 使用我们精心设计的红色主题结构
- },
- // {
- // thumbnail: template8Slide1, // 使用模板8的缩略图
- // title: '蓝色科技主题PPT',
- // updateTime: '2025-01-15 10:00 更新',
- // pageCount: '5',
- // type: 'static', // 使用我们设计的template_8.json
- // description: '蓝色科技风格的PPT模板',
- // style: 'blueTech', // 蓝色科技主题样式
- // templateData: template8JsonData // 使用我们精心设计的蓝色科技主题结构
- // }
- ])
- // 动态模板相关状态
- const isDynamicTemplate = ref(false)
- const templatePreview = ref(null)
- const templateAnalysis = ref(null)
- // 填充动态模板内容(使用AI填充详细内容)
- const fillDynamicTemplateContentWithAI = async (template, outlineData, title) => {
- try {
- console.log('开始填充动态模板内容(AI增强)...')
- console.log('模板数据:', template)
- console.log('大纲数据:', outlineData)
- console.log('标题:', title)
- // 第一步:填充基本内容(标题等)
- const basicFilledTemplate = fillDynamicTemplateBasicContent(template, outlineData, title)
- // 第二步:使用AI填充详细内容
- console.log('开始使用AI填充详细内容...')
- const aiFilledTemplate = await fillDynamicTemplateWithAI(basicFilledTemplate, outlineData, title)
- console.log('动态模板内容填充完成(AI增强):', aiFilledTemplate)
- return aiFilledTemplate
- } catch (error) {
- console.error('填充动态模板内容失败(AI增强):', error)
- throw error
- }
- }
- // 填充动态模板基本内容
- const fillDynamicTemplateBasicContent = (template, outlineData, title) => {
- try {
- console.log('开始填充动态模板基本内容...')
- console.log('模板数据:', template)
- console.log('大纲数据:', outlineData)
- console.log('标题:', title)
- const filledTemplate = JSON.parse(JSON.stringify(template)) // 深拷贝
- // 填充封面页
- if (filledTemplate[0] && filledTemplate[0].type === 'cover') {
- filledTemplate[0].elements.forEach(element => {
- if (element.textType === 'title') {
- 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>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: ${getResponsiveFontSize(24)}px; color: ${element.defaultColor};">基于AI生成的培训大纲,包含相关内容</span></p>`
- }
- })
- }
- // 填充目录页
- if (filledTemplate[1] && filledTemplate[1].type === 'contents') {
- const chapterTitles = outlineData.map(chapter => chapter.title)
- let itemIndex = 0
- filledTemplate[1].elements.forEach(element => {
- if (element.textType === 'item' && itemIndex < chapterTitles.length) {
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${itemIndex + 1}. ${chapterTitles[itemIndex]}</span></p>`
- itemIndex++
- }
- })
- }
- // 填充章节内容页
- let slideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- outlineData.forEach((chapter, chapterIndex) => {
- // 章节过渡页
- if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'transition') {
- filledTemplate[slideIndex].elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${chapter.title}</span></strong></p>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${chapter.content || `本章将介绍${chapter.title}的相关内容`}</span></p>`
- }
- })
- slideIndex++
- }
- // 小节内容页
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'content') {
- // 填充小节标题
- filledTemplate[slideIndex].elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${section.title}</span></strong></p>`
- }
- })
- // 填充小节内容(只填充标题,详细内容留给AI)
- let contentIndex = 0
- const subsections = section.subsections || []
- filledTemplate[slideIndex].elements.forEach(element => {
- if (element.textType === 'itemTitle' && contentIndex < subsections.length) {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${subsections[contentIndex].title}</span></strong></p>`
- } else if (element.textType === 'itemContent' && contentIndex < subsections.length) {
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">待AI填充</span></p>`
- contentIndex++
- }
- })
- // 确保所有itemContent都有内容,即使没有对应的subsection
- filledTemplate[slideIndex].elements.forEach(element => {
- if (element.textType === 'itemContent' && element.content.includes('待AI填充')) {
- // 保持"待AI填充"标记,让AI来处理
- }
- })
- slideIndex++
- }
- })
- } else {
- // 如果章节没有小节,填充默认内容
- if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'content') {
- filledTemplate[slideIndex].elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${chapter.title}</span></strong></p>`
- } else if (element.textType === 'itemTitle') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">概述</span></strong></p>`
- } else if (element.textType === 'itemContent') {
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">待AI填充</span></p>`
- }
- })
- slideIndex++
- }
- }
- })
- // 填充结束页(最后一张)
- const lastSlide = filledTemplate[filledTemplate.length - 1]
- if (lastSlide && lastSlide.type === 'end') {
- lastSlide.elements.forEach(element => {
- if (element.textType === 'title') {
- 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>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">感谢您的时间与关注</span></p>`
- }
- })
- }
- console.log('动态模板基本内容填充完成:', filledTemplate)
- return filledTemplate
- } catch (error) {
- console.error('填充动态模板基本内容失败:', error)
- throw error
- }
- }
- // 使用AI填充动态模板详细内容(借用6目录模板的填充方式)
- const fillDynamicTemplateWithAI = async (template, outlineData, title) => {
- try {
- console.log('开始使用AI填充动态模板详细内容...')
- console.log('接收到的template类型:', typeof template, '是否为数组:', Array.isArray(template))
- console.log('template内容:', template)
- // 验证template参数
- if (!Array.isArray(template)) {
- throw new Error(`模板数据格式错误,期望数组但得到: ${typeof template}`)
- }
- // 统计需要填充的内容数量
- let pendingFillCount = 0
- template.forEach(slide => {
- if (slide.elements) {
- slide.elements.forEach(element => {
- if (element.textType === 'itemContent' && element.content && element.content.includes('待AI填充')) {
- pendingFillCount++
- }
- })
- }
- })
- console.log(`需要AI填充的内容项数量: ${pendingFillCount}`)
- // 借用6目录模板的填充方式:直接填充itemContent内容
- const filledTemplate = JSON.parse(JSON.stringify(template)) // 深拷贝
- // 为每个需要填充的itemContent生成内容
- let contentIndex = 0
- for (let slideIndex = 0; slideIndex < filledTemplate.length; slideIndex++) {
- const slide = filledTemplate[slideIndex]
- if (slide.elements) {
- for (let elementIndex = 0; elementIndex < slide.elements.length; elementIndex++) {
- const element = slide.elements[elementIndex]
- if (element.textType === 'itemContent' && element.content && element.content.includes('待AI填充')) {
- // 显示进度提示
- console.log(`📝 正在生成第${contentIndex + 1}/${pendingFillCount}项内容...`)
- try {
- // 获取对应的标题用于AI生成
- const titleForAI = getTitleForAIContent(outlineData, slideIndex, contentIndex)
- console.log(`🤖 正在为第${contentIndex + 1}项内容调用AI,标题: ${titleForAI}`)
- // 调用AI API生成内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.要有独特性和创新性,避免与其他内容重复 6.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
- })
- console.log(`🤖 AI响应:`, aiResponse)
- if (aiResponse && aiResponse.data) {
- // 提取AI回复的内容,参考其他地方的解析方式
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
- // 添加动态填充效果
- await animateTextFill(element, aiContent, element.defaultColor)
- console.log(`✅ AI生成内容成功: ${aiContent.substring(0, 50)}...`)
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, contentIndex)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- console.log(`⚠️ AI调用失败,使用备用内容: ${fallbackContent}`)
- }
- } catch (aiError) {
- console.error(`❌ AI调用失败 (第${contentIndex + 1}项):`, aiError)
- // AI调用失败,使用备用内容
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, contentIndex)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- }
- contentIndex++
- }
- }
- }
- }
- console.log(`动态模板内容填充完成,共填充 ${contentIndex} 项内容`)
- return filledTemplate
- } catch (error) {
- console.error('AI填充动态模板详细内容失败:', error)
- throw new Error('AI填充动态模板详细内容失败: ' + error.message)
- }
- }
- // 逐页生成PPT效果(类似AIPPT)
- const generatePPTWithAnimation = async (template, outlineData, title) => {
- console.log('🎬 开始逐页生成PPT效果...')
- // 初始化空的PPT
- generatedPPT.value = []
- // 逐页生成效果
- for (let slideIndex = 0; slideIndex < template.length; slideIndex++) {
- const slide = template[slideIndex]
- console.log(`📄 正在生成第${slideIndex + 1}页: ${slide.type || '未知类型'}`)
- // 1. 先显示空白页面(缩略图条显示实际内容,大图显示"正在生成...")
- generatedPPT.value.push({
- ...slide,
- elements: slide.elements ? slide.elements.map(el => ({
- ...el,
- content: el.content, // 缩略图条显示实际内容
- // 确保位置信息不被修改
- left: el.left,
- top: el.top,
- width: el.width,
- height: el.height
- })) : []
- })
- // 强制更新预览
- generatedPPT.value = [...generatedPPT.value]
- // 调试信息:检查当前页面的背景
- console.log(`第${slideIndex + 1}页背景信息:`, slide.background)
- console.log(`第${slideIndex + 1}页元素数量:`, slide.elements?.length)
- // 强制Vue重新渲染缩略图条
- await nextTick()
- // 强制更新generatedPPT以触发缩略图重新渲染
- generatedPPT.value = [...generatedPPT.value]
- // 2. 等待一下让用户看到页面出现
- await new Promise(resolve => setTimeout(resolve, 300))
- // 3. 异步填充页面内容(逐元素渲染)
- const filledSlide = await fillSlideContentWithAnimation(slide, outlineData, title, slideIndex)
- // 4. 更新当前页面
- generatedPPT.value[slideIndex] = filledSlide
- generatedPPT.value = [...generatedPPT.value]
- // 强制Vue重新渲染缩略图条
- await nextTick()
- // 强制更新generatedPPT以触发缩略图重新渲染
- generatedPPT.value = [...generatedPPT.value]
- // 5. 确保大图显示当前页面
- currentPPTSlideIndex.value = slideIndex
- // 6. 等待一下让用户看到内容填充
- await new Promise(resolve => setTimeout(resolve, 400))
- console.log(`✅ 第${slideIndex + 1}页生成完成`)
- }
- console.log('🎉 所有页面生成完成!')
- // 生成完成后回到第一页
- currentPPTSlideIndex.value = 0
- console.log('📄 已回到第一页')
- }
- // 填充单个页面内容(带动画效果)
- const fillSlideContentWithAnimation = async (slide, outlineData, title, slideIndex) => {
- const filledSlide = JSON.parse(JSON.stringify(slide))
- if (filledSlide.elements) {
- console.log(`🔍 第${slideIndex + 1}页有${filledSlide.elements.length}个元素`)
- // 逐个处理元素,实现逐元素渲染效果
- for (let elementIndex = 0; elementIndex < filledSlide.elements.length; elementIndex++) {
- const element = filledSlide.elements[elementIndex]
- console.log(`🔍 处理元素${elementIndex + 1}:`, {
- textType: element.textType,
- content: element.content,
- id: element.id,
- type: element.type
- })
- // 处理封面页标题
- if (slideIndex === 0 && element.textType === 'title' && element.content && element.content.includes('标题占位符')) {
- 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>`
- console.log(`📝 填充封面标题: ${title}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理封面页副标题
- else if (slideIndex === 0 && element.textType === 'content' && element.content && element.content.includes('副标题占位符')) {
- // 使用完整的描述信息作为副标题
- const subtitle = await getFullDescriptionForCover(outlineData, title)
- element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: #f0f0f0;">${subtitle}</span></p>`
- console.log(`📝 填充封面副标题: ${subtitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理目录页内容
- else if (slideIndex === 1 && element.textType === 'item' && element.content && element.content.includes('目录项')) {
- // 从元素ID中提取目录项索引
- const itemMatch = element.id.match(/item-(\d+)/)
- if (itemMatch) {
- const itemIndex = parseInt(itemMatch[1]) - 1 // 转换为0基索引
- if (itemIndex < outlineData.length) {
- const chapterTitle = outlineData[itemIndex].title
- element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${chapterTitle}</span></p>`
- console.log(`📝 填充目录项${itemIndex + 1}: ${chapterTitle}`)
- } else {
- element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};"> </span></p>`
- }
- }
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理过渡页标题
- else if (element.textType === 'title' && element.content && element.content.includes('章节标题')) {
- const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 36px; color: ${element.defaultColor};">${chapterTitle}</span></strong></p>`
- console.log(`📝 填充过渡页标题: ${chapterTitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理过渡页内容(使用AI生成)
- else if (element.textType === 'content' && element.content && element.content.includes('章节介绍')) {
- // 获取当前章节标题用于AI生成
- const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
- try {
- console.log(`🤖 正在为过渡页生成章节介绍内容: ${chapterTitle}`)
- // 调用AI API生成章节介绍内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT章节"${chapterTitle}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
- })
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 过渡页章节介绍生成完成: ${aiContent}`)
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${aiContent}</span></p>`
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${fallbackContent}</span></p>`
- }
- } catch (aiError) {
- console.error(`❌ AI生成过渡页内容失败:`, aiError)
- // AI生成失败,使用备用内容
- const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${fallbackContent}</span></p>`
- }
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理内容页标题
- else if (element.textType === 'title' && element.content && element.content.includes('内容页标题')) {
- const contentTitle = getContentPageTitle(outlineData, slideIndex)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${contentTitle}</span></strong></p>`
- console.log(`📝 填充内容页标题: ${contentTitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理需要填充的元素
- else if (element.textType === 'itemContent' && element.content && (element.content.includes('正在生成...') || element.content.includes('待AI填充'))) {
- console.log(`🤖 找到需要AI填充的元素:`, {
- textType: element.textType,
- content: element.content,
- id: element.id
- })
- // 获取对应的标题用于AI生成
- const titleForAI = getTitleForAIContent(outlineData, slideIndex, elementIndex)
- try {
- console.log(`🤖 正在为第${slideIndex + 1}页生成内容: ${titleForAI}`)
- console.log(`🤖 元素信息:`, element)
- // 调用AI API生成内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
- })
- console.log(`🤖 AI响应:`, aiResponse)
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
- console.log(`🤖 AI生成内容:`, aiContent)
- // 逐字符显示效果
- await animateTextFill(element, aiContent, element.defaultColor)
- console.log(`✅ 第${slideIndex + 1}页内容生成完成`)
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
- console.log(`🔄 使用备用内容:`, fallbackContent)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- } catch (aiError) {
- console.error(`❌ AI生成内容失败:`, aiError)
- // AI生成失败,使用备用内容
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
- console.log(`🔄 使用备用内容:`, fallbackContent)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- }
- // 处理标题元素
- else if (element.textType === 'itemTitle' && element.content && (element.content.includes('要点标题') || element.content.includes('待AI生成子小节'))) {
- // 获取对应的标题
- const titleForElement = getTitleForElement(outlineData, slideIndex, elementIndex)
- // 检查是否需要AI生成标题 - 统一使用通用PPT的方式
- if (titleForElement.includes('待AI生成子小节')) {
- // 需要AI生成子小节标题
- try {
- console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
- // 获取小节标题用于AI生成
- const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, elementIndex)
- // 调用AI生成子小节标题
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片的小节"${sectionTitle}"生成3-4个简洁的子标题。要求:1.每个标题都要不同 2.标题简洁明了,控制在6-12字 3.专业准确 4.适合PPT展示 5.直接返回标题,用换行分隔,不要编号,不要重复,不要解释,只要标题`
- })
- if (aiResponse && aiResponse.data) {
- const aiTitles = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- const titles = aiTitles.split('\n').filter(title => title.trim()).map(title => title.trim())
- if (titles.length > 0) {
- // 根据元素索引选择不同的标题
- const titleIndex = elementIndex % titles.length
- const finalTitle = titles[titleIndex]
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${finalTitle}</span></strong></p>`
- console.log(`✅ AI生成子小节标题[${titleIndex}]: ${finalTitle}`)
- } else {
- // AI生成失败,使用默认标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`⚠️ AI生成标题失败,使用默认标题`)
- }
- } else {
- // AI调用失败,使用默认标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`⚠️ AI调用失败,使用默认标题`)
- }
- } catch (aiError) {
- console.error(`❌ AI生成标题失败:`, aiError)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- }
- } else {
- // 使用现有标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`📝 填充标题: ${titleForElement}`)
- }
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- // 处理其他需要填充的元素(但不要覆盖已经AI生成的内容)
- else if (element.content && element.content.includes('待AI填充') && !element.content.includes('正在生成...')) {
- // 获取对应的内容
- const contentForElement = getContentForElement(outlineData, slideIndex, elementIndex)
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${contentForElement}</span></p>`
- console.log(`📝 填充内容: ${contentForElement}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 等待一下让用户看到元素出现
- await new Promise(resolve => setTimeout(resolve, 200))
- }
- }
- }
- return filledSlide
- }
- // 填充单个页面内容
- const fillSlideContent = async (slide, outlineData, title, slideIndex) => {
- const filledSlide = JSON.parse(JSON.stringify(slide))
- if (filledSlide.elements) {
- console.log(`🔍 第${slideIndex + 1}页有${filledSlide.elements.length}个元素`)
- // 逐个处理元素,实现逐元素渲染效果
- for (let elementIndex = 0; elementIndex < filledSlide.elements.length; elementIndex++) {
- const element = filledSlide.elements[elementIndex]
- console.log(`🔍 处理元素${elementIndex + 1}:`, {
- textType: element.textType,
- content: element.content,
- id: element.id,
- type: element.type
- })
- // 处理封面页标题
- if (slideIndex === 0 && element.textType === 'title' && element.content && element.content.includes('标题占位符')) {
- 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>`
- console.log(`📝 填充封面标题: ${title}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理封面页副标题
- else if (slideIndex === 0 && element.textType === 'content' && element.content && element.content.includes('副标题占位符')) {
- // 使用完整的描述信息作为副标题
- const subtitle = await getFullDescriptionForCover(outlineData, title)
- element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: #f0f0f0;">${subtitle}</span></p>`
- console.log(`📝 填充封面副标题: ${subtitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理目录页内容
- else if (slideIndex === 1 && element.textType === 'item' && element.content && element.content.includes('目录项')) {
- // 从元素ID中提取目录项索引
- const itemMatch = element.id.match(/item-(\d+)/)
- if (itemMatch) {
- const itemIndex = parseInt(itemMatch[1]) - 1 // 转换为0-based索引
- if (itemIndex < outlineData.length) {
- const chapter = outlineData[itemIndex]
- const tocItem = `${itemIndex + 1}. ${chapter.title}`
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;">${tocItem}</span></p>`
- console.log(`📝 填充目录项${itemIndex + 1}: ${tocItem}`)
- } else {
- // 如果章节数量少于目录项数量,隐藏多余的目录项
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;"></span></p>`
- console.log(`📝 隐藏多余的目录项${itemIndex + 1}`)
- }
- } else {
- // 如果无法解析ID,使用默认内容
- element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;">目录项</span></p>`
- console.log(`📝 使用默认目录项内容`)
- }
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理过渡页标题
- else if (slide.type === 'transition' && element.textType === 'title' && element.content && element.content.includes('章节标题')) {
- // 获取当前章节标题
- const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #2d3748;">${chapterTitle}</span></strong></p>`
- console.log(`📝 填充过渡页标题: ${chapterTitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理过渡页章节介绍内容
- else if (slide.type === 'transition' && element.textType === 'content' && element.content && element.content.includes('章节介绍内容')) {
- // 获取当前章节标题用于AI生成
- const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
- try {
- console.log(`🤖 正在为过渡页生成章节介绍内容: ${chapterTitle}`)
- // 调用AI API生成章节介绍内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT章节"${chapterTitle}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
- })
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 过渡页章节介绍生成完成: ${aiContent}`)
- // 添加动态填充效果
- await animateTextFill(element, aiContent, element.defaultColor)
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- } catch (aiError) {
- console.error(`❌ AI生成过渡页内容失败:`, aiError)
- // AI生成失败,使用备用内容
- const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- }
- // 处理内容页标题
- else if (element.textType === 'title' && element.content && element.content.includes('内容页标题')) {
- // 获取当前内容页标题
- const contentTitle = getContentPageTitle(outlineData, slideIndex)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #2d3748;">${contentTitle}</span></strong></p>`
- console.log(`📝 填充内容页标题: ${contentTitle}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理需要填充的元素
- else if (element.textType === 'itemContent' && element.content && (element.content.includes('正在生成...') || element.content.includes('待AI填充'))) {
- console.log(`🤖 找到需要AI填充的元素:`, {
- textType: element.textType,
- content: element.content,
- id: element.id
- })
- // 获取对应的标题用于AI生成
- const titleForAI = getTitleForAIContent(outlineData, slideIndex, elementIndex)
- try {
- console.log(`🤖 正在为第${slideIndex + 1}页生成内容: ${titleForAI}`)
- console.log(`🤖 元素信息:`, element)
- // 调用AI API生成内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
- })
- console.log(`🤖 AI响应:`, aiResponse)
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
- console.log(`🤖 AI生成内容:`, aiContent)
- // 逐字符显示效果
- await animateTextFill(element, aiContent, element.defaultColor)
- console.log(`✅ 第${slideIndex + 1}页内容生成完成`)
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
- console.log(`⚠️ AI调用失败,使用备用内容:`, fallbackContent)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- } catch (aiError) {
- console.error(`❌ AI调用失败:`, aiError)
- const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
- console.log(`🔄 使用备用内容:`, fallbackContent)
- await animateTextFill(element, fallbackContent, element.defaultColor)
- }
- }
- // 处理标题元素
- else if (element.textType === 'itemTitle' && element.content && (element.content.includes('要点标题') || element.content.includes('待AI生成子小节'))) {
- // 获取对应的标题
- const titleForElement = getTitleForElement(outlineData, slideIndex, elementIndex)
- // 检查是否需要AI生成标题 - 统一使用通用PPT的方式
- if (titleForElement.includes('待AI生成子小节')) {
- // 需要AI生成子小节标题
- try {
- console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
- // 获取小节标题用于AI生成
- const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, elementIndex)
- // 调用AI生成子小节标题
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片的小节"${sectionTitle}"生成3-4个简洁的子标题。要求:1.每个标题都要不同 2.标题简洁明了,控制在6-12字 3.专业准确 4.适合PPT展示 5.直接返回标题,用换行分隔,不要编号,不要重复,不要解释,只要标题`
- })
- if (aiResponse && aiResponse.data) {
- const aiTitles = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- const titles = aiTitles.split('\n').filter(title => title.trim()).map(title => title.trim())
- if (titles.length > 0) {
- // 根据元素索引选择不同的标题
- const titleIndex = elementIndex % titles.length
- const finalTitle = titles[titleIndex]
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${finalTitle}</span></strong></p>`
- console.log(`✅ AI生成子小节标题[${titleIndex}]: ${finalTitle}`)
- } else {
- // AI生成失败,使用默认标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`⚠️ AI生成标题失败,使用默认标题`)
- }
- } else {
- // AI调用失败,使用默认标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`⚠️ AI调用失败,使用默认标题`)
- }
- } catch (aiError) {
- console.error(`❌ AI生成标题失败:`, aiError)
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- }
- } else {
- // 使用现有标题
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
- console.log(`📝 填充标题: ${titleForElement}`)
- }
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 处理其他需要填充的元素(但不要覆盖已经AI生成的内容)
- else if (element.content && element.content.includes('待AI填充') && !element.content.includes('正在生成...')) {
- // 获取对应的内容
- const contentForElement = getContentForElement(outlineData, slideIndex, elementIndex)
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${contentForElement}</span></p>`
- console.log(`📝 填充内容: ${contentForElement}`)
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- }
- // 每个元素处理完成后稍作延迟,让用户看到逐元素渲染效果
- await new Promise(resolve => setTimeout(resolve, 100))
- }
- }
- return filledSlide
- }
- // 动态文本填充效果
- const animateTextFill = async (element, content, color) => {
- return new Promise((resolve) => {
- // 先显示加载状态
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${color};">正在生成内容...</span></p>`
- // 强制更新PPT预览 - 使用nextTick确保DOM更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- // 延迟一下让用户看到加载状态
- setTimeout(() => {
- // 逐步显示内容
- let currentText = ''
- const words = content.split('')
- let index = 0
- const typeWriter = () => {
- if (index < words.length) {
- currentText += words[index]
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${color};">${currentText}</span></p>`
- // 强制更新PPT预览,触发响应式更新
- nextTick(() => {
- generatedPPT.value = [...generatedPPT.value]
- })
- index++
- setTimeout(typeWriter, 30) // 每30ms显示一个字符,加快速度
- } else {
- resolve()
- }
- }
- typeWriter()
- }, 500) // 500ms后开始打字效果
- })
- }
- // 获取元素标题
- const getTitleForElement = (outlineData, slideIndex, elementIndex) => {
- console.log(`获取元素标题 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
- console.log(`大纲数据:`, outlineData)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认标题'
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前幻灯片对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- currentSlideIndex++ // Account for chapter transition slide
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++ // Account for section content slide
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- // If a chapter has no sections, it still gets one content slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的章节和小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- if (section && section.subsections && section.subsections.length > 0) {
- // 根据子小节生成标题
- const subsection = section.subsections[elementIndex % section.subsections.length]
- if (subsection && subsection.title) {
- // 如果是"待AI生成子小节"标记,直接返回
- if (subsection.title.includes('待AI生成子小节')) {
- return subsection.title
- }
- return subsection.title
- }
- }
- // 如果没有子小节,根据小节标题生成标题
- if (section && section.title) {
- return section.title
- }
- // 最后根据章节标题生成标题
- if (chapter && chapter.title) {
- return chapter.title
- }
- // 默认标题
- return '默认标题'
- }
- // 获取元素内容
- const getContentForElement = (outlineData, slideIndex, elementIndex) => {
- console.log(`获取元素内容 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认内容'
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前幻灯片对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- currentSlideIndex++ // Account for chapter transition slide
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++ // Account for section content slide
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- // If a chapter has no sections, it still gets one content slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的章节和小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- if (section && section.subsections && section.subsections.length > 0) {
- // 根据子小节生成内容
- const subsection = section.subsections[elementIndex % section.subsections.length]
- if (subsection && subsection.title) {
- return generateProfessionalContent(subsection.title, elementIndex)
- }
- }
- // 如果没有子小节,根据小节标题生成内容
- if (section && section.title) {
- return generateProfessionalContent(section.title, elementIndex)
- }
- // 最后根据章节标题生成内容
- if (chapter && chapter.title) {
- return generateProfessionalContent(chapter.title, elementIndex)
- }
- // 默认内容
- return generateProfessionalContent('默认内容', elementIndex)
- }
- // 获取内容页标题
- const getContentPageTitle = (outlineData, slideIndex) => {
- console.log(`获取内容页标题 - 幻灯片索引: ${slideIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认内容页标题'
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前内容页对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- currentSlideIndex++ // Account for chapter transition slide
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++ // Account for section content slide
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- // If a chapter has no sections, it still gets one content slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的章节和小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- // 优先使用小节标题
- if (section && section.title) {
- return section.title
- }
- // 如果没有小节,使用章节标题
- if (chapter && chapter.title) {
- return chapter.title
- }
- // 默认标题
- return '默认内容页标题'
- }
- // 转换用户大纲数据为兼容格式
- const convertUserOutlineToCompatibleFormat = async (userOutline) => {
- console.log('转换用户大纲数据为兼容格式...')
- console.log('原始用户大纲数据:', userOutline)
- if (!userOutline || !Array.isArray(userOutline)) {
- console.log('用户大纲数据无效,返回空数组')
- return []
- }
- const convertedOutline = []
- for (let chapterIndex = 0; chapterIndex < userOutline.length; chapterIndex++) {
- const chapter = userOutline[chapterIndex]
- // 生成章节内容
- let chapterContent = chapter.content
- if (!chapterContent) {
- try {
- console.log(`🤖 正在为章节"${chapter.title}"生成内容...`)
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT章节"${chapter.title}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
- })
- if (aiResponse && aiResponse.data) {
- chapterContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 章节内容生成完成: ${chapterContent}`)
- } else {
- chapterContent = `第${chapterIndex + 1}章节的内容`
- }
- } catch (error) {
- console.error(`❌ 章节内容生成失败:`, error)
- chapterContent = `第${chapterIndex + 1}章节的内容`
- }
- }
- const convertedChapter = {
- title: chapter.title || `章节${chapterIndex + 1}`,
- content: chapterContent,
- sections: []
- }
- if (chapter.sections && Array.isArray(chapter.sections)) {
- for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
- const section = chapter.sections[sectionIndex]
- // 生成小节内容
- let sectionContent = section.content
- if (!sectionContent) {
- try {
- console.log(`🤖 正在为小节"${section.title}"生成内容...`)
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT小节"${section.title}"生成一个简洁的小节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
- })
- if (aiResponse && aiResponse.data) {
- sectionContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 小节内容生成完成: ${sectionContent}`)
- } else {
- sectionContent = `第${sectionIndex + 1}小节的内容`
- }
- } catch (error) {
- console.error(`❌ 小节内容生成失败:`, error)
- sectionContent = `第${sectionIndex + 1}小节的内容`
- }
- }
- const convertedSection = {
- title: section.title || `小节${sectionIndex + 1}`,
- content: sectionContent,
- subsections: []
- }
- if (section.subsections && Array.isArray(section.subsections) && section.subsections.length > 0) {
- // 如果用户大纲有子小节,使用用户的子小节
- convertedSection.subsections = section.subsections.map((subsection, subsectionIndex) => {
- return {
- title: subsection.title || `子小节${subsectionIndex + 1}`
- // 注意:不添加content字段,与mock数据结构保持一致
- }
- })
- } else {
- // 如果用户大纲没有子小节,让AI生成子小节标题
- // 随机生成2-4个子小节
- const subsectionCount = Math.floor(Math.random() * 3) + 2 // 2-4个子小节
- convertedSection.subsections = []
- // 创建占位符,稍后由AI填充
- for (let i = 0; i < subsectionCount; i++) {
- convertedSection.subsections.push({
- title: `待AI生成子小节${i + 1}` // 占位符,稍后由AI填充
- })
- }
- console.log(`为小节"${section.title}"创建了${subsectionCount}个待AI生成的子小节`)
- }
- convertedChapter.sections.push(convertedSection)
- }
- } else {
- // 如果没有小节,创建默认的小节(与mock数据结构一致)
- convertedChapter.sections = [
- {
- title: '主要内容',
- content: '主要内容描述',
- subsections: [
- { title: `${convertedChapter.title} - 要点1` },
- { title: `${convertedChapter.title} - 要点2` },
- { title: `${convertedChapter.title} - 要点3` },
- { title: `${convertedChapter.title} - 要点4` }
- ]
- }
- ]
- console.log(`为章节"${convertedChapter.title}"创建了默认小节和子小节`)
- }
- convertedOutline.push(convertedChapter)
- }
- console.log('转换后的兼容格式数据:', convertedOutline)
- return convertedOutline
- }
- // 获取封面页的完整描述信息
- const getFullDescriptionForCover = async (outlineData, title) => {
- console.log(`获取封面页完整描述 - 标题: ${title}`)
- // 如果是用户大纲数据(检查是否包含用户大纲的特征)
- if (title !== '用户生成的大纲' && outlineData && outlineData.length > 0) {
- if (outlineData && outlineData.length > 0) {
- try {
- // 使用AI生成专业的描述
- console.log(`🤖 正在为用户大纲生成专业描述...`)
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT演示文稿生成一个专业的副标题描述。大纲包含${outlineData.length}个章节,每个章节都有多个小节和子小节。要求:1.描述专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在30-45字以内 5.突出培训的专业性和系统性`
- })
- if (aiResponse && aiResponse.data) {
- const aiDescription = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 用户大纲描述生成完成: ${aiDescription}`)
- return aiDescription
- }
- } catch (error) {
- console.error(`❌ 用户大纲描述生成失败:`, error)
- }
- // AI生成失败,使用默认描述
- const chapterCount = outlineData.length
- let description = `${chapterCount}章节`
- // 添加每个章节的小节信息
- outlineData.forEach((chapter, index) => {
- if (chapter.sections && chapter.sections.length > 0) {
- const sectionCount = chapter.sections.length
- description += `,第${index + 1}章节${sectionCount}小节`
- // 添加子小节信息
- const subsectionCounts = chapter.sections.map(section =>
- section.subsections ? section.subsections.length : 0
- )
- if (subsectionCounts.length > 0 && subsectionCounts.some(count => count > 0)) {
- description += `(${subsectionCounts.join('+')}子小节)`
- }
- }
- })
- console.log(`生成用户大纲描述: ${description}`)
- return description
- }
- return '用户生成的大纲结构'
- }
- // 根据标题找到对应的mock数据选项
- const selectedOption = mockDataOptions.value.find(option => option.title === title)
- if (selectedOption && selectedOption.description) {
- console.log(`找到对应的描述: ${selectedOption.description}`)
- return selectedOption.description
- }
- // 如果没有找到对应的选项,根据大纲数据生成描述
- if (outlineData && outlineData.length > 0) {
- const chapterCount = outlineData.length
- let description = `${chapterCount}章节`
- // 添加每个章节的小节信息
- outlineData.forEach((chapter, index) => {
- if (chapter.sections && chapter.sections.length > 0) {
- const sectionCount = chapter.sections.length
- description += `,第${index + 1}章节${sectionCount}小节`
- // 添加子小节信息
- const subsectionCounts = chapter.sections.map(section =>
- section.subsections ? section.subsections.length : 0
- )
- if (subsectionCounts.length > 0 && subsectionCounts.some(count => count > 0)) {
- description += `(${subsectionCounts.join('+')}子小节)`
- }
- }
- })
- console.log(`生成的描述: ${description}`)
- return description
- }
- // 默认描述
- return '安全培训演示文稿'
- }
- // 获取过渡页的章节介绍内容
- const getChapterContentForTransition = (outlineData, slideIndex) => {
- console.log(`获取过渡页章节介绍内容 - 幻灯片索引: ${slideIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认章节介绍'
- }
- // 计算当前过渡页对应的章节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- // 找到当前过渡页对应的章节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- break
- }
- currentSlideIndex++ // Account for chapter transition slide
- // Then account for all section content slides in this chapter
- if (chapter.sections && chapter.sections.length > 0) {
- currentSlideIndex += chapter.sections.length
- } else {
- // If a chapter has no sections, it still gets one content slide
- currentSlideIndex++
- }
- }
- console.log(`当前过渡页对应 - 章节: ${chapterIndex}`)
- // 获取对应的章节数据
- const chapter = outlineData[chapterIndex]
- // 返回章节的介绍内容
- if (chapter && chapter.content) {
- return chapter.content
- }
- // 如果没有content字段,生成一个基于章节标题的介绍
- if (chapter && chapter.title) {
- return `本章将介绍${chapter.title}的相关内容,包括核心概念、重要知识点和实践应用。`
- }
- // 默认内容
- return '默认章节介绍'
- }
- // 获取过渡页的章节标题
- const getChapterTitleForTransition = (outlineData, slideIndex) => {
- console.log(`获取过渡页章节标题 - 幻灯片索引: ${slideIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认章节'
- }
- // 计算当前过渡页对应的章节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- // 找到当前过渡页对应的章节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- break
- }
- currentSlideIndex++ // Account for chapter transition slide
- // 为每个小节生成内容页
- if (chapter.sections && chapter.sections.length > 0) {
- currentSlideIndex += chapter.sections.length // Account for section content slides
- } else {
- currentSlideIndex++ // Account for default content slide
- }
- }
- console.log(`当前过渡页对应 - 章节: ${chapterIndex}`)
- // 获取对应的章节数据
- const chapter = outlineData[chapterIndex]
- if (chapter && chapter.title) {
- // 使用干净的标题(去掉第X章前缀)
- return getCleanChapterTitle(chapter.title)
- }
- // 默认标题
- return '默认章节'
- }
- // 获取小节标题用于AI生成子小节标题
- const getSectionTitleForAI = (outlineData, slideIndex, elementIndex) => {
- console.log(`获取小节标题用于AI生成 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认小节'
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前内容页对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- currentSlideIndex++ // Account for chapter transition slide
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++ // Account for section content slide
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- // If a chapter has no sections, it still gets one content slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- // 返回小节标题
- if (section && section.title) {
- return section.title
- }
- // 默认标题
- return '默认小节'
- }
- // 获取用于AI内容生成的标题
- const getTitleForAIContent = (outlineData, slideIndex, contentIndex) => {
- console.log(`获取AI标题 - 幻灯片索引: ${slideIndex}, 内容索引: ${contentIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return '默认内容'
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前幻灯片对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- // Each chapter has a transition slide
- currentSlideIndex++ // Account for chapter transition slide
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++ // Account for section content slide
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- // If a chapter has no sections, it still gets one content slide
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的章节和小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- if (section && section.subsections && section.subsections.length > 0) {
- // 根据子小节生成标题
- const subsection = section.subsections[contentIndex % section.subsections.length]
- if (subsection && subsection.title) {
- return `${chapter.title} - ${section.title} - ${subsection.title}`
- }
- }
- // 如果没有子小节,根据小节标题生成标题
- if (section && section.title) {
- return `${chapter.title} - ${section.title}`
- }
- // 最后根据章节标题生成标题
- if (chapter && chapter.title) {
- return chapter.title
- }
- // 默认标题
- return '默认内容'
- }
- // 生成专业内容(使用mock数据测试)
- const generateProfessionalContent = (title, index) => {
- // Mock数据:根据大纲结构生成对应的内容
- const mockContentMap = {
- '信息上报与指挥体系建立': [
- '建立完善的信息收集机制,确保各类安全信息及时准确上报',
- '构建统一的指挥调度平台,实现各部门协调联动',
- '制定标准化的上报流程,提高信息处理效率',
- '建立应急响应机制,确保突发事件快速处置'
- ],
- '安全风险评估与控制': [
- '开展全面的安全风险识别,建立风险清单',
- '制定风险等级评估标准,实施分级管控',
- '建立风险监测预警系统,实现动态监控',
- '完善风险控制措施,确保风险可控'
- ],
- '应急预案与处置': [
- '制定完善的应急预案体系,覆盖各类突发事件',
- '建立应急响应队伍,提高应急处置能力',
- '开展应急演练,检验预案有效性',
- '完善应急物资储备,确保应急保障'
- ],
- '培训教育与能力提升': [
- '制定系统化的培训计划,提高全员安全意识',
- '开展专业技能培训,提升操作能力',
- '建立培训考核机制,确保培训效果',
- '持续改进培训方式,提高培训质量'
- ],
- '监督检查与持续改进': [
- '建立监督检查机制,确保制度有效执行',
- '开展定期检查评估,发现问题及时整改',
- '建立问题跟踪机制,确保整改到位',
- '持续改进工作方法,提升管理水平'
- ]
- }
- // 根据标题匹配内容
- for (const [key, contents] of Object.entries(mockContentMap)) {
- if (title.includes(key) || key.includes(title)) {
- return contents[index % contents.length]
- }
- }
- // 默认内容模板
- const defaultTemplates = [
- '建立完善的管理体系,确保各项工作有序开展',
- '制定详细的操作规范,提高工作效率和质量',
- '加强人员培训,提升专业技能和综合素质',
- '建立监督检查机制,确保制度有效执行',
- '完善应急预案,提高应对突发事件的能力',
- '加强沟通协调,促进部门间有效合作',
- '持续改进优化,不断提升管理水平',
- '注重细节管理,确保工作质量稳定可靠',
- '强化责任意识,确保各项工作落实到位',
- '创新工作方法,提高工作效率和效果'
- ]
- return defaultTemplates[index % defaultTemplates.length]
- }
- // 根据大纲数据生成对应的内容
- const generateContentFromOutline = (outlineData, slideIndex, contentIndex) => {
- console.log(`生成内容 - 幻灯片索引: ${slideIndex}, 内容索引: ${contentIndex}`)
- // 跳过封面页和目录页(前2页)
- if (slideIndex < 2) {
- return generateProfessionalContent('默认内容', contentIndex)
- }
- // 计算当前内容页对应的章节和小节
- let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
- let chapterIndex = 0
- let sectionIndex = 0
- // 找到当前幻灯片对应的章节和小节
- for (let i = 0; i < outlineData.length; i++) {
- const chapter = outlineData[i]
- if (chapter.sections && chapter.sections.length > 0) {
- for (let j = 0; j < chapter.sections.length; j++) {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = j
- break
- }
- currentSlideIndex++
- }
- if (currentSlideIndex === slideIndex) break
- } else {
- if (currentSlideIndex === slideIndex) {
- chapterIndex = i
- sectionIndex = 0
- break
- }
- currentSlideIndex++
- }
- }
- console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
- // 获取对应的章节和小节数据
- const chapter = outlineData[chapterIndex]
- const section = chapter && chapter.sections && chapter.sections[sectionIndex]
- if (section && section.subsections && section.subsections.length > 0) {
- // 根据子小节生成内容
- const subsection = section.subsections[contentIndex % section.subsections.length]
- if (subsection && subsection.title) {
- return generateProfessionalContent(subsection.title, contentIndex)
- }
- }
- // 如果没有子小节,根据小节标题生成内容
- if (section && section.title) {
- return generateProfessionalContent(section.title, contentIndex)
- }
- // 最后根据章节标题生成内容
- if (chapter && chapter.title) {
- return generateProfessionalContent(chapter.title, contentIndex)
- }
- // 默认内容
- return generateProfessionalContent('默认内容', contentIndex)
- }
- // Mock数据配置选项
- const mockDataOptions = ref([
- {
- id: 'safety-basic',
- title: '基础安全培训',
- description: '2章节,每章节2小节,每小节4子小节',
- data: [
- {
- title: '信息上报与指挥体系建立',
- content: '建立完善的信息上报和指挥体系',
- sections: [
- {
- title: '信息收集机制',
- content: '建立完善的信息收集机制',
- subsections: [
- { title: '信息收集渠道' },
- { title: '信息收集标准' },
- { title: '信息收集流程' },
- { title: '信息质量控制' }
- ]
- },
- {
- title: '指挥调度平台',
- content: '构建统一的指挥调度平台',
- subsections: [
- { title: '平台功能设计' },
- { title: '系统架构规划' },
- { title: '数据集成方案' },
- { title: '用户权限管理' }
- ]
- }
- ]
- },
- {
- title: '安全风险评估与控制',
- content: '开展全面的安全风险评估与控制',
- sections: [
- {
- title: '风险识别与评估',
- content: '建立风险识别与评估体系',
- subsections: [
- { title: '风险识别方法' },
- { title: '风险评估标准' },
- { title: '风险等级划分' },
- { title: '风险评估流程' }
- ]
- },
- {
- title: '风险控制措施',
- content: '制定有效的风险控制措施',
- subsections: [
- { title: '预防控制措施' },
- { title: '监测预警机制' },
- { title: '应急处置方案' },
- { title: '持续改进机制' }
- ]
- }
- ]
- }
- ]
- },
- {
- id: 'comprehensive-training',
- title: '综合培训体系',
- description: '3章节,每章节3小节,每小节3子小节',
- data: [
- {
- title: '培训体系建设',
- content: '建立完善的培训体系',
- sections: [
- {
- title: '培训需求分析',
- content: '分析培训需求',
- subsections: [
- { title: '需求调研方法' },
- { title: '需求分析工具' },
- { title: '需求确认流程' }
- ]
- },
- {
- title: '培训计划制定',
- content: '制定培训计划',
- subsections: [
- { title: '计划制定原则' },
- { title: '计划执行方案' },
- { title: '计划调整机制' }
- ]
- },
- {
- title: '培训效果评估',
- content: '评估培训效果',
- subsections: [
- { title: '评估指标体系' },
- { title: '评估方法选择' },
- { title: '评估结果应用' }
- ]
- }
- ]
- },
- {
- title: '师资队伍建设',
- content: '建设专业师资队伍',
- sections: [
- {
- title: '师资选拔标准',
- content: '制定师资选拔标准',
- subsections: [
- { title: '专业能力要求' },
- { title: '教学经验要求' },
- { title: '综合素质要求' }
- ]
- },
- {
- title: '师资培训体系',
- content: '建立师资培训体系',
- subsections: [
- { title: '培训内容设计' },
- { title: '培训方式选择' },
- { title: '培训效果跟踪' }
- ]
- },
- {
- title: '师资激励机制',
- content: '建立师资激励机制',
- subsections: [
- { title: '激励政策制定' },
- { title: '激励措施实施' },
- { title: '激励效果评估' }
- ]
- }
- ]
- },
- {
- title: '培训资源管理',
- content: '管理培训资源',
- sections: [
- {
- title: '培训设施建设',
- content: '建设培训设施',
- subsections: [
- { title: '设施规划布局' },
- { title: '设施设备配置' },
- { title: '设施维护管理' }
- ]
- },
- {
- title: '培训教材开发',
- content: '开发培训教材',
- subsections: [
- { title: '教材编写标准' },
- { title: '教材审核流程' },
- { title: '教材更新机制' }
- ]
- },
- {
- title: '培训技术支持',
- content: '提供技术支持',
- subsections: [
- { title: '技术平台建设' },
- { title: '技术维护服务' },
- { title: '技术培训支持' }
- ]
- }
- ]
- }
- ]
- },
- {
- id: 'emergency-management',
- title: '应急管理体系',
- description: '4章节,每章节2小节,每小节2子小节',
- data: [
- {
- title: '应急预案制定',
- content: '制定应急预案',
- sections: [
- {
- title: '预案编制流程',
- content: '编制应急预案',
- subsections: [
- { title: '预案编制标准' },
- { title: '预案审核程序' }
- ]
- },
- {
- title: '预案演练实施',
- content: '实施预案演练',
- subsections: [
- { title: '演练计划制定' },
- { title: '演练效果评估' }
- ]
- }
- ]
- },
- {
- title: '应急响应机制',
- content: '建立应急响应机制',
- sections: [
- {
- title: '响应流程设计',
- content: '设计响应流程',
- subsections: [
- { title: '响应级别划分' },
- { title: '响应时间要求' }
- ]
- },
- {
- title: '响应队伍建设',
- content: '建设响应队伍',
- subsections: [
- { title: '队伍组建标准' },
- { title: '队伍培训体系' }
- ]
- }
- ]
- },
- {
- title: '应急物资保障',
- content: '保障应急物资',
- sections: [
- {
- title: '物资储备管理',
- content: '管理物资储备',
- subsections: [
- { title: '储备标准制定' },
- { title: '储备检查制度' }
- ]
- },
- {
- title: '物资调配机制',
- content: '建立调配机制',
- subsections: [
- { title: '调配流程设计' },
- { title: '调配效率优化' }
- ]
- }
- ]
- },
- {
- title: '应急信息管理',
- content: '管理应急信息',
- sections: [
- {
- title: '信息收集系统',
- content: '建设收集系统',
- subsections: [
- { title: '系统功能设计' },
- { title: '系统运行维护' }
- ]
- },
- {
- title: '信息发布机制',
- content: '建立发布机制',
- subsections: [
- { title: '发布渠道建设' },
- { title: '发布效果监控' }
- ]
- }
- ]
- }
- ]
- },
- {
- id: 'quality-management',
- title: '质量管理体系',
- description: '5章节,每章节2小节,每小节3子小节',
- data: [
- {
- title: '质量方针制定',
- content: '制定质量方针',
- sections: [
- {
- title: '方针内容设计',
- content: '设计方针内容',
- subsections: [
- { title: '方针目标设定' },
- { title: '方针实施策略' },
- { title: '方针效果评估' }
- ]
- },
- {
- title: '方针宣传推广',
- content: '推广质量方针',
- subsections: [
- { title: '宣传渠道建设' },
- { title: '推广活动组织' },
- { title: '推广效果跟踪' }
- ]
- }
- ]
- },
- {
- title: '质量目标管理',
- content: '管理质量目标',
- sections: [
- {
- title: '目标设定方法',
- content: '设定质量目标',
- subsections: [
- { title: '目标分解原则' },
- { title: '目标量化标准' },
- { title: '目标调整机制' }
- ]
- },
- {
- title: '目标监控体系',
- content: '监控目标实现',
- subsections: [
- { title: '监控指标设计' },
- { title: '监控频率设定' },
- { title: '监控结果分析' }
- ]
- }
- ]
- },
- {
- title: '质量过程控制',
- content: '控制质量过程',
- sections: [
- {
- title: '过程识别分析',
- content: '识别分析过程',
- subsections: [
- { title: '过程流程图绘制' },
- { title: '过程关键点识别' },
- { title: '过程风险分析' }
- ]
- },
- {
- title: '过程改进优化',
- content: '改进优化过程',
- subsections: [
- { title: '改进机会识别' },
- { title: '改进方案设计' },
- { title: '改进效果验证' }
- ]
- }
- ]
- },
- {
- title: '质量审核评估',
- content: '审核评估质量',
- sections: [
- {
- title: '审核计划制定',
- content: '制定审核计划',
- subsections: [
- { title: '审核范围确定' },
- { title: '审核标准制定' },
- { title: '审核人员安排' }
- ]
- },
- {
- title: '审核实施管理',
- content: '管理审核实施',
- subsections: [
- { title: '审核流程执行' },
- { title: '审核记录管理' },
- { title: '审核结果处理' }
- ]
- }
- ]
- },
- {
- title: '质量持续改进',
- content: '持续改进质量',
- sections: [
- {
- title: '改进机会识别',
- content: '识别改进机会',
- subsections: [
- { title: '问题分析方法' },
- { title: '改进需求评估' },
- { title: '改进优先级排序' }
- ]
- },
- {
- title: '改进措施实施',
- content: '实施改进措施',
- subsections: [
- { title: '改进方案制定' },
- { title: '改进资源保障' },
- { title: '改进效果跟踪' }
- ]
- }
- ]
- }
- ]
- },
- {
- id: 'innovation-system',
- title: '创新管理体系',
- description: '6章节,每章节1小节,每小节4子小节',
- data: [
- {
- title: '创新战略规划',
- content: '规划创新战略',
- sections: [
- {
- title: '战略分析制定',
- content: '制定创新战略',
- subsections: [
- { title: '内外部环境分析' },
- { title: '创新机会识别' },
- { title: '战略目标设定' },
- { title: '战略实施路径' }
- ]
- }
- ]
- },
- {
- title: '创新文化建设',
- content: '建设创新文化',
- sections: [
- {
- title: '文化理念塑造',
- content: '塑造创新文化',
- subsections: [
- { title: '创新价值观建立' },
- { title: '创新氛围营造' },
- { title: '创新激励机制' },
- { title: '创新成果分享' }
- ]
- }
- ]
- },
- {
- title: '创新团队建设',
- content: '建设创新团队',
- sections: [
- {
- title: '团队组建管理',
- content: '管理创新团队',
- subsections: [
- { title: '团队成员选拔' },
- { title: '团队能力建设' },
- { title: '团队协作机制' },
- { title: '团队绩效管理' }
- ]
- }
- ]
- },
- {
- title: '创新项目管理',
- content: '管理创新项目',
- sections: [
- {
- title: '项目全生命周期',
- content: '管理项目全周期',
- subsections: [
- { title: '项目立项评估' },
- { title: '项目执行监控' },
- { title: '项目风险控制' },
- { title: '项目成果转化' }
- ]
- }
- ]
- },
- {
- title: '创新资源保障',
- content: '保障创新资源',
- sections: [
- {
- title: '资源统筹配置',
- content: '配置创新资源',
- subsections: [
- { title: '资金投入保障' },
- { title: '技术平台建设' },
- { title: '人才资源开发' },
- { title: '信息资源整合' }
- ]
- }
- ]
- },
- {
- title: '创新成果转化',
- content: '转化创新成果',
- sections: [
- {
- title: '成果产业化',
- content: '实现成果产业化',
- subsections: [
- { title: '成果评估筛选' },
- { title: '产业化路径设计' },
- { title: '市场推广策略' },
- { title: '经济效益评估' }
- ]
- }
- ]
- }
- ]
- },
- {
- id: 'mixed-structure',
- title: '混合结构演示',
- description: '2章节,第一章节2小节(3+4子小节),第二章节3小节(2+3+4子小节)',
- data: [
- {
- title: '第一章节:基础管理',
- content: '建立基础管理体系',
- sections: [
- {
- title: '制度建设',
- content: '建立完善的管理制度',
- subsections: [
- { title: '制度框架设计' },
- { title: '制度内容制定' },
- { title: '制度执行监督' }
- ]
- },
- {
- title: '流程优化',
- content: '优化工作流程',
- subsections: [
- { title: '流程梳理分析' },
- { title: '流程改进设计' },
- { title: '流程实施推广' },
- { title: '流程效果评估' }
- ]
- }
- ]
- },
- {
- title: '第二章节:运营管理',
- content: '提升运营管理水平',
- sections: [
- {
- title: '资源配置',
- content: '优化资源配置',
- subsections: [
- { title: '资源需求分析' },
- { title: '资源配置方案' }
- ]
- },
- {
- title: '绩效管理',
- content: '建立绩效管理体系',
- subsections: [
- { title: '绩效指标设定' },
- { title: '绩效评估方法' },
- { title: '绩效改进措施' }
- ]
- },
- {
- title: '风险控制',
- content: '加强风险控制',
- subsections: [
- { title: '风险识别评估' },
- { title: '风险控制措施' },
- { title: '风险监测预警' },
- { title: '风险应急处置' }
- ]
- }
- ]
- }
- ]
- }
- ])
- // 当前选中的mock数据
- const selectedMockData = ref(0)
- // 是否使用用户大纲数据
- const useUserOutline = ref(false)
- // 模板风格选择
- const selectedTemplateStyle = ref('default')
- const availableTemplateStyles = ref(getAvailableTemplateStyles())
- // 测试动态模板填充功能
- const testDynamicTemplateFill = async () => {
- try {
- console.log('开始测试动态模板填充功能...')
- let testOutline, testTitle, testDescription
- // 根据用户选择决定使用哪个数据源
- if (useUserOutline.value && outlineData.value.length > 0) {
- // 使用用户大纲数据,并转换为兼容格式
- testOutline = await convertUserOutlineToCompatibleFormat(outlineData.value)
- testTitle = outlineTitle.value || '用户生成的大纲' // 使用大纲标题作为PPT标题
- testDescription = `${testOutline.length}章节结构演示`
- console.log('使用用户大纲数据:', testTitle, `共${testOutline.length}个章节`)
- console.log('转换后的用户大纲数据结构:', JSON.stringify(testOutline, null, 2))
- } else {
- // 使用选中的mock数据
- const selectedOption = mockDataOptions.value[selectedMockData.value]
- testOutline = selectedOption.data
- testTitle = selectedOption.title
- testDescription = selectedOption.description
- console.log('使用mock数据:', selectedOption.title, selectedOption.description)
- }
- // 生成动态模板
- const result = await matchOutlineAndGeneratePPT(testOutline, testTitle, selectedTemplateStyle.value)
- console.log('生成的动态模板结果:', result)
- if (!result.success) {
- throw new Error(result.error || '动态模板生成失败')
- }
- // 获取模板数据
- const dynamicTemplate = result.data.template
- console.log('提取的模板数据:', dynamicTemplate)
- // 验证模板数据格式
- if (!Array.isArray(dynamicTemplate)) {
- console.error('模板数据不是数组格式:', typeof dynamicTemplate, dynamicTemplate)
- throw new Error('模板数据格式不正确,期望数组但得到: ' + typeof dynamicTemplate)
- }
- // 启用预览模式
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果
- await generatePPTWithAnimation(dynamicTemplate, testOutline, testTitle)
- const successMessage = useUserOutline.value ?
- `动态模板测试完成!使用用户大纲数据 (${testOutline.length}个章节)` :
- `动态模板测试完成!使用数据: ${testTitle} (${testDescription})`
- ElMessage.success(successMessage)
- } catch (error) {
- console.error('测试动态模板填充失败:', error)
- ElMessage.error('测试失败: ' + error.message)
- }
- }
- // 导入下载选项图标
- import pptxIcon from '@/assets/Safety/28.png'
- import examIcon from '@/assets/Safety/27.png'
- import documentIcon from '@/assets/Safety/26.png'
- import wordDocIcon from '@/assets/Chat/26.png'
- // 下载选项数据
- const downloadOptions = ref([
- {
- icon: pptxIcon,
- title: 'PowerPoint (PPTX)',
- description: '可编辑的演示文稿'
- },
- {
- icon: examIcon,
- title: '考试工坊',
- description: '基于该文档,生成考试题'
- },
- {
- icon: documentIcon,
- title: '培训讲义文档',
- description: '基于PPT内容,提取文档文字'
- }
- ])
- // 计算属性
- const currentSlideImage = computed(() => slideImages.value[currentSlideIndex.value])
- const currentTime = computed(() => {
- const now = new Date()
- return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
- })
- // 历史记录数据
- const historyData = ref([])
- const historyTotal = ref(0) // 历史记录总数
- // 获取历史记录列表
- const getHistoryRecordList = async () => {
- try {
- console.log('📋 开始获取安全培训历史记录列表...')
- const startTime = performance.now()
- const response = await apis.getHistoryRecord({
- // ===== 已删除:user_id - 后端从token解析 =====
- ai_conversation_id: 0, // 0表示获取对话列表
- business_type: 1 // 安全培训类型
- })
- const endTime = performance.now()
- console.log(`📋 历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
- console.log('📋 安全培训历史记录列表响应:', response)
- if (response.statusCode === 200) {
- // 设置历史记录总数
- historyTotal.value = response.total || 0
- // 转换后端数据为前端格式
- historyData.value = response.data.map(conversation => ({
- id: conversation.id,
- title: generateConversationTitle(conversation.content),
- time: formatTime(conversation.updated_at),
- businessType: conversation.business_type,
- step: conversation.step || 0, // 添加步骤字段
- cover_image: conversation.cover_image || '', // 添加封面图字段
- ppt_json_url: conversation.ppt_json_url || '', // 添加PPT JSON URL字段
- ppt_json_content: conversation.ppt_json_content || '', // 添加PPT JSON内容字段
- isActive: false,
- // 保存原始数据用于后续查询
- rawData: conversation
- }))
- console.log(`✅ 安全培训历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
- } else {
- console.error('❌ 获取安全培训历史记录列表失败:', response.statusCode)
- }
- } catch (error) {
- console.error('❌ 获取安全培训历史记录列表失败:', error)
- }
- }
- // 获取指定对话的详细消息
- const getConversationMessages = async (conversationId) => {
- try {
- console.log('开始获取安全培训对话消息,conversationId:', conversationId)
- const response = await apis.getHistoryRecord({
- // ===== 已删除:user_id - 后端从token解析 =====
- ai_conversation_id: conversationId,
- business_type: 1 // 安全培训类型
- })
- console.log('安全培训对话消息响应:', response)
- if (response.statusCode === 200) {
- // 转换后端消息数据为前端格式
- const messages = response.data.map(message => {
- const userFeedback = convertUserFeedback(message.user_feedback)
- console.log(`安全培训消息 ${message.id} 的反馈状态:`, {
- raw: message.user_feedback,
- converted: userFeedback
- })
- // 如果是用户消息且包含文件标签,提取文件信息
- let file = null
- let userContent = message.content
- if (message.type === 'user' && message.content.includes('</filesize>')) {
- // 提取文件信息
- const filenameMatch = message.content.match(/<filename>(.*?)<\/filename>/)
- const filesizeMatch = message.content.match(/<filesize>(.*?)<\/filesize>/)
- const wordMatch = message.content.match(/<word>(.*?)<\/word>/s)
- if (filenameMatch && filesizeMatch) {
- const filename = filenameMatch[1]
- const filesize = parseInt(filesizeMatch[1])
- const fileContent = wordMatch ? wordMatch[1].trim() : ''
- // 创建文件对象
- file = {
- name: filename,
- size: filesize,
- type: filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '.docx',
- icon: getFileIcon(filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '.docx'),
- content: fileContent
- }
- // 提取用户实际说的话(</filesize>标签后的内容)
- const userMessageMatch = message.content.split('</filesize>')[1]
- userContent = userMessageMatch ? userMessageMatch.trim() : ''
- }
- }
- return {
- type: message.type, // 'user' 或 'ai'
- content: userContent, // 使用提取的用户消息
- displayContent: message.type === 'ai' ? markdownToHtml(message.content) : userContent,
- file: file, // 添加文件对象
- isTyping: false,
- id: message.id,
- userFeedback: userFeedback,
- // 保存原始数据
- rawData: message
- }
- })
- // 设置聊天消息
- chatMessages.value = messages
- console.log('安全培训对话消息已设置:', chatMessages.value)
- // 更新对话ID
- ai_conversation_id.value = conversationId
- // 直接使用AI回复内容解析大纲(不再使用ppt_outline字段)
- const aiMessage = messages.find(msg => msg.type === 'ai')
- if (aiMessage && aiMessage.content) {
- console.log('找到AI回复内容,直接解析大纲:', aiMessage.content)
- // 检查是否正在切换历史记录,如果是则强制更新数据
- const isCurrentlySwitching = isSwitchingHistory.value
- if (!outlineData.value || outlineData.value.length === 0 || isCurrentlySwitching) {
- // 解析AI回复中的大纲
- const parsedOutline = parseOutlineFromAI(aiMessage.content)
- if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
- outlineData.value = parsedOutline.chapters
- outlineTitle.value = parsedOutline.title || '安全培训大纲'
- outlineStats.value = calculateOutlineStats(parsedOutline.chapters)
- console.log('从AI回复解析大纲数据成功', isCurrentlySwitching ? '(强制更新)' : '')
- } else {
- console.log('AI回复中未找到有效的大纲内容')
- }
- } else {
- console.log('已有大纲数据,跳过设置(避免覆盖用户编辑结果)')
- }
- } else {
- console.log('未找到AI回复内容')
- }
- // 设置大纲反馈状态(从AI消息中获取)
- if (aiMessage && aiMessage.rawData && aiMessage.rawData.user_feedback !== undefined) {
- outlineFeedback.value = aiMessage.rawData.user_feedback
- currentAiMessageId.value = aiMessage.id
- console.log('设置大纲反馈状态:', outlineFeedback.value, 'AI消息ID:', currentAiMessageId.value)
- console.log('AI消息原始数据:', aiMessage.rawData)
- } else {
- outlineFeedback.value = null
- currentAiMessageId.value = null
- console.log('未找到AI消息或反馈状态,重置大纲反馈状态')
- }
- return true
- } else {
- console.error('获取安全培训对话消息失败:', response.statusCode)
- return false
- }
- } catch (error) {
- console.error('获取安全培训对话消息失败:', error)
- return false
- }
- }
- // 生成对话标题(从内容中提取)
- const generateConversationTitle = (content) => {
- if (!content) return '未知对话'
- // 检查是否包含文件标签格式
- if (content.includes('</filesize>')) {
- // 提取</filesize>标签后面的内容
- const userMessage = content.split('</filesize>')[1]
- if (userMessage && userMessage.trim()) {
- // 清理多余的空格和换行
- const cleanMessage = userMessage.replace(/\s+/g, ' ').trim()
- // 提取第一句话作为标题
- const firstSentence = cleanMessage.split(/[。!?\n]/)[0]
- // 限制标题长度
- if (firstSentence.length > 30) {
- return firstSentence.substring(0, 30) + '...'
- }
- return firstSentence || '新对话'
- }
- }
- // 如果没有文件标签,使用原来的方法
- let cleanContent = content.replace(/<[^>]*>/g, '')
- cleanContent = cleanContent.replace(/\s+/g, ' ').trim()
- // 提取第一句话作为标题
- const firstSentence = cleanContent.split(/[。!?\n]/)[0]
- // 限制标题长度
- if (firstSentence.length > 30) {
- return firstSentence.substring(0, 30) + '...'
- }
- return firstSentence || '新对话'
- }
- // 格式化时间
- const formatTime = (timestamp) => {
- if (!timestamp) return '未知时间'
- // 处理时间戳
- let date
- if (typeof timestamp === 'string') {
- // 如果是ISO字符串格式,直接创建Date对象
- date = new Date(timestamp)
- } else {
- // 如果是数字时间戳
- let timestampMs = timestamp
- if (timestamp.toString().length === 10) {
- timestampMs = timestamp * 1000
- } else if (timestamp.toString().length === 11) {
- timestampMs = timestamp * 1000
- } else if (timestamp.toString().length === 13) {
- // 13位时间戳,直接使用
- } else {
- timestampMs = timestamp * 1000
- }
- date = new Date(timestampMs)
- }
- const now = new Date()
- // 获取今天的开始时间(0点0分0秒)
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- // 获取昨天的开始时间
- const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000)
- // 今天的对话(日期相同)
- if (date >= todayStart) {
- return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- // 昨天的对话(日期是昨天)
- if (date >= yesterdayStart && date < todayStart) {
- return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- // 更早的对话:显示为 "8月30日 15:30" 格式
- const month = date.getMonth() + 1 // getMonth() 返回 0-11
- const day = date.getDate()
- const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- return `${month}月${day}日 ${time}`
- }
- // 转换用户反馈状态
- const convertUserFeedback = (feedback) => {
- console.log('转换用户反馈状态:', feedback, '类型:', typeof feedback)
- switch (parseInt(feedback)) {
- case 2: return 'like' // 满意(赞)
- case 3: return 'dislike' // 不满意(踩)
- case 0:
- default: return null // 无反馈
- }
- }
- // 滚动到底部
- const scrollToBottom = () => {
- nextTick(() => {
- const chatContent = document.querySelector('.chat-content')
- console.log('滚动函数执行,找到聊天区域:', chatContent)
- if (chatContent) {
- console.log('滚动前 - scrollTop:', chatContent.scrollTop, 'scrollHeight:', chatContent.scrollHeight, 'clientHeight:', chatContent.clientHeight)
- // 强制滚动到底部
- chatContent.scrollTop = chatContent.scrollHeight
- // 如果上面的方法不工作,尝试其他方法
- setTimeout(() => {
- chatContent.scrollTop = chatContent.scrollHeight
- console.log('延迟滚动后 - scrollTop:', chatContent.scrollTop)
- }, 10)
- // 使用scrollIntoView方法作为备选
- const lastMessage = chatContent.lastElementChild
- if (lastMessage) {
- lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' })
- }
- console.log('滚动后 - scrollTop:', chatContent.scrollTop)
- } else {
- console.warn('未找到聊天内容区域')
- }
- })
- }
- // 检查是否在底部
- const isAtBottom = () => {
- const chatContent = document.querySelector('.chat-content')
- if (!chatContent) return true
- const threshold = 3.125 // 50px的容差 (50px)
- return chatContent.scrollHeight - chatContent.scrollTop - chatContent.clientHeight < threshold
- }
- // 删除历史记录
- const deleteHistoryItem = (historyItem, index) => {
- console.log('准备删除安全培训历史记录:', historyItem)
- // 设置要删除的项目并显示确认弹窗
- deleteTargetItem.value = { item: historyItem, index: index }
- showDeleteModal.value = true
- }
- // 确认删除历史记录
- const confirmDeleteHistory = async () => {
- if (!deleteTargetItem.value) return
- const { item: historyItem, index } = deleteTargetItem.value
- try {
- // 调用删除接口
- const response = await apis.deleteHistoryRecord({
- ai_conversation_id: historyItem.id
- })
- if (response.statusCode === 200) {
- // 删除成功,从列表中移除
- historyData.value.splice(index, 1)
- // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
- if (historyItem.isActive) {
- await createNewChat()
- }
- console.log('安全培训历史记录删除成功')
- ElMessage.success('删除成功')
- } else {
- console.error('删除安全培训历史记录失败:', response.msg)
- ElMessage.error(response.msg || '删除失败')
- }
- } catch (error) {
- console.error('删除安全培训历史记录失败:', error)
- ElMessage.error('删除失败,请稍后重试')
- } finally {
- // 关闭弹窗并清除目标项
- showDeleteModal.value = false
- deleteTargetItem.value = null
- }
- }
- // 取消删除
- const cancelDeleteHistory = () => {
- showDeleteModal.value = false
- deleteTargetItem.value = null
- }
- // 处理新建任务点击
- const handleNewChatClick = () => {
- console.log('点击新建任务按钮,isProcessing:', isProcessing.value)
- if (isProcessing.value) {
- console.log('正在处理中,无法新建任务')
- return
- }
- createNewChat()
- }
- // 处理历史记录项点击
- const handleHistoryItemClick = (item, index) => {
- console.log('点击历史记录项,isProcessing:', isProcessing.value, 'isGeneratingOutline:', isGeneratingOutline.value, 'item.isActive:', item.isActive, 'isSwitchingHistory:', isSwitchingHistory.value)
- if (item.isActive || isProcessing.value || isGeneratingOutline.value || isSwitchingHistory.value) {
- console.log('正在处理中、正在生成大纲、已激活或正在切换历史记录,无法切换')
- return
- }
- handleHistoryItem(item)
- }
- // 方法
- const createNewChat = async () => {
- console.log('创建新安全培训任务')
- // 重置所有状态
- ai_conversation_id.value = 0
- chatMessages.value = []
- messageText.value = ''
- selectedFile.value = null
- // 重置保存状态
- lastSavedOutlineData.value = null
- lastSavedPPTData.value = null
- isSaving.value = false
- showChat.value = false
- // 清除所有历史记录的选中状态
- historyData.value.forEach((item) => {
- item.isActive = false
- })
- // 重置步骤状态,回到第一步
- currentStep.value = 'step1'
- // 清除大纲相关数据
- outlineData.value = null
- outlineStats.value = {}
- outlineTitle.value = ''
- // 清除评价状态
- evaluation.value = ''
- outlineFeedback.value = null
- currentAiMessageId.value = null
- outlineId.value = null
- // 清除编辑状态
- editingItem.value = null
- editingType.value = ''
- editingIndex.value = null
- editingContent.value = ''
- // 清除PPT相关状态
- currentSlideIndex.value = 0
- selectedTemplate.value = 0
- showDownloadOptions.value = false
- selectedDownloadOption.value = 0
- pptSlides.value = []
- currentEditingSlide.value = { title: '', content: '' }
- // 清除PPT预览相关状态
- generatedPPT.value = []
- currentPPTSlideIndex.value = 0
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- zoom.value = 1
- selectedImageIndex.value = null
- // 重新初始化幻灯片图片数组
- slideImages.value = [
- template5Slide1, // 第1页
- template5Slide2, // 第2页
- template5Slide3, // 第3页
- template5Slide4, // 第4页
- template5Slide5 // 第5页
- ]
- // 刷新历史记录列表
- await getHistoryRecordList()
- }
- // 获取历史记录图片
- const getHistoryImage = (item) => {
- // 如果有封面图且不为空,使用封面图
- if (item.cover_image && item.cover_image.trim()) {
- return item.cover_image
- }
- // 否则使用默认图片
- return defaultHistoryIcon
- }
- // 从URL加载PPT数据
- const loadPPTFromUrl = async (pptJsonUrl) => {
- try {
- console.log('开始从URL加载PPT数据:', pptJsonUrl)
- // 使用后端API代理获取JSON数据
- const response = await apis.getPPTJson({ url: pptJsonUrl })
- console.log('从API获取PPT数据响应:', response)
- if (response.statusCode === 200 && response.data) {
- const pptData = response.data
- console.log('从URL加载的PPT数据:', pptData)
- // 设置PPT数据
- generatedPPT.value = pptData
- // 启用PPT预览模式
- showDownloadOptions.value = true
- console.log('PPT数据加载完成,已启用预览模式')
- return pptData
- } else {
- throw new Error(`API返回错误: ${response.msg || '未知错误'}`)
- }
- } catch (error) {
- console.error('从URL加载PPT数据失败:', error)
- // 如果API也失败,尝试重新生成PPT
- try {
- console.log('API加载失败,尝试重新生成PPT数据...')
- await loadAIPPTData()
- showDownloadOptions.value = true
- console.log('重新生成PPT数据完成')
- } catch (regenerateError) {
- console.error('重新生成PPT也失败:', regenerateError)
- throw error
- }
- }
- }
- // 保存步骤信息到后端
- const saveStepToBackend = async (updateCoverImage = true, forceSave = false) => {
- try {
- if (isSaving.value) {
- console.log('正在保存中,跳过重复保存请求')
- return false
- }
- // 检查PPT数据是否有变化
- if (!forceSave && !hasPPTDataChanged()) {
- console.log('PPT数据未发生变化,跳过保存')
- return false
- }
- isSaving.value = true
- console.log('开始保存步骤信息到后端...')
- // 生成PPT JSON数据
- const pptJsonData = {
- slides: generatedPPT.value,
- title: outlineTitle.value || '安全培训演示文稿',
- generatedAt: new Date().toISOString()
- }
- // 直接使用JSON数据,不再上传到OSS
- const pptJsonContent = JSON.stringify(pptJsonData, null, 2)
- console.log('PPT JSON数据已准备,长度:', pptJsonContent.length)
- // 封面图处理逻辑
- let coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0909_1757403783.png' // 默认封面图
- // 如果使用的是模板7(红色主题),使用指定的封面图
- if (selectedTemplateStyle.value === 'red') {
- coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png'
- console.log('使用模板7专用封面图:', coverImage)
- }
- // 如果使用的是模板8(蓝色科技主题),使用指定的封面图
- if (selectedTemplateStyle.value === 'blueTech') {
- coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png'
- console.log('使用模板8专用封面图:', coverImage)
- }
- if (updateCoverImage) {
- // 只有在需要更新封面图时才查找新的封面图
- // 遍历所有幻灯片,查找第一张用户上传的图片(排除模板默认图片)
- for (const slide of generatedPPT.value) {
- if (slide.elements && slide.elements.length > 0) {
- for (const element of slide.elements) {
- if (element.type === 'image' && element.src && element.src.startsWith('http')) {
- // 跳过模板中的默认图片(unsplash图片)
- if (element.src.includes('unsplash.com') || element.src.includes('placeholder')) {
- console.log('跳过模板默认图片:', element.src)
- continue
- }
- // 找到第一张用户上传的图片,使用它作为封面图
- coverImage = element.src
- console.log('使用用户上传的图片作为封面图:', coverImage)
- break
- }
- }
- if (coverImage !== 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0909_1757403783.png' &&
- coverImage !== 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png') {
- break // 已经找到用户上传的图片,跳出外层循环
- }
- }
- }
- } else {
- // 不更新封面图,保持使用现有的封面图
- if (ai_conversation_id.value) {
- const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
- if (currentHistoryItem && currentHistoryItem.cover_image && currentHistoryItem.cover_image.trim()) {
- coverImage = currentHistoryItem.cover_image
- console.log('保持使用现有封面图:', coverImage)
- }
- }
- }
- // 调用保存步骤接口
- const saveStepData = {
- ai_conversation_id: ai_conversation_id.value,
- step: 1, // 设置为步骤1,表示PPT已生成
- ppt_json_url: '', // 不再使用OSS URL
- ppt_json_content: JSON.stringify(generatedPPT.value), // 存储完整的PPT JSON内容
- cover_image: coverImage // 使用检测到的封面图
- }
- console.log('正在保存步骤信息:', saveStepData)
- const saveResponse = await apis.saveStep(saveStepData)
- if (saveResponse.statusCode === 200) {
- console.log('步骤信息保存成功')
- // 更新上次保存的PPT数据
- lastSavedPPTData.value = JSON.parse(JSON.stringify(generatedPPT.value)) // 深拷贝
- ElMessage.success('PPT生成完成并已保存!')
- return true
- } else {
- console.error('保存步骤信息失败:', saveResponse.msg)
- ElMessage.warning('PPT生成完成,但保存步骤信息失败')
- return false
- }
- } catch (error) {
- console.error('保存步骤信息时发生错误:', error)
- ElMessage.warning('PPT生成完成,但保存步骤信息时发生错误')
- return false
- } finally {
- isSaving.value = false
- }
- }
- // 点击历史记录
- const handleHistoryItem = async (historyItem) => {
- console.log('点击安全培训历史记录:', historyItem)
- // 设置切换状态,防止快速点击
- isSwitchingHistory.value = true
- try {
- // 立即更新激活状态,让用户看到反馈
- selectedHistoryItem.value = historyItem
- // 保存当前的历史记录ID,避免在异步保存过程中被覆盖
- const currentConversationId = ai_conversation_id.value
- const currentOutlineId = outlineId.value
- // 保存当前的数据快照,避免在异步保存过程中被覆盖
- const currentOutlineData = outlineData.value ? JSON.parse(JSON.stringify(outlineData.value)) : null
- const currentOutlineTitle = outlineTitle.value
- const currentOutlineStats = outlineStats.value ? JSON.parse(JSON.stringify(outlineStats.value)) : null
- const currentGeneratedPPT = generatedPPT.value ? JSON.parse(JSON.stringify(generatedPPT.value)) : null
- // 更新数据层的active状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === historyItem.id
- })
- // 设置加载状态
- isLoadingHistory.value = true
- // 异步保存当前数据(不阻塞UI更新)
- const saveCurrentData = async () => {
- // 只有在数据真正发生变化时才保存
- let hasChanges = false
- // 检查大纲数据是否有变化(使用数据快照)
- if (currentOutlineData && currentOutlineData.length > 0 && currentOutlineId) {
- // 检查数据快照是否有变化
- const hasChangesInSnapshot = !lastSavedOutlineData.value ||
- JSON.stringify({
- title: currentOutlineTitle,
- stats: currentOutlineStats,
- chapters: currentOutlineData
- }) !== JSON.stringify(lastSavedOutlineData.value)
- if (hasChangesInSnapshot) {
- console.log('检测到当前大纲数据有变化,保存当前修改到历史记录:', currentOutlineId)
- try {
- // 临时设置ai_conversation_id为当前要保存的历史记录ID
- const originalConversationId = ai_conversation_id.value
- ai_conversation_id.value = currentOutlineId
- // 临时设置数据快照
- const originalOutlineData = outlineData.value
- const originalOutlineTitle = outlineTitle.value
- const originalOutlineStats = outlineStats.value
- outlineData.value = currentOutlineData
- outlineTitle.value = currentOutlineTitle
- outlineStats.value = currentOutlineStats
- const saved = await saveOutlineToBackend()
- // 恢复ai_conversation_id
- ai_conversation_id.value = originalConversationId
- // 检查是否需要恢复数据(如果当前数据不是目标历史记录的数据)
- if (ai_conversation_id.value !== historyItem.id) {
- // 如果当前不是目标历史记录,恢复原始数据
- outlineData.value = originalOutlineData
- outlineTitle.value = originalOutlineTitle
- outlineStats.value = originalOutlineStats
- }
- if (saved) {
- hasChanges = true
- console.log('当前大纲修改已保存到历史记录:', currentOutlineId)
- // 更新历史记录列表中的大纲数据
- const currentHistoryItem = historyData.value.find(item => item.id === currentOutlineId)
- if (currentHistoryItem) {
- currentHistoryItem.rawData = {
- ...currentHistoryItem.rawData,
- ppt_outline: JSON.stringify({
- title: currentOutlineTitle,
- stats: currentOutlineStats,
- chapters: currentOutlineData,
- timestamp: Date.now()
- })
- }
- console.log('已更新历史记录列表中的大纲数据')
- }
- }
- } catch (error) {
- console.error('保存当前大纲修改失败:', error)
- }
- } else {
- console.log('大纲数据快照无变化,跳过保存')
- }
- }
- // 检查PPT数据是否有变化(使用数据快照)
- if (currentGeneratedPPT && currentGeneratedPPT.length > 0 && currentConversationId) {
- // 检查PPT数据快照是否有变化
- const hasChangesInPPTSnapshot = !lastSavedPPTData.value ||
- JSON.stringify(currentGeneratedPPT) !== JSON.stringify(lastSavedPPTData.value)
- if (hasChangesInPPTSnapshot) {
- console.log('检测到当前PPT数据有变化,保存当前修改到历史记录:', currentConversationId)
- try {
- // 临时设置ai_conversation_id为当前要保存的历史记录ID
- const originalConversationId = ai_conversation_id.value
- ai_conversation_id.value = currentConversationId
- // 临时设置PPT数据快照
- const originalGeneratedPPT = generatedPPT.value
- generatedPPT.value = currentGeneratedPPT
- const saved = await saveStepToBackend()
- // 恢复ai_conversation_id
- ai_conversation_id.value = originalConversationId
- // 检查是否需要恢复数据(如果当前数据不是目标历史记录的数据)
- if (currentConversationId !== historyItem.id) {
- // 如果当前不是目标历史记录,恢复原始数据
- generatedPPT.value = originalGeneratedPPT
- }
- if (saved) {
- hasChanges = true
- console.log('当前PPT修改已保存到历史记录:', currentConversationId)
- // 更新历史记录列表中的PPT数据
- const currentHistoryItem = historyData.value.find(item => item.id === currentConversationId)
- if (currentHistoryItem) {
- currentHistoryItem.rawData = {
- ...currentHistoryItem.rawData,
- ppt_json_content: JSON.stringify(currentGeneratedPPT),
- timestamp: Date.now()
- }
- console.log('已更新历史记录列表中的PPT数据')
- }
- }
- } catch (error) {
- console.error('保存当前PPT修改失败:', error)
- }
- } else {
- console.log('PPT数据快照无变化,跳过保存')
- }
- }
- if (!hasChanges) {
- console.log('当前数据无变化,跳过保存')
- }
- }
- // 更新ai_conversation_id(在保存任务启动前)
- ai_conversation_id.value = historyItem.id
- // 启动异步保存任务(不等待完成,避免阻塞UI)
- saveCurrentData()
- // 根据step字段决定跳转到哪个页面
- if (historyItem.step === 0) {
- // step为0,跳转到步骤二(大纲页)
- console.log('历史记录step为0,跳转到步骤二(大纲页)')
- // 清除PPT预览相关状态
- generatedPPT.value = []
- currentPPTSlideIndex.value = 0
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- zoom.value = 1
- selectedImageIndex.value = null
- showDownloadOptions.value = false
- // 重置为模板预览状态
- currentSlideIndex.value = 0
- // 显示加载状态
- const loadingMessage = {
- type: 'ai',
- content: '正在加载历史对话...',
- displayContent: '正在加载历史对话...',
- isTyping: true,
- id: Date.now() + 1,
- userFeedback: null
- }
- chatMessages.value = [loadingMessage]
- try {
- // 获取该对话的详细消息
- const success = await getConversationMessages(historyItem.id)
- if (success) {
- console.log('安全培训历史对话加载成功', historyItem)
- // 移除加载消息
- chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
- // 数据加载完成后再设置步骤
- currentStep.value = 'step2'
- // 清除加载状态
- isLoadingHistory.value = false
- // 检查是否有大纲数据(优先使用outlineData.value,因为getConversationMessages可能已经设置了)
- // 但是要确保这是当前历史记录的大纲数据,不是上一个历史记录的
- if (outlineData.value && outlineData.value.length > 0 && ai_conversation_id.value === historyItem.id) {
- console.log('检测到已有大纲数据,直接使用(来自getConversationMessages)')
- // 设置大纲ID
- outlineId.value = historyItem.id
- // 更新保存状态,避免重复保存
- lastSavedOutlineData.value = {
- title: outlineTitle.value,
- stats: outlineStats.value,
- chapters: JSON.parse(JSON.stringify(outlineData.value)) // 深拷贝
- }
- console.log('设置大纲ID:', outlineId.value)
- // 跳转到步骤二
- currentStep.value = 'step2'
- console.log('已跳转到步骤二:培训大纲界面(使用getConversationMessages数据)')
- // 确保模板预览数据已加载
- loadLocalPPT()
- updateSlideThumbnails()
- } else {
- console.log('未找到大纲数据,显示聊天界面')
- showChat.value = true
- }
- } else {
- // 加载失败,显示错误消息
- chatMessages.value = [{
- type: 'ai',
- content: '抱歉,加载历史对话失败,请稍后重试。',
- displayContent: '抱歉,加载历史对话失败,请稍后重试。',
- isTyping: false,
- id: Date.now() + 1,
- userFeedback: null
- }]
- showChat.value = true
- }
- } catch (error) {
- console.error('加载安全培训历史对话失败:', error)
- chatMessages.value = [{
- type: 'ai',
- content: '抱歉,加载历史对话时发生错误,请稍后重试。',
- displayContent: '抱歉,加载历史对话时发生错误,请稍后重试。',
- isTyping: false,
- id: Date.now() + 1,
- userFeedback: null
- }]
- showChat.value = true
- // 清除加载状态
- isLoadingHistory.value = false
- }
- } else if (historyItem.step === 1) {
- // step为1,直接跳转到PPT预览页面
- console.log('历史记录step为1,直接跳转到PPT预览页面')
- // 清除之前的PPT预览状态,准备加载新的PPT数据
- generatedPPT.value = []
- currentPPTSlideIndex.value = 0
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- zoom.value = 1
- selectedImageIndex.value = null
- // 显示加载状态
- const loadingMessage = {
- type: 'ai',
- content: '正在加载PPT数据...',
- displayContent: '正在加载PPT数据...',
- isTyping: true,
- id: Date.now() + 1,
- userFeedback: null
- }
- chatMessages.value = [loadingMessage]
- try {
- // 获取该对话的详细消息
- const success = await getConversationMessages(historyItem.id)
- if (success) {
- console.log('安全培训历史对话加载成功')
- // 移除加载消息
- chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
- // 数据加载完成后再设置步骤
- currentStep.value = 'step3'
- // 清除加载状态
- isLoadingHistory.value = false
- // 获取最新的AI回复消息并解析大纲
- const aiMessages = chatMessages.value.filter(msg => msg.type === 'ai')
- const aiMessage = aiMessages.length > 1 ? aiMessages[1] : (aiMessages.length > 0 ? aiMessages[0] : null)
- if (aiMessage && aiMessage.content) {
- console.log('找到AI回复,开始解析大纲并直接生成PPT预览')
- // 设置大纲ID
- outlineId.value = historyItem.id
- // 解析AI回复获取大纲数据
- const parsedOutline = parseOutlineFromAI(aiMessage.content)
- if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
- outlineData.value = parsedOutline.chapters
- outlineTitle.value = parsedOutline.title || '安全培训大纲'
- // 使用新的统计计算函数
- outlineStats.value = calculateOutlineStats(parsedOutline.chapters)
- // step=1时,直接从数据库读取已保存的PPT数据
- try {
- console.log('step=1,从数据库读取已保存的PPT数据...')
- // 检查是否有保存的PPT JSON内容
- if (historyItem.ppt_json_content && historyItem.ppt_json_content.trim()) {
- console.log('找到已保存的PPT JSON内容,直接加载...')
- // 解析JSON内容
- const savedPPTData = JSON.parse(historyItem.ppt_json_content)
- console.log('解析的PPT数据:', savedPPTData)
- // 直接设置PPT数据(后端数据已经是最新的)
- generatedPPT.value = savedPPTData
- showDownloadOptions.value = true
- // 更新保存状态,避免重复保存
- lastSavedPPTData.value = JSON.parse(JSON.stringify(savedPPTData)) // 深拷贝
- console.log('PPT数据加载完成,共', savedPPTData.length, '张幻灯片')
- // 确保历史记录列表中的数据也是最新的
- const updatedHistoryItem = historyData.value.find(item => item.id === historyItem.id)
- if (updatedHistoryItem) {
- updatedHistoryItem.ppt_json_content = historyItem.ppt_json_content
- console.log('已同步历史记录列表中的PPT数据')
- }
- } else {
- console.log('未找到保存的PPT内容,重新生成...')
- // 如果没有保存的内容,则重新生成
- const aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
- const filledAIPPTData = await fillAIPPTContent(aipptData, outlineTitle.value)
- await loadAIPPTData(filledAIPPTData, false)
- showDownloadOptions.value = true
- console.log('PPT数据重新生成完成')
- }
- } catch (error) {
- console.error('加载PPT失败:', error)
- ElMessage.error('加载PPT失败: ' + error.message)
- }
- } else {
- console.log('解析大纲失败,显示聊天界面')
- showChat.value = true
- }
- } else {
- console.log('未找到AI回复,显示聊天界面')
- showChat.value = true
- }
- } else {
- // 加载失败,显示错误消息
- chatMessages.value = [{
- type: 'ai',
- content: '抱歉,加载历史对话失败,请稍后重试。',
- displayContent: '抱歉,加载历史对话失败,请稍后重试。',
- isTyping: false,
- id: Date.now() + 1,
- userFeedback: null
- }]
- showChat.value = true
- }
- } catch (error) {
- console.error('加载安全培训历史对话失败:', error)
- chatMessages.value = [{
- type: 'ai',
- content: '抱歉,加载历史对话时发生错误,请稍后重试。',
- displayContent: '抱歉,加载历史对话时发生错误,请稍后重试。',
- isTyping: false,
- id: Date.now() + 1,
- userFeedback: null
- }]
- showChat.value = true
- // 清除加载状态
- isLoadingHistory.value = false
- }
- } else {
- // 其他情况,显示聊天界面
- console.log('历史记录step未知,显示聊天界面')
- showChat.value = true
- }
- } catch (error) {
- console.error('处理历史记录切换失败:', error)
- ElMessage.error('切换历史记录失败,请稍后重试')
- } finally {
- // 重置切换状态
- isSwitchingHistory.value = false
- isLoadingHistory.value = false
- }
- }
- // 功能卡片图标计数器
- let functionCardIconIndex = 0
- // 推荐问题图标计数器
- let questionIconIndex = 0
- // 根据功能卡片标题返回对应的图标
- const getFunctionCardIcon = (title) => {
- // 按顺序循环使用4个图标
- const icons = [safetyTrainingIcon, safetyAssessmentIcon, safetyRegulationsIcon, emergencyProceduresIcon]
- const icon = icons[functionCardIconIndex % icons.length]
- functionCardIconIndex++
- return icon
- }
- // 根据问题标题返回对应的图标
- const getQuestionIcon = (question) => {
- // 按顺序循环使用3个图标
- const icons = [questionIcon1, questionIcon2, questionIcon3]
- const icon = icons[questionIconIndex % icons.length]
- questionIconIndex++
- return icon
- }
- // 点击功能卡片
- const handleFunctionCard = (cardType) => {
- console.log('点击功能卡片:', cardType)
- // 显示聊天界面
- showChat.value = true
- // 根据卡片类型设置不同的消息
- let message = ''
- // 如果是后端数据,直接使用卡片标题作为消息
- if (typeof cardType === 'string' && cardType.length > 0) {
- message = `请详细介绍${cardType}的相关内容`
- } else {
- // 兼容原有的硬编码逻辑
- switch (cardType) {
- case 'safety-training':
- message = '请详细介绍安全培训课程的相关内容'
- break
- case 'safety-assessment':
- message = '请介绍安全评估测试的关键要点'
- break
- case 'safety-regulations':
- message = '请查询相关的安全法规和标准'
- break
- case 'emergency-procedures':
- message = '请介绍应急处理程序的关键步骤'
- break
- default:
- message = `请详细介绍${cardType}的相关内容`
- }
- }
- // 自动发送消息
- messageText.value = message
- sendMessage()
- }
- // 发送消息
- const sendMessage = async () => {
- if (messageText.value.trim() && !isSending.value) {
- console.log('开始发送消息:', messageText.value, '文件:', selectedFile.value)
- // 设置发送状态
- isSending.value = true
- isProcessing.value = true
- // 显示聊天界面
- showChat.value = true
- // 如果是新对话(没有历史记录或当前没有激活的历史记录),清除所有历史记录的选中状态
- if (chatMessages.value.length === 0) {
- historyData.value.forEach((item) => {
- item.isActive = false;
- });
- console.log('新对话开始,清除所有历史记录的选中状态');
- }
- // 添加用户消息
- const userMessage = {
- type: 'user',
- content: messageText.value,
- file: selectedFile.value, // 添加文件信息
- id: Date.now() // 添加唯一ID
- }
- chatMessages.value.push(userMessage)
- // 添加AI消息(初始状态为思考中)
- const aiMessage = {
- type: 'ai',
- content: '',
- displayContent: '',
- isTyping: true,
- id: Date.now() + 1, // 添加唯一ID
- userFeedback: null // 用户反馈状态:null, 'like', 'dislike'
- }
- chatMessages.value.push(aiMessage)
- // 清空输入框
- const currentMessage = messageText.value
- messageText.value = ''
- // 立即清理选中的文件
- if (selectedFile.value) {
- removeSelectedFile()
- }
- // 发送消息后滚动到底部
- scrollToBottom()
- console.log('当前聊天消息:', chatMessages.value)
- try {
- // 构建发送给AI的消息
- let messageToAI = currentMessage
- // 如果有文件,使用标签格式
- if (userMessage.file && userMessage.file.content) {
- messageToAI = `<word>${userMessage.file.content}</word><filename>${userMessage.file.name}</filename><filesize>${userMessage.file.size}</filesize>${currentMessage}`
- }
- // 调用后端DeepSeek接口
- const response = await apis.sendDeepseekMessage({
- // ai_conversation_id: ai_conversation_id.value,
- // ===== 已删除:user_id - 后端从token解析 =====
- business_type: 1,
- message: messageToAI
- })
- console.log('DeepSeek API响应:', response)
- if (response.statusCode === 200) {
- const aiReply = response.data.reply
- ai_conversation_id.value = response.data.ai_conversation_id
- // 处理特殊字符和表情符号
- const processedReply = processAIResponse(aiReply)
- // 处理文件标签格式的回显
- const processedReplyWithFileDisplay = processFileDisplay(processedReply, userMessage.file)
- // 将Markdown格式转换为HTML格式
- const htmlReply = markdownToHtml(processedReplyWithFileDisplay)
- // 开始打字效果 - 按完整HTML标签或文本块显示,避免标签分割
- const textBlocks = []
- let currentBlock = ''
- let inTag = false
- let tagContent = ''
- // 将HTML内容分割成完整的块
- for (let i = 0; i < htmlReply.length; i++) {
- const char = htmlReply[i]
- if (char === '<') {
- // 如果之前有文本内容,先保存
- if (currentBlock && !inTag) {
- textBlocks.push({ type: 'text', content: currentBlock })
- currentBlock = ''
- }
- inTag = true
- tagContent = char
- } else if (char === '>') {
- // 标签结束
- tagContent += char
- textBlocks.push({ type: 'tag', content: tagContent })
- inTag = false
- tagContent = ''
- } else if (inTag) {
- // 在标签内
- tagContent += char
- } else {
- // 普通文本
- currentBlock += char
- }
- }
- // 保存最后一个文本块
- if (currentBlock) {
- textBlocks.push({ type: 'text', content: currentBlock })
- }
- console.log('分割后的文本块:', textBlocks)
- let currentBlockIndex = 0
- let currentCharIndex = 0
- const typeInterval = setInterval(() => {
- if (currentBlockIndex < textBlocks.length) {
- const currentBlock = textBlocks[currentBlockIndex]
- if (currentBlock.type === 'tag') {
- // 标签直接显示,不分字符
- aiMessage.displayContent += currentBlock.content
- currentBlockIndex++
- currentCharIndex = 0
- } else {
- // 文本按字符显示
- if (currentCharIndex < currentBlock.content.length) {
- const newContent = aiMessage.displayContent + currentBlock.content[currentCharIndex]
- aiMessage.displayContent = newContent
- currentCharIndex++
- } else {
- // 当前文本块完成,移动到下一个
- currentBlockIndex++
- currentCharIndex = 0
- }
- }
- // 强制触发Vue响应式更新
- chatMessages.value = [...chatMessages.value]
- // 逐步扩展宽度
- const messageElement = document.querySelector(`[data-message-index="${chatMessages.value.length - 1}"] .message-content`)
- if (messageElement) {
- messageElement.style.width = 'fit-content'
- }
- // 每显示一个字符都滚动到底部,确保长回复时滚动条始终跟随
- scrollToBottom()
- // 强制触发DOM更新后再滚动一次
- nextTick(() => {
- scrollToBottom()
- })
- } else {
- // 所有块都显示完成
- aiMessage.isTyping = false
- aiMessage.content = processedReply // 保存完整内容
- clearInterval(typeInterval)
- console.log('打字完成')
- // 打字完成后设置最终宽度
- const messageElement = document.querySelector(`[data-message-index="${chatMessages.value.length - 1}"] .message-content`)
- if (messageElement) {
- messageElement.style.width = '100%'
- }
- // 强制触发Vue响应式更新
- setTimeout(async () => {
- chatMessages.value = [...chatMessages.value]
- console.log('打字完成,AI回复内容已全部显示')
- // AI回复完成后,获取最新的历史记录
- await getHistoryRecordList()
- // 如果是新对话,将最新的历史记录设为激活状态
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- // 设置大纲ID(使用对话ID作为大纲ID)
- outlineId.value = ai_conversation_id.value
- console.log('设置新对话的大纲ID:', outlineId.value);
- }
- // 打字完成后强制滚动到底部,确保长回复完全可见
- scrollToBottom()
- // AI回复完全结束后,解禁历史记录和新建任务
- isProcessing.value = false
- console.log('AI回复完成,解禁历史记录和新建任务')
- // 立即检查是否为PPT生成需求,如果是则跳转到步骤二
- console.log('AI回复完成,开始检查是否为PPT需求')
- checkAndNavigateToOutline(processedReply)
- }, 100)
- }
- }, 20) // 每20ms显示一个字符,更频繁地滚动
- } else {
- // API调用失败
- aiMessage.isTyping = false
- aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后重试。'
- aiMessage.displayContent = aiMessage.content
- console.error('DeepSeek API调用失败:', response)
- // API调用失败时也要解禁历史记录和新建任务
- isProcessing.value = false
- }
- } catch (error) {
- console.error('发送消息失败:', error)
- // 显示错误消息
- aiMessage.isTyping = false
- aiMessage.content = '抱歉,网络连接出现问题,请检查网络后重试。'
- aiMessage.displayContent = aiMessage.content
- // 网络错误时也要解禁历史记录和新建任务
- isProcessing.value = false
- } finally {
- // 重置发送状态
- isSending.value = false
- }
- }
- }
- // 生成安全培训智能提示词
- const generateSafetyTrainingPrompt = () => {
- return `请根据用户的需求进行回复,无论用户说什么都要提取关键词生成安全培训PPT大纲,请严格按照以下格式回复:
- 以下是为您准备的PPT大纲,包含相关内容:
- [在这里生成大纲内容,使用Markdown格式,包含:
- 1. 章节标题(如:第一章 xxxx)
- 2. 小节内容(如:1.1 xxxx)
- 3. 子小节内容(如:1.1.1 xxxx)
- 大纲统计信息:
- - 总章节数:X章
- - 总小节数:X小节
- - 预计PPT页数:X-X页
- - 预计讲解时长:X-X分钟]
- 请确保:
- 1. 每个章节都以"第X章"开头
- 2. 每个小节都以"X.X"开头
- 3. 每个子小节都以"X.X.X"开头
- 4. 统计信息严格按照上述格式
- 5. 不要添加其他无关内容或占位符
- 6. 内容要符合安全培训的主题,包括但不限于:安全生产法规、安全操作规程、事故预防、应急处理、安全文化建设等`;
- }
- // 生成新大纲
- const generateNewOutline = async () => {
- try {
- // 设置生成状态
- isGeneratingOutline.value = true
- isProcessing.value = true
- // 锁定当前的ai_conversation_id,防止在生成过程中被切换
- const lockedConversationId = ai_conversation_id.value
- // 获取当前大纲标题作为请求内容
- const currentTitle = outlineTitle.value || '安全培训大纲'
- const requestMessage = `${currentTitle}`
- console.log('开始生成新大纲111:', requestMessage)
- console.log("锁定的ai_conversation_id:", lockedConversationId)
- console.log("outlineTitle.value:", outlineTitle.value)
- // 调用后端DeepSeek接口
- const response = await apis.sendDeepseekMessage({
- business_type: 1,
- message: outlineTitle.value,
- ai_conversation_id: lockedConversationId
- })
- console.log('新大纲生成API响应:', response)
- if (response.statusCode === 200) {
- const aiReply = response.data.reply
- // 处理特殊字符和表情符号
- const processedReply = processAIResponse(aiReply)
- // 解析新大纲
- const newOutline = parseOutlineFromAI(processedReply)
- if (newOutline && newOutline.chapters && newOutline.chapters.length > 0) {
- console.log('新大纲解析成功:', newOutline)
- // 更新大纲数据
- outlineData.value = newOutline.chapters
- outlineStats.value = newOutline.stats
- outlineTitle.value = newOutline.title
- // 重置反馈状态(新大纲没有反馈)
- outlineFeedback.value = null
- evaluation.value = ''
- // currentAiMessageId.value = null
- outlineId.value = null
- // 立即保存新生成的大纲到后端,防止切换历史记录时数据被覆盖
- // 直接调用保存接口,不依赖全局的ai_conversation_id
- await saveOutlineToBackendDirectly(lockedConversationId, newOutline.chapters, newOutline.title, newOutline.stats)
- ElMessage.success('大纲已生成')
- } else {
- console.log('新大纲解析失败')
- ElMessage.error('新大纲生成失败,请重试')
- }
- } else {
- // API调用失败
- console.error('新大纲生成API调用失败:', response)
- ElMessage.error('生成失败,请重试')
- }
- } catch (error) {
- console.error('生成新大纲失败:', error)
- ElMessage.error('生成失败,请重试')
- } finally {
- // 重置生成状态
- isGeneratingOutline.value = false
- isProcessing.value = false
- }
- }
- // 确保题目初始值正确
- const ensureQuestionInitialValues = (examData) => {
- // 单选题
- if (examData.singleChoice && examData.singleChoice.questions) {
- examData.singleChoice.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = ""
- }
- if (!question.options || question.options.length === 0) {
- question.options = [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ]
- }
- })
- }
- // 判断题
- if (examData.judge && examData.judge.questions) {
- examData.judge.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = ""
- }
- })
- }
- // 多选题
- if (examData.multiple && examData.multiple.questions) {
- examData.multiple.questions.forEach(question => {
- if (!question.selectedAnswers) {
- question.selectedAnswers = []
- }
- if (!question.options || question.options.length === 0) {
- question.options = [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ]
- }
- })
- }
- // 简答题
- if (examData.short && examData.short.questions) {
- examData.short.questions.forEach(question => {
- if (!question.outline) {
- question.outline = { keyFactors: "答题要点、关键因素、示例答案" }
- }
- })
- }
- }
- // 生成默认考试
- const generateDefaultExam = () => {
- return {
- title: "安全培训考试",
- totalScore: 100,
- totalQuestions: 17,
- singleChoice: {
- scorePerQuestion: 5,
- totalScore: 25,
- count: 5,
- questions: []
- },
- judge: {
- scorePerQuestion: 3,
- totalScore: 15,
- count: 5,
- questions: []
- },
- multiple: {
- scorePerQuestion: 8,
- totalScore: 40,
- count: 5,
- questions: []
- },
- short: {
- scorePerQuestion: 10,
- totalScore: 20,
- count: 2,
- questions: []
- }
- }
- }
- // 导出考试文件为Word格式
- const exportExamToFile = (examData) => {
- try {
- // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
- const wordContent = createExamWordContent(examData)
- // 创建Blob对象 - 使用Word兼容的MIME类型
- const blob = new Blob([wordContent], {
- type: 'application/msword'
- })
- // 下载文件
- const url = URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.setAttribute('href', url)
- link.setAttribute('download', `${examData.title}_${new Date().toISOString().split('T')[0]}.doc`)
- link.style.visibility = 'hidden'
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- URL.revokeObjectURL(url)
- ElMessage.success('考试文件已下载!')
- } catch (error) {
- console.error('导出考试文件失败:', error)
- ElMessage.error('导出考试文件失败,请重试')
- }
- }
- // 创建Word格式的考试文档内容(兼容WPS和Word)
- const createExamWordContent = (examData) => {
- const currentTime = new Date().toLocaleString('zh-CN')
- // HTML文档内容,使用Word兼容的格式
- let htmlContent = `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word 15">
- <meta name="Originator" content="Microsoft Word 15">
- <title>${examData.title || '考试试卷'}</title>
- <!--[if gte mso 9]>
- <xml>
- <w:WordDocument>
- <w:View>Print</w:View>
- <w:Zoom>100</w:Zoom>
- <w:DoNotPromptForConvert/>
- <w:DoNotShowRevisions/>
- <w:DoNotPrintRevisions/>
- <w:DoNotShowComments/>
- <w:DoNotShowInsertionsAndDeletions/>
- <w:DoNotShowPropertyChanges/>
- <w:Compatibility>
- <w:BreakWrappedTables/>
- <w:SnapToGridInCell/>
- <w:WrapTextWithPunct/>
- <w:UseAsianBreakRules/>
- <w:DontGrowAutofit/>
- </w:Compatibility>
- </w:WordDocument>
- </xml>
- <![endif]-->
- <style>
- body {
- font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- margin: 24px;
- color: #000;
- }
- .header {
- text-align: center;
- margin-bottom: 14px;
- }
- .exam-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- }
- .exam-info {
- font-size: 14px;
- color: #666;
- margin-bottom: 14px;
- }
- .section {
- margin-bottom: 14px;
- }
- .section-title {
- font-size: 18px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- border-bottom: 2px solid #3e7bfa;
- padding-bottom: 5px;
- }
- .question {
- margin-bottom: 14px;
- padding: 10px;
- background-color: #f9f9f9;
- border-left: 4px solid #3e7bfa;
- }
- .question-header {
- margin-bottom: 14px;
- line-height: 1.6;
- }
- .question-number {
- font-weight: bold;
- color: #3e7bfa;
- }
- .options {
- margin-left: 12px;
- }
- .option {
- margin-bottom: 5px;
- }
- .answer {
- margin-top: 10px;
- padding: 8px;
- background: #e8f4fd;
- border-radius: 4px;
- font-weight: bold;
- color: #2c5aa0;
- }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="exam-title">${examData.title || '考试试卷'}</div>
- <div class="exam-info">
- 总分:${examData.totalScore || 0}分 | 总题数:${examData.totalQuestions || 0}题 | 生成时间:${currentTime}
- </div>
- </div>`
- // 单选题
- if (examData.singleChoice && examData.singleChoice.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">一、单选题(${examData.singleChoice.count}题,每题${examData.singleChoice.scorePerQuestion}分,共${examData.singleChoice.totalScore}分)</div>`
- examData.singleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="options">`
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`
- })
- htmlContent += `
- </div>
- <div class="answer">正确答案:${question.selectedAnswer}</div>
- </div>`
- })
- htmlContent += `
- </div>`
- }
- // 判断题
- if (examData.judge && examData.judge.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">二、判断题(${examData.judge.count}题,每题${examData.judge.scorePerQuestion}分,共${examData.judge.totalScore}分)</div>`
- examData.judge.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="answer">正确答案:${question.selectedAnswer}</div>
- </div>`
- })
- htmlContent += `
- </div>`
- }
- // 多选题
- if (examData.multiple && examData.multiple.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">三、多选题(${examData.multiple.count}题,每题${examData.multiple.scorePerQuestion}分,共${examData.multiple.totalScore}分)</div>`
- examData.multiple.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="options">`
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`
- })
- htmlContent += `
- </div>
- <div class="answer">正确答案:${question.selectedAnswers.join(', ')}</div>
- </div>`
- })
- htmlContent += `
- </div>`
- }
- // 简答题
- if (examData.short && examData.short.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">四、简答题(${examData.short.count}题,每题${examData.short.scorePerQuestion}分,共${examData.short.totalScore}分)</div>`
- examData.short.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="answer">答题要点:${question.outline.keyFactors}</div>
- </div>`
- })
- htmlContent += `
- </div>`
- }
- htmlContent += `
- </body>
- </html>`
- return htmlContent
- }
- // 检查并跳转到大纲页面
- const checkAndNavigateToOutline = (aiReply) => {
- console.log('AI回复完成,直接解析并跳转到步骤二')
- // 直接解析AI回复,提取大纲信息
- const parsedOutline = parseOutlineFromAI(aiReply)
- if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
- console.log('解析成功,更新大纲数据:', parsedOutline)
- // 更新大纲数据
- outlineData.value = parsedOutline.chapters
- outlineStats.value = parsedOutline.stats
- outlineTitle.value = parsedOutline.title
- // 只有在没有现有反馈状态时才重置(新生成的大纲)
- if (outlineFeedback.value === null && currentAiMessageId.value === null) {
- console.log('新生成的大纲,重置反馈状态')
- outlineFeedback.value = null
- evaluation.value = ''
- outlineId.value = null
- // 立即跳转到步骤二
- currentStep.value = 'step2'
- console.log('已跳转到步骤二:培训大纲界面')
- console.log('当前大纲数据:', outlineData.value)
- console.log('当前统计信息:', outlineStats.value)
- console.log('当前标题:', outlineTitle.value)
- // 确保模板预览数据已加载
- loadLocalPPT()
- updateSlideThumbnails()
- // 不需要自动保存,后台会自动保存AI回复内容
- } else {
- console.log('从历史记录加载的大纲,保持现有反馈状态:', outlineFeedback.value)
- // 立即跳转到步骤二
- currentStep.value = 'step2'
- console.log('已跳转到步骤二:培训大纲界面')
- console.log('当前大纲数据:', outlineData.value)
- console.log('当前统计信息:', outlineStats.value)
- console.log('当前标题:', outlineTitle.value)
- // 确保模板预览数据已加载
- loadLocalPPT()
- updateSlideThumbnails()
- // 从历史记录加载的大纲不需要保存
- }
- } else {
- console.log('解析失败或没有章节数据')
- }
- }
- // 从AI回复中解析大纲信息
- const parseOutlineFromAI = (aiReply) => {
- try {
- console.log('开始解析AI回复中的大纲信息')
- console.log('AI回复内容:', aiReply)
- // 提取大纲标题 - 从AI回复的第一行或标题行提取
- let title = '安全培训大纲' // 默认标题
- // 尝试从第一行提取标题
- const lines = aiReply.split('\n')
- for (let line of lines) {
- const trimmedLine = line.trim()
- // 查找包含"以下是为您准备的PPT大纲"的行,下一行通常是标题
- if (trimmedLine.includes('以下是为您准备的PPT大纲')) {
- // 查找下一行作为标题
- const nextLineIndex = lines.indexOf(line) + 1
- if (nextLineIndex < lines.length) {
- const nextLine = lines[nextLineIndex].trim()
- if (nextLine && nextLine.length > 0 && !nextLine.includes('以下') && !nextLine.includes('大纲统计信息')) {
- title = nextLine
- break
- }
- }
- }
- // 或者直接查找以#开头的标题行
- if (trimmedLine.startsWith('#') && trimmedLine.length > 1) {
- title = trimmedLine.replace(/^#+\s*/, '').trim()
- break
- }
- }
- // 先解析章节内容,然后自动计算统计信息
- // 不再从AI回复中提取统计信息,而是根据解析的章节内容自动计算
- // 提取章节内容 - 使用基于井号数量的解析逻辑
- const chapters = []
- const contentLines = aiReply.split('\n')
- let currentChapter = null
- console.log('开始解析行数:', lines.length)
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i]
- const trimmedLine = line.trim()
- console.log(`第${i}行: "${trimmedLine}"`)
- // 根据井号数量来解析不同层级的标题
- const hashCount = (trimmedLine.match(/^#+/) || [''])[0].length
- if (hashCount === 1) {
- // # 开头的大标题,作为整个大纲的标题
- console.log('找到大标题:', trimmedLine)
- let mainTitle = trimmedLine.replace(/^#\s*/, '').trim()
- title = mainTitle
- continue
- } else if (hashCount === 2) {
- // ## 开头的标题作为章节
- console.log('找到章节:', trimmedLine)
- let chapterTitle = trimmedLine.replace(/^##\s*/, '').trim()
- currentChapter = {
- title: chapterTitle,
- sections: []
- }
- chapters.push(currentChapter)
- continue
- } else if (hashCount === 3) {
- // ### 开头的标题作为小节
- if (currentChapter) {
- console.log('找到小节:', trimmedLine)
- let sectionTitle = trimmedLine.replace(/^###\s*/, '').trim()
- currentChapter.sections.push({
- title: sectionTitle,
- subsections: []
- })
- continue
- }
- } else if (hashCount === 4) {
- // #### 开头的标题作为子标题
- if (currentChapter && currentChapter.sections.length > 0) {
- console.log('找到子标题:', trimmedLine)
- let subsectionTitle = trimmedLine.replace(/^####\s*/, '').trim()
- const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
- lastSection.subsections.push({
- title: subsectionTitle,
- subsubsections: []
- })
- continue
- }
- } else if (trimmedLine.startsWith('-')) {
- // - 开头的作为具体内容要点
- if (currentChapter && currentChapter.sections.length > 0) {
- console.log('找到具体内容要点:', trimmedLine)
- let contentTitle = trimmedLine.replace(/^-\s*/, '').trim()
- const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
- if (lastSection.subsections.length > 0) {
- const lastSubsection = lastSection.subsections[lastSection.subsections.length - 1]
- if (!lastSubsection.subsubsections) {
- lastSubsection.subsubsections = []
- }
- lastSubsection.subsubsections.push({
- title: contentTitle,
- content: ''
- })
- }
- continue
- }
- } else if (hashCount === 0 && trimmedLine.match(/^\d+\.\d+\.\d+/)) {
- // 没有井号但有三级数字格式的作为子小节
- if (currentChapter && currentChapter.sections.length > 0) {
- console.log('找到子小节(数字格式):', trimmedLine)
- const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
- lastSection.subsections.push({
- title: trimmedLine
- })
- continue
- }
- } else if (hashCount === 0 && trimmedLine.match(/^\d+\.\d+/)) {
- // 没有井号但有二级数字格式的作为小节
- if (currentChapter) {
- console.log('找到小节(数字格式):', trimmedLine)
- currentChapter.sections.push({
- title: trimmedLine,
- subsections: []
- })
- continue
- }
- }
- // 如果没有找到结构化内容,尝试将内容作为章节处理
- if (!currentChapter && trimmedLine && trimmedLine.length > 5 &&
- !trimmedLine.includes('以下') &&
- !trimmedLine.includes('以上') &&
- !trimmedLine.startsWith('#') &&
- !trimmedLine.includes('大纲统计信息')) {
- console.log('将内容作为章节处理:', trimmedLine)
- currentChapter = {
- title: trimmedLine,
- sections: []
- }
- chapters.push(currentChapter)
- }
- // 如果当前行看起来像内容,但不是结构化的,添加到当前章节
- if (currentChapter && trimmedLine && trimmedLine.length > 3 &&
- !trimmedLine.match(/^第[一二三四五六七八九十]+章/) &&
- !trimmedLine.match(/^第\d+章/) &&
- !trimmedLine.match(/^\d+\.\d+/) &&
- !trimmedLine.match(/^[一二三四五六七八九十]+\.\d+/) &&
- !trimmedLine.match(/^\d+\.\d+\.\d+/) &&
- !trimmedLine.match(/^[一二三四五六七八九十]+\.\d+\.\d+/) &&
- !trimmedLine.includes('以下') &&
- !trimmedLine.includes('以上') &&
- !trimmedLine.includes('大纲统计信息') &&
- !trimmedLine.includes('预计PPT页数') &&
- !trimmedLine.includes('预计讲解时长') &&
- !trimmedLine.includes('总章节数') &&
- !trimmedLine.includes('总小节数') &&
- !trimmedLine.startsWith('#')) {
- // 如果当前章节没有小节,创建一个默认小节
- if (currentChapter.sections.length === 0) {
- currentChapter.sections.push({
- title: '内容详情',
- subsections: []
- })
- }
- // 将内容添加到最后一个小节
- const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
- if (!lastSection.subsections) {
- lastSection.subsections = []
- }
- // 过滤掉一些无用的占位符文本和统计信息
- if (trimmedLine &&
- trimmedLine !== '内容要点' &&
- trimmedLine !== '概述' &&
- trimmedLine !== '内容详情' &&
- !trimmedLine.includes('总章节数') &&
- !trimmedLine.includes('总小节数') &&
- !trimmedLine.includes('预计PPT页数') &&
- !trimmedLine.includes('预计讲解时长') &&
- !trimmedLine.startsWith('-') &&
- trimmedLine.length > 2) {
- // 检查是否应该作为具体内容要点的正文内容
- const lastSubsection = lastSection.subsections[lastSection.subsections.length - 1]
- if (lastSubsection && lastSubsection.subsubsections && lastSubsection.subsubsections.length > 0) {
- // 如果最后一个子标题有具体内容要点,将内容添加到最后一个具体内容要点
- const lastSubsubsection = lastSubsection.subsubsections[lastSubsection.subsubsections.length - 1]
- if (lastSubsubsection && !lastSubsubsection.content) {
- lastSubsubsection.content = trimmedLine
- continue
- }
- }
- // 否则作为普通子小节
- lastSection.subsections.push({
- title: trimmedLine,
- subsubsections: []
- })
- }
- }
- }
- // 确保所有数据结构完整
- chapters.forEach(chapter => {
- if (chapter && chapter.sections) {
- chapter.sections.forEach(section => {
- if (section && !section.subsections) {
- section.subsections = []
- }
- // 确保每个子小节都有subsubsections数组
- if (section.subsections) {
- section.subsections.forEach(subsection => {
- if (!subsection.subsubsections) {
- subsection.subsubsections = []
- }
- })
- }
- })
- }
- })
- // 根据解析的章节内容自动计算统计信息
- const stats = calculateOutlineStats(chapters)
- return {
- title,
- stats,
- chapters
- }
- } catch (error) {
- console.error('解析大纲信息失败:', error)
- return null
- }
- }
- // 测试大纲转换为AIPPT.json格式并应用模板5
- const testOutlineToAIPPT = async () => {
- try {
- console.log('开始测试大纲转换为AIPPT.json格式并应用模板5')
- if (outlineData.value && outlineData.value.length > 0) {
- console.log('当前大纲数据:', outlineData.value)
- console.log('当前标题:', outlineTitle.value)
- // 显示loading效果
- const loadingInstance = ElMessage({
- message: '正在填充大纲内容,请稍候...',
- type: 'info',
- duration: 0,
- showClose: false
- })
- try {
- // 第一步:填充大纲内容
- console.log('开始填充大纲内容...')
- const enrichedOutlineData = await enrichOutlineContent(outlineData.value, outlineTitle.value)
- // 检查是否所有内容都填充完成
- let allContentFilled = true
- let missingItems = []
- for (let chapterIndex = 0; chapterIndex < enrichedOutlineData.length; chapterIndex++) {
- const chapter = enrichedOutlineData[chapterIndex]
- if (!chapter.content || chapter.content.trim() === '') {
- allContentFilled = false
- missingItems.push(`章节: ${chapter.title}`)
- }
- if (chapter.sections && chapter.sections.length > 0) {
- for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
- const section = chapter.sections[sectionIndex]
- if (!section.content || section.content.trim() === '') {
- allContentFilled = false
- missingItems.push(`小节: ${section.title}`)
- }
- if (section.subsections && section.subsections.length > 0) {
- for (let subsectionIndex = 0; subsectionIndex < section.subsections.length; subsectionIndex++) {
- const subsection = section.subsections[subsectionIndex]
- if (!subsection.content || subsection.content.trim() === '') {
- allContentFilled = false
- missingItems.push(`子小节: ${subsection.title}`)
- }
- }
- }
- }
- }
- }
- if (!allContentFilled) {
- loadingInstance.close()
- console.warn('部分内容填充失败:', missingItems)
- ElMessage.warning(`部分内容填充失败,请重试。失败项目: ${missingItems.slice(0, 3).join(', ')}${missingItems.length > 3 ? '...' : ''}`)
- return
- }
- // 更新loading消息
- loadingInstance.message = '正在转换大纲为PPT格式...'
- // 第二步:转换大纲为AIPPT.json格式
- const aipptData = convertOutlineToAIPPT(enrichedOutlineData, outlineTitle.value)
- console.log('转换结果:')
- console.log(JSON.stringify(aipptData, null, 2))
- // 更新loading消息
- loadingInstance.message = '正在应用模板5...'
- // 第三步:应用模板5
- await loadAIPPTData(aipptData)
- // 确保数据被正确保存到本地存储
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
- console.log('PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
- }
- // 大纲数据现在只存储在后端
- // 关闭loading
- loadingInstance.close()
- // 显示成功消息
- ElMessage.success('转换完成并已应用模板5,正在跳转到PPT预览...')
- // 自动跳转到步骤三(PPT预览)
- setTimeout(() => {
- currentStep.value = 'step3'
- }, 1000)
- } catch (error) {
- // 关闭loading
- loadingInstance.close()
- throw error
- }
- } else {
- ElMessage.warning('没有大纲数据,无法进行转换')
- }
- } catch (error) {
- console.error('测试转换失败:', error)
- ElMessage.error('转换失败: ' + error.message)
- }
- }
- // 监听语音识别结果
- watch(transcript, (newVal) => {
- if (!newVal || isListening.value) return
- messageText.value = newVal
- })
- // 监听语音识别错误
- watch(speechError, (newVal) => {
- if (newVal) {
- console.error('语音识别错误:', newVal)
- ElMessage.error(newVal)
- }
- })
- // 生命周期钩子
- onMounted(async () => {
- console.log('🚀 页面初始化开始,优先加载历史记录...')
- // 检查URL中是否有id参数,如果有,将其设置为当前的 ai_conversation_id
- if (route.query.id) {
- const id = parseInt(route.query.id);
- if (!isNaN(id) && id > 0) {
- ai_conversation_id.value = id;
- console.log('从URL获取到对话ID:', id);
-
- // 等待历史记录加载完成后,自动选中并触发点击事件
- const checkHistory = setInterval(() => {
- if (historyData.value && historyData.value.length > 0) {
- clearInterval(checkHistory);
- const targetItem = historyData.value.find(item => item.id === id);
- if (targetItem) {
- handleHistoryItem(targetItem);
- } else {
- handleHistoryItem({ id }); // 降级处理
- }
- }
- }, 500);
-
- // 5秒后自动清除定时器,避免死循环
- setTimeout(() => clearInterval(checkHistory), 5000);
- }
- }
- // 设置初始加载状态
- isLoadingHistory.value = true
- try {
- // 1. 首先获取历史记录列表(最高优先级)
- await getHistoryRecordList()
- console.log('✅ 历史记录加载完成')
- // 2. 并行获取其他非关键数据(不阻塞历史记录显示)
- // 注释掉功能卡片和热点问题获取,因为版本没有步骤三和四
- const otherDataPromise = Promise.all([
- getFunctionCards(),
- getHotQuestions()
- ])
- // 3. 初始化大纲统计信息
- // if (outlineData.value && outlineData.value.length > 0) {
- // updateOutlineStats()
- // }
- // 4. 初始化模板选择 - 注释掉,因为版本没有步骤三
- // if (templateStyles.value.length > 0) {
- // selectTemplate(0) // 选择第一个模板
- // console.log('初始化模板选择完成,当前模板:', templateStyles.value[0].title)
- // }
- // 5. 等待其他数据加载完成(后台进行) - 注释掉
- // try {
- // await otherDataPromise
- // console.log('✅ 其他数据加载完成')
- // } catch (error) {
- // console.warn('⚠️ 其他数据加载失败,但不影响主要功能:', error)
- // }
- console.log('🎉 页面初始化完成')
- } catch (error) {
- console.error('❌ 页面初始化失败:', error)
- } finally {
- // 清除加载状态
- isLoadingHistory.value = false
- }
- })
- // 点击推荐问题
- const handleRecommendedQuestion = (question) => {
- messageText.value = question
- console.log('选择推荐问题:', question)
- // 显示聊天界面
- showChat.value = true
- // 自动发送问题
- sendMessage()
- }
- // 获取评价状态(优先使用后端数据,如果没有则使用本地状态)
- const getEvaluationStatus = () => {
- console.log('getEvaluationStatus - outlineFeedback:', outlineFeedback.value, 'evaluation:', evaluation.value)
- if (outlineFeedback.value !== null) {
- // 根据后端数据判断状态
- switch (outlineFeedback.value) {
- case 2: return 'satisfied' // 满意
- case 3: return 'unsatisfied' // 不满意
- case 0: return '' // 无反馈(取消评价)
- default: return '' // 无反馈
- }
- }
- return evaluation.value // 如果没有后端数据,使用本地状态
- }
- // 设置大纲评价
- const setEvaluation = async (value) => {
- try {
- console.log('设置评价:', value)
- // 检查当前状态,如果点击的是当前已激活的状态,则取消评价
- const currentStatus = getEvaluationStatus()
- let feedbackValue
- if (currentStatus === value) {
- // 如果点击的是当前已激活的状态,取消评价
- feedbackValue = 0
- console.log('取消评价,发送0')
- } else {
- // 否则设置新的评价
- feedbackValue = value === 'satisfied' ? 2 : 3
- console.log('设置新评价:', feedbackValue)
- }
- console.log('currentAiMessageId.value', currentAiMessageId.value)
- // 调用后端API保存评价
- const response = await apis.likeAndDislike({
- id: currentAiMessageId.value, // 优先使用大纲ID,如果没有则使用AI消息ID
- user_feedback: feedbackValue
- })
- if (response.statusCode === 200) {
- console.log('评价保存成功')
- // 更新本地状态
- if (feedbackValue === 0) {
- // 取消评价
- evaluation.value = ''
- outlineFeedback.value = 0
- ElMessage.success('评价已取消')
- } else {
- // 设置新评价
- evaluation.value = value
- outlineFeedback.value = feedbackValue
- ElMessage.success('评价已保存')
- }
- } else {
- console.error('评价保存失败:', response)
- ElMessage.error('评价保存失败,请重试')
- }
- } catch (error) {
- console.error('设置评价失败:', error)
- ElMessage.error('评价设置失败,请重试')
- }
- }
- // 大纲编辑相关方法
- const startEditing = (item, type, index, content) => {
- editingItem.value = item
- editingType.value = type
- editingIndex.value = index
- // 提取纯文字内容,去掉编号部分
- let pureContent = content
- if (type === 'chapter') {
- // 章节:去掉"第一章"、"第二章"等前缀
- pureContent = content.replace(/^第[一二三四五六七八九十\d]+章\s*/, '')
- } else if (type === 'section') {
- // 小节:去掉"1.1"、"2.1"等前缀
- pureContent = content.replace(/^\d+\.\d+\s*/, '')
- } else if (type === 'subsection') {
- // 四级标题:去掉"#### "前缀
- pureContent = content.replace(/^####\s*/, '')
- } else if (type === 'subsubsection') {
- // 具体内容要点:去掉"- "前缀
- pureContent = content.replace(/^-\s*/, '')
- }
- editingContent.value = pureContent
- console.log('开始编辑:', { type, index, content: pureContent })
- }
- const saveEdit = async () => {
- if (!editingContent.value.trim()) {
- console.log('编辑内容为空,取消保存')
- cancelEdit()
- return
- }
- try {
- // 根据编辑类型更新对应的数据
- if (editingType.value === 'title') {
- outlineTitle.value = editingContent.value.trim()
- } else if (editingType.value === 'chapter') {
- const chapterIndex = editingIndex.value
- // 保持原有的章节编号格式,不重新生成
- const originalTitle = outlineData.value[chapterIndex].title
- const match = originalTitle.match(/^(第[一二三四五六七八九十\d]+章)\s*/)
- if (match) {
- // 保持原有的编号格式
- const chapterNumber = match[1]
- const chapterTitle = `${chapterNumber} ${editingContent.value.trim()}`
- outlineData.value[chapterIndex].title = chapterTitle
- } else {
- // 如果没有匹配到编号格式,使用中文数字格式
- const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
- const chapterNumber = chapterIndex < chineseNumbers.length ? chineseNumbers[chapterIndex] : (chapterIndex + 1).toString()
- const chapterTitle = `第${chapterNumber}章 ${editingContent.value.trim()}`
- outlineData.value[chapterIndex].title = chapterTitle
- }
- } else if (editingType.value === 'section') {
- const [chapterIndex, sectionIndex] = editingIndex.value.split('-')
- // 直接保存用户编辑的内容,不添加数字前缀
- outlineData.value[chapterIndex].sections[sectionIndex].title = editingContent.value.trim()
- } else if (editingType.value === 'subsection') {
- const [chapterIndex, sectionIndex, subsectionIndex] = editingIndex.value.split('-')
- // 直接保存用户编辑的内容,不添加数字前缀
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].title = editingContent.value.trim()
- } else if (editingType.value === 'subsubsection') {
- const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = editingIndex.value.split('-')
- // 直接保存用户编辑的内容,不添加数字前缀
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections[subsubsectionIndex].title = editingContent.value.trim()
- } else if (editingType.value === 'subsubsection-content') {
- const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = editingIndex.value.split('-')
- // 保存具体内容要点(-开头)下的正文内容
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections[subsubsectionIndex].content = editingContent.value.trim()
- }
- console.log('保存成功:', editingContent.value.trim())
- // 更新大纲统计信息
- updateOutlineStats()
- // 自动保存到后端
- await saveOutlineToBackend(true) // 强制保存编辑后的内容
- // 清除编辑状态
- cancelEdit()
- // 显示成功提示
- ElMessage.success('保存成功!')
- } catch (error) {
- console.error('保存失败:', error)
- ElMessage.error('保存失败,请重试')
- }
- }
- const cancelEdit = () => {
- editingItem.value = null
- editingType.value = ''
- editingIndex.value = null
- editingContent.value = ''
- console.log('取消编辑')
- }
- const toggleEditOptions = (itemId) => {
- showEditOptions.value = showEditOptions.value === itemId ? null : itemId
- }
- const deleteItem = async (type, index) => {
- try {
- if (type === 'chapter') {
- // 限制章节数量不能少于2章
- if (outlineData.value.length <= 2) {
- ElMessage.warning('至少需要保留2个章节')
- return
- }
- outlineData.value.splice(index, 1)
- } else if (type === 'section') {
- const [chapterIndex, sectionIndex] = index.split('-')
- // 限制每个章节至少保留1个小节
- if (outlineData.value[chapterIndex].sections.length <= 1) {
- ElMessage.warning('每个章节至少需要保留1个小节')
- return
- }
- outlineData.value[chapterIndex].sections.splice(sectionIndex, 1)
- } else if (type === 'subsection') {
- const [chapterIndex, sectionIndex, subsectionIndex] = index.split('-')
- outlineData.value[chapterIndex].sections[sectionIndex].subsections.splice(subsectionIndex, 1)
- } else if (type === 'subsubsection') {
- const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = index.split('-')
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.splice(subsubsectionIndex, 1)
- }
- // 更新统计信息
- updateOutlineStats()
- // 自动保存到后端
- await saveOutlineToBackend(true) // 强制保存删除后的内容
- ElMessage.success('删除成功!')
- } catch (error) {
- console.error('删除失败:', error)
- ElMessage.error('删除失败,请重试')
- }
- }
- // 智能编号生成函数
- const generateNextNumber = (type, parentIndex) => {
- try {
- if (type === 'chapter') {
- // 生成下一个章节编号
- const existingChapters = outlineData.value || []
- let maxChapterNum = 0
- // 查找现有的最大章节编号
- const chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
- existingChapters.forEach(chapter => {
- const match = chapter.title.match(/^第([一二三四五六七八九十\d]+)章/)
- if (match) {
- let num = 0
- const matchedText = match[1]
- // 如果是阿拉伯数字
- if (/^\d+$/.test(matchedText)) {
- num = parseInt(matchedText)
- } else {
- // 如果是中文数字,转换为阿拉伯数字
- const index = chineseNumbers.indexOf(matchedText)
- if (index > 0) {
- num = index
- }
- }
- maxChapterNum = Math.max(maxChapterNum, num)
- }
- })
- const nextChapterNum = maxChapterNum + 1
- // 最多6章,直接使用中文数字
- return `第${chineseNumbers[nextChapterNum]}章`
- } else if (type === 'section') {
- // 生成下一个小节编号
- const chapterIndex = parentIndex
- const existingSections = outlineData.value[chapterIndex]?.sections || []
- let maxSectionNum = 0
- // 查找现有的最大小节编号
- existingSections.forEach(section => {
- const match = section.title.match(/^\d+\.(\d+)/)
- if (match) {
- const num = parseInt(match[1]) || 0
- maxSectionNum = Math.max(maxSectionNum, num)
- }
- })
- const nextSectionNum = maxSectionNum + 1
- return `${chapterIndex + 1}.${nextSectionNum}`
- } else if (type === 'subsection') {
- // 生成下一个子小节编号
- const [chapterIndex, sectionIndex] = parentIndex.split('-')
- const existingSubsections = outlineData.value[chapterIndex]?.sections[sectionIndex]?.subsections || []
- let maxSubsectionNum = 0
- // 查找现有的最大子小节编号
- existingSubsections.forEach(subsection => {
- const match = subsection.title.match(/^\d+\.\d+\.(\d+)/)
- if (match) {
- const num = parseInt(match[1]) || 0
- maxSubsectionNum = Math.max(maxSubsectionNum, num)
- }
- })
- const nextSubsectionNum = maxSubsectionNum + 1
- return `${parseInt(chapterIndex) + 1}.${parseInt(sectionIndex) + 1}.${nextSubsectionNum}`
- } else if (type === 'subsubsection') {
- // 生成下一个具体内容要点(-开头)编号
- const [chapterIndex, sectionIndex, subsectionIndex] = parentIndex.split('-')
- const existingSubsubsections = outlineData.value[chapterIndex]?.sections[sectionIndex]?.subsections[subsectionIndex]?.subsubsections || []
- let maxSubsubsectionNum = 0
- // 查找现有的最大具体内容要点(-开头)编号
- existingSubsubsections.forEach(subsubsection => {
- const match = subsubsection.title.match(/^\d+\.\d+\.\d+\.(\d+)/)
- if (match) {
- const num = parseInt(match[1]) || 0
- maxSubsubsectionNum = Math.max(maxSubsubsectionNum, num)
- }
- })
- const nextSubsubsectionNum = maxSubsubsectionNum + 1
- return `${parseInt(chapterIndex) + 1}.${parseInt(sectionIndex) + 1}.${parseInt(subsectionIndex) + 1}.${nextSubsubsectionNum}`
- }
- return ''
- } catch (error) {
- console.error('生成编号失败:', error)
- return ''
- }
- }
- // 拖拽相关函数
- const handleDragStart = (event, chapterIndex) => {
- // 如果事件来自编辑框内部,不启动拖拽
- if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT' ||
- event.target.closest('.edit-input-container') || event.target.closest('.edit-input-wrapper')) {
- event.preventDefault()
- return
- }
- // 如果正在编辑状态,不启动拖拽
- if (editingType.value !== '') {
- event.preventDefault()
- return
- }
- draggedChapterIndex.value = chapterIndex
- event.dataTransfer.effectAllowed = 'move'
- event.dataTransfer.setData('text/html', event.target.outerHTML)
- event.target.style.opacity = '0.5'
- }
- const handleDragEnd = (event) => {
- event.target.style.opacity = '1'
- draggedChapterIndex.value = null
- dragOverChapterIndex.value = null
- }
- const handleDragOver = (event, chapterIndex) => {
- event.preventDefault()
- event.dataTransfer.dropEffect = 'move'
- dragOverChapterIndex.value = chapterIndex
- }
- const handleDragLeave = (event) => {
- // 只有当鼠标真正离开元素时才清除悬停状态
- if (!event.currentTarget.contains(event.relatedTarget)) {
- dragOverChapterIndex.value = null
- }
- }
- const handleDrop = async (event, targetChapterIndex) => {
- event.preventDefault()
- const sourceIndex = draggedChapterIndex.value
- const targetIndex = targetChapterIndex
- if (sourceIndex === null || sourceIndex === targetIndex) {
- return
- }
- try {
- // 移动章节
- const chapters = [...outlineData.value]
- const [movedChapter] = chapters.splice(sourceIndex, 1)
- chapters.splice(targetIndex, 0, movedChapter)
- // 重新生成所有章节的编号
- const chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
- chapters.forEach((chapter, index) => {
- // 提取章节标题中的内容部分(去掉"第X章"前缀)
- const contentMatch = chapter.title.match(/^第[一二三四五六七八九十\d]+章\s*(.+)$/)
- if (contentMatch) {
- const content = contentMatch[1]
- chapter.title = `第${chineseNumbers[index + 1]}章 ${content}`
- } else {
- // 如果没有匹配到标准格式,直接使用新编号
- chapter.title = `第${chineseNumbers[index + 1]}章 ${chapter.title}`
- }
- // 保持小节和子小节无序号格式
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- // 移除小节标题中的序号,只保留内容
- const sectionContentMatch = section.title.match(/^\d+\.\d+\s*(.+)$/)
- if (sectionContentMatch) {
- section.title = sectionContentMatch[1]
- }
- // 如果已经是无序号格式,保持不变
- // 保持子小节无序号格式
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- // 移除子小节标题中的序号,只保留内容
- const subsectionContentMatch = subsection.title.match(/^\d+\.\d+\.\d+\s*(.+)$/)
- if (subsectionContentMatch) {
- subsection.title = subsectionContentMatch[1]
- }
- // 如果已经是无序号格式,保持不变
- })
- }
- })
- }
- })
- // 更新数据
- outlineData.value = chapters
- // 更新统计信息
- updateOutlineStats()
- // 保存到后端
- await saveOutlineToBackend(true) // 强制保存调整后的内容
- ElMessage.success('章节顺序已调整,编号已重新排序')
- } catch (error) {
- console.error('调整章节顺序失败:', error)
- ElMessage.error('调整章节顺序失败,请重试')
- }
- // 清除拖拽状态
- draggedChapterIndex.value = null
- dragOverChapterIndex.value = null
- }
- const addNewItem = async (type, parentIndex) => {
- try {
- if (type === 'chapter') {
- // 限制章节数量不能超过6章
- if (outlineData.value.length >= 6) {
- ElMessage.warning('最多只能添加6个章节')
- return
- }
- const nextNumber = generateNextNumber('chapter', null)
- const newChapter = {
- title: `${nextNumber} 新章节`,
- sections: [{
- title: `新小节`,
- subsections: []
- }]
- }
- outlineData.value.push(newChapter)
- // 自动开始编辑新章节
- startEditing(newChapter, 'chapter', outlineData.value.length - 1, `${nextNumber} 新章节`)
- } else if (type === 'section') {
- const nextNumber = generateNextNumber('section', parentIndex)
- const newSection = {
- title: `${nextNumber} 新小节`,
- subsections: []
- }
- outlineData.value[parentIndex].sections.push(newSection)
- // 自动开始编辑新小节
- startEditing(newSection, 'section', `${parentIndex}-${outlineData.value[parentIndex].sections.length - 1}`, `新小节`)
- } else if (type === 'subsection') {
- const [chapterIndex, sectionIndex] = parentIndex.split('-')
- const nextNumber = generateNextNumber('subsection', parentIndex)
- const newSubsection = {
- title: `新子标题`,
- subsubsections: []
- }
- outlineData.value[chapterIndex].sections[sectionIndex].subsections.push(newSubsection)
- // 自动开始编辑新子标题
- startEditing(newSubsection, 'subsection', `${chapterIndex}-${sectionIndex}-${outlineData.value[chapterIndex].sections[sectionIndex].subsections.length - 1}`, `新子标题`)
- } else if (type === 'subsubsection') {
- const [chapterIndex, sectionIndex, subsectionIndex] = parentIndex.split('-')
- const nextNumber = generateNextNumber('subsubsection', parentIndex)
- const newSubsubsection = {
- title: `${nextNumber} 新具体内容要点`,
- content: ''
- }
- // 确保subsection有subsubsections数组
- if (!outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections) {
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections = []
- }
- outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.push(newSubsubsection)
- // 自动开始编辑新具体内容要点(-开头)
- startEditing(newSubsubsection, 'subsubsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.length - 1}`, `${nextNumber} 新具体内容要点`)
- }
- // 更新统计信息
- updateOutlineStats()
- // 自动保存到后端
- await saveOutlineToBackend(true) // 强制保存添加后的内容
- } catch (error) {
- console.error('添加失败:', error)
- ElMessage.error('添加失败,请重试')
- }
- }
- // 填充大纲内容 - 整体处理方案
- const enrichOutlineContent = async (outlineData, outlineTitle) => {
- console.log('开始填充大纲内容...')
- // 深拷贝大纲数据,避免修改原始数据
- const enrichedData = JSON.parse(JSON.stringify(outlineData))
- // 最大重试次数
- const maxRetries = 3
- let retryCount = 0
- while (retryCount < maxRetries) {
- try {
- console.log(`第 ${retryCount + 1} 次尝试填充内容...`)
- // 构建整体提示词
- const overallPrompt = `请为以下大纲数据填充内容,要求:
- 1. title字段:15字以内
- 2. text字段:20-50字以内
- 3. 内容要专业、实用、简洁
- 4. 保持JSON格式不变,填充空的内容
- 5. 请自由发挥,生成丰富多样的标题和内容
- 6. 重要:请确保所有空的内容都被填充,不要遗漏任何项目
- 7. 如果数据量较大,请耐心处理,确保完整性
- 大纲主题:${outlineTitle}
- 大纲数据:${JSON.stringify(enrichedData, null, 2)}
- 请直接返回完整的JSON数据,不要添加任何说明文字。确保所有content字段都有内容。`
- console.log('发送整体填充请求...')
- const response = await apis.reProduceSingleQuestion({ message: overallPrompt })
- console.log('API响应:', response)
- // 解析AI返回的JSON数据
- let aiResponse = null
- if (response && response.data) {
- if (response.data && typeof response.data === 'object' && response.data.reply) {
- aiResponse = response.data.reply
- } else if (response.data && typeof response.data === 'string') {
- aiResponse = response.data
- } else {
- aiResponse = JSON.stringify(response.data)
- }
- } else if (response && response.message) {
- aiResponse = response.message
- } else if (response && response.content) {
- aiResponse = response.content
- } else if (response && response.reply) {
- aiResponse = response.reply
- } else if (response && typeof response === 'string') {
- aiResponse = response
- } else if (response && typeof response === 'object') {
- aiResponse = JSON.stringify(response)
- }
- if (aiResponse && typeof aiResponse === 'string' && aiResponse.trim() !== '') {
- try {
- // 尝试解析JSON
- const parsedData = JSON.parse(aiResponse.trim())
- console.log('AI返回的JSON数据解析成功:', parsedData)
- // 验证数据结构
- if (Array.isArray(parsedData)) {
- // 验证内容完整性
- let totalItems = 0
- let filledItems = 0
- parsedData.forEach(chapter => {
- if (chapter.content && chapter.content.trim() !== '') {
- filledItems++
- }
- totalItems++
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach(section => {
- if (section.content && section.content.trim() !== '') {
- filledItems++
- }
- totalItems++
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach(subsection => {
- if (subsection.content && subsection.content.trim() !== '') {
- filledItems++
- }
- totalItems++
- })
- }
- })
- }
- })
- console.log(`内容填充统计: ${filledItems}/${totalItems} 项已填充`)
- if (filledItems < totalItems * 0.8) {
- console.warn('内容填充不完整,尝试重新填充...')
- retryCount++
- if (retryCount >= maxRetries) {
- throw new Error('多次尝试后内容填充仍不完整')
- }
- continue
- }
- console.log('大纲内容填充完成')
- return parsedData
- } else {
- throw new Error('AI返回的数据不是数组格式')
- }
- } catch (parseError) {
- console.error('解析AI返回的JSON失败:', parseError)
- console.log('AI返回的原始内容:', aiResponse)
- retryCount++
- if (retryCount >= maxRetries) {
- throw new Error('AI返回的数据格式不正确')
- }
- continue
- }
- } else {
- retryCount++
- if (retryCount >= maxRetries) {
- throw new Error('AI返回内容为空或格式不正确')
- }
- continue
- }
- } catch (error) {
- retryCount++
- if (retryCount >= maxRetries) {
- throw error
- }
- console.warn(`第 ${retryCount} 次尝试失败,准备重试...`)
- // 等待一段时间后重试
- await new Promise(resolve => setTimeout(resolve, 2000))
- }
- }
- }
- // 直接填充AIPPT格式的内容
- const fillAIPPTContent = async (aipptData, outlineTitle) => {
- console.log('开始填充AIPPT内容...')
- try {
- // 构建提示词
- const prompt = `请为以下PPT数据填充内容,要求:
- 1. title字段:15字以内
- 2. text字段:20-50字以内
- 3. 内容要专业、实用、简洁
- 4. 保持JSON格式不变,只填充空的内容
- 5. 请自由发挥,生成丰富多样的标题和内容
- 6. 重要:必须保持原有的幻灯片数量和结构,不能删除或合并任何幻灯片
- 7. 对于每个content类型的幻灯片,确保items数组有4个元素
- PPT主题:${outlineTitle}
- PPT数据:${JSON.stringify(aipptData, null, 2)}
- 请直接返回完整的JSON数据,不要添加任何说明文字。确保所有空的内容都被填充,并且保持原有的幻灯片数量。`
- console.log('发送AIPPT填充请求...')
- const response = await apis.reProduceSingleQuestion({ message: prompt })
- console.log('API响应:', response)
- // 解析AI返回的JSON数据
- let aiResponse = null
- if (response && response.data) {
- if (response.data && typeof response.data === 'object' && response.data.reply) {
- aiResponse = response.data.reply
- } else if (response.data && typeof response.data === 'string') {
- aiResponse = response.data
- } else {
- aiResponse = JSON.stringify(response.data)
- }
- } else if (response && response.message) {
- aiResponse = response.message
- } else if (response && response.content) {
- aiResponse = response.content
- } else if (response && response.reply) {
- aiResponse = response.reply
- } else if (response && typeof response === 'string') {
- aiResponse = response
- } else if (response && typeof response === 'object') {
- aiResponse = JSON.stringify(response)
- }
- if (aiResponse && typeof aiResponse === 'string' && aiResponse.trim() !== '') {
- try {
- // 尝试解析JSON
- const parsedData = JSON.parse(aiResponse.trim())
- console.log('AI返回的AIPPT数据解析成功:', parsedData)
- // 验证数据结构
- if (Array.isArray(parsedData)) {
- console.log('AIPPT内容填充完成,原始数量:', aipptData.length, '填充后数量:', parsedData.length)
- // 检查数量是否一致
- if (parsedData.length !== aipptData.length) {
- console.warn('警告:AI返回的幻灯片数量与原始数量不一致!')
- console.warn('原始数量:', aipptData.length, 'AI返回数量:', parsedData.length)
- }
- return parsedData
- } else {
- throw new Error('AI返回的数据不是数组格式')
- }
- } catch (parseError) {
- console.error('解析AI返回的JSON失败:', parseError)
- console.log('AI返回的原始内容:', aiResponse)
- throw new Error('AI返回的数据格式不正确')
- }
- } else {
- throw new Error('AI返回内容为空或格式不正确')
- }
- } catch (error) {
- console.error('填充AIPPT内容失败:', error)
- throw new Error('填充AIPPT内容失败: ' + error.message)
- }
- }
- // 转换字符串数组为大纲对象格式
- const convertStringArrayToOutline = (stringArray) => {
- const outline = []
- let currentChapter = null
- let currentSection = null
- stringArray.forEach((item, index) => {
- if (item.startsWith('章节:')) {
- // 创建新章节
- currentChapter = {
- title: item.replace('章节: ', ''),
- content: '',
- sections: []
- }
- outline.push(currentChapter)
- currentSection = null
- } else if (item.startsWith('小节:')) {
- // 创建新小节
- if (currentChapter) {
- currentSection = {
- title: item.replace('小节: ', ''),
- content: '',
- subsections: []
- }
- currentChapter.sections.push(currentSection)
- }
- } else if (item.startsWith('子小节:')) {
- // 创建新子小节
- if (currentSection) {
- const subsection = {
- title: item.replace('子小节: ', ''),
- content: ''
- }
- currentSection.subsections.push(subsection)
- }
- }
- })
- // 确保每个小节至少有4个子小节
- outline.forEach(chapter => {
- chapter.sections.forEach(section => {
- while (section.subsections.length < 4) {
- // 让AI自由发挥,不预设标题模式
- const additionalSubsections = [
- {
- title: '',
- content: ''
- },
- {
- title: '',
- content: ''
- },
- {
- title: '',
- content: ''
- },
- {
- title: '',
- content: ''
- }
- ]
- const index = section.subsections.length
- if (index < additionalSubsections.length) {
- section.subsections.push(additionalSubsections[index])
- }
- }
- })
- })
- return outline
- }
- // 检查大纲数据是否有变化
- const hasOutlineDataChanged = () => {
- if (!lastSavedOutlineData.value) return true
- const currentData = {
- title: outlineTitle.value,
- stats: outlineStats.value,
- chapters: outlineData.value
- }
- return JSON.stringify(currentData) !== JSON.stringify(lastSavedOutlineData.value)
- }
- // 检查PPT数据是否有变化
- const hasPPTDataChanged = () => {
- if (!lastSavedPPTData.value) return true
- return JSON.stringify(generatedPPT.value) !== JSON.stringify(lastSavedPPTData.value)
- }
- // 获取章节的显示标题(加上第X章前缀)
- const getDisplayChapterTitle = (chapterTitle, chapterIndex) => {
- // 如果标题已经包含"第X章",直接返回
- if (chapterTitle.match(/^第[一二三四五六七八九十\d]+章/)) {
- return chapterTitle
- }
- // 否则加上"第X章"前缀,使用中文数字
- const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
- const chapterNumber = chapterIndex < chineseNumbers.length ? chineseNumbers[chapterIndex] : (chapterIndex + 1).toString()
- return `第${chapterNumber}章 ${chapterTitle}`
- }
- // 获取章节的干净标题(去掉第X章前缀,用于编辑)
- const getCleanChapterTitle = (chapterTitle) => {
- // 去掉章节标题中的"第X章"前缀
- return chapterTitle.replace(/^第[一二三四五六七八九十\d]+章\s*/, '')
- }
- // 将大纲数据转换为markdown格式
- const convertOutlineToMarkdown = (chapters, title) => {
- let markdown = `# ${title}\n\n`
- chapters.forEach((chapter, index) => {
- // 直接使用原始标题,不要修改
- markdown += `## ${chapter.title}\n\n`
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- markdown += `### ${section.title}\n\n`
- // 处理子小节
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- // 四级标题添加 #### 前缀
- markdown += `#### ${subsection.title}\n\n`
- // 处理具体内容要点(-开头)
- if (subsection.subsubsections && subsection.subsubsections.length > 0) {
- subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
- // 具体内容要点添加 - 前缀
- markdown += `- ${subsubsection.title}\n\n`
- // 添加具体内容要点(-开头)下的正文内容
- if (subsubsection.content && subsubsection.content.trim()) {
- markdown += `${subsubsection.content}\n\n`
- }
- })
- }
- })
- }
- })
- }
- markdown += '\n'
- })
- return markdown
- }
- // 直接保存大纲到后端(不依赖全局状态,用于生成大纲时)
- const saveOutlineToBackendDirectly = async (conversationId, chapters, title, stats) => {
- try {
- if (isSaving.value) {
- console.log('正在保存中,跳过重复保存请求')
- return false
- }
- if (!conversationId || !chapters) {
- console.log('缺少conversationId或大纲数据,跳过后端保存')
- return false
- }
- isSaving.value = true
- // 将大纲数据转换为markdown格式
- const markdownContent = convertOutlineToMarkdown(chapters, title)
- console.log('直接保存大纲到后端:', {
- ai_conversation_id: conversationId,
- markdownContent: markdownContent
- })
- const response = await apis.savePPTOutline({
- ai_conversation_id: conversationId,
- ppt_content: markdownContent
- })
- if (response.statusCode === 200) {
- console.log('大纲已直接保存到后端服务器,conversation_id:', conversationId)
- return true
- } else {
- console.error('直接保存到后端服务器失败:', response)
- return false
- }
- } catch (error) {
- console.error('直接保存大纲失败:', error)
- return false
- } finally {
- isSaving.value = false
- }
- }
- // 保存大纲到后端(用户编辑时才调用)
- const saveOutlineToBackend = async (forceSave = false) => {
- try {
- if (isSaving.value) {
- console.log('正在保存中,跳过重复保存请求')
- return false
- }
- if (!ai_conversation_id.value || !outlineData.value) {
- console.log('缺少ai_conversation_id或大纲数据,跳过后端保存')
- return false
- }
- // 检查数据是否有变化
- if (!forceSave && !hasOutlineDataChanged()) {
- console.log('大纲数据未发生变化,跳过保存')
- return false
- }
- isSaving.value = true
- // 将大纲数据转换为markdown格式
- const markdownContent = convertOutlineToMarkdown(outlineData.value, outlineTitle.value)
- console.log('准备保存大纲到后端:', {
- ai_conversation_id: ai_conversation_id.value,
- markdownContent: markdownContent
- })
- const response = await apis.savePPTOutline({
- ai_conversation_id: ai_conversation_id.value,
- ppt_content: markdownContent
- })
- if (response.statusCode === 200) {
- console.log('大纲已保存到后端服务器,conversation_id:', ai_conversation_id.value)
- // 更新上次保存的数据
- lastSavedOutlineData.value = {
- title: outlineTitle.value,
- stats: outlineStats.value,
- chapters: JSON.parse(JSON.stringify(outlineData.value)) // 深拷贝
- }
- return true
- } else {
- console.error('保存到后端服务器失败:', response)
- return false
- }
- } catch (error) {
- console.error('保存大纲失败:', error)
- return false
- } finally {
- isSaving.value = false
- }
- }
- // 步骤三相关方法
- const goToStep3 = () => {
- currentStep.value = 'step3'
- console.log('进入步骤三:PPT模板选择')
- // 确保处于模板预览模式
- showDownloadOptions.value = false
- selectedDownloadOption.value = 0
- // 清理PPT预览相关状态(保留generatedPPT数据)
- currentPPTSlideIndex.value = 0
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- zoom.value = 1
- selectedImageIndex.value = null
- // 加载模板预览数据
- loadLocalPPT()
- updateSlideThumbnails()
- // 重置当前幻灯片索引
- currentSlideIndex.value = 0
- console.log('模板预览已加载,共', slideImages.value.length, '张图片')
- }
- // 显示复制大纲提示
- const showCopyOutlineToast = () => {
- showCopyToast.value = true
- setTimeout(() => {
- showCopyToast.value = false
- }, 1000)
- }
- // WPS AI PPT集成方法
- const openWPS = () => {
- try {
- // 检查是否有大纲数据
- if (!outlineData.value || outlineData.value.length === 0) {
- ElMessage.warning('请先生成大纲后再使用WPS AI PPT')
- return
- }
- // 默认调用复制大纲功能
- copyEntireOutline()
-
- // 显示复制大纲提示
- showCopyOutlineToast()
- // 重置到WPS AI PPT页面
- selectedWPSUrl.value = 'https://aippt.wps.cn/aippt/'
- // 显示WPS AI PPT集成页面
- showWPSModal.value = true
- ElMessage.success('正在加载WPS AI PPT...')
- console.log('WPS AI PPT集成页面已打开')
- } catch (error) {
- console.error('WPS AI PPT加载失败:', error)
- ElMessage.error('WPS AI PPT加载失败,请稍后重试')
- }
- }
- // 将大纲转换为文本格式
- const convertOutlineToText = () => {
- if (!outlineData.value || outlineData.value.length === 0) {
- return '暂无大纲内容'
- }
- let text = ''
- outlineData.value.forEach((chapter, chapterIndex) => {
- if (chapter && chapter.title) {
- text += `${chapterIndex + 1}. ${chapter.title}\n`
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- if (section && section.title &&
- section.title !== '内容要点' &&
- section.title !== '概述') {
- text += ` ${chapterIndex + 1}.${sectionIndex + 1} ${section.title}\n`
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- if (subsection && subsection.title &&
- subsection.title !== '内容要点' &&
- subsection.title !== '概述' &&
- subsection.title !== '内容详情') {
- text += ` ${chapterIndex + 1}.${sectionIndex + 1}.${subsectionIndex + 1} ${subsection.title}\n`
- }
- })
- }
- }
- })
- }
- text += '\n'
- }
- })
- return text
- }
- // 复制大纲到剪贴板
- const copyOutlineToClipboard = () => {
- const outlineText = convertOutlineToText()
- navigator.clipboard.writeText(outlineText).then(() => {
- ElMessage.success('大纲已复制到剪贴板!')
- }).catch(() => {
- // 降级方案
- const textArea = document.createElement('textarea')
- textArea.value = outlineText
- document.body.appendChild(textArea)
- textArea.select()
- document.execCommand('copy')
- document.body.removeChild(textArea)
- ElMessage.success('大纲已复制到剪贴板!')
- })
- }
- // 更新iframe源地址
- const updateIframeSrc = () => {
- console.log('切换到WPS页面:', selectedWPSUrl.value)
- ElMessage.info(`正在切换到${selectedWPSUrl.value}`)
- }
- // 刷新iframe
- const refreshIframe = () => {
- console.log('刷新WPS页面')
- ElMessage.info('正在刷新页面...')
- // 通过改变src来刷新iframe
- const currentSrc = selectedWPSUrl.value
- selectedWPSUrl.value = ''
- setTimeout(() => {
- selectedWPSUrl.value = currentSrc
- }, 100)
- }
- // 处理iframe错误
- const handleIframeError = () => {
- console.log('iframe加载失败')
- ElMessage.warning('页面加载失败,请尝试其他选项或检查网络连接')
- }
- // ==================== 下载监听插件功能 ====================
- // 切换下载监听状态
- const toggleDownloadListener = () => {
- if (isDownloadListenerActive.value) {
- startDownloadListener()
- ElMessage.success('下载监听插件已启用')
- } else {
- stopDownloadListener()
- ElMessage.info('下载监听插件已关闭')
- }
- }
- // 启动下载监听
- const startDownloadListener = () => {
- console.log('启动下载监听插件...')
- // 方法1: 监听浏览器的下载事件
- setupBrowserDownloadListener()
- // 方法2: 监听iframe的postMessage事件
- setupIframeMessageListener()
- // 方法3: 定期检查下载文件夹变化(如果可能)
- setupDownloadFolderMonitor()
- // 方法4: 监听网络请求中的下载链接
- setupNetworkRequestListener()
- }
- // 停止下载监听
- const stopDownloadListener = () => {
- console.log('停止下载监听插件...')
- // 移除所有事件监听器
- window.removeEventListener('beforeunload', handleBeforeUnload)
- window.removeEventListener('message', handleIframeMessage)
- // 清除定时器
- if (downloadMonitorTimer.value) {
- clearInterval(downloadMonitorTimer.value)
- downloadMonitorTimer.value = null
- }
- }
- // 方法1: 监听浏览器下载事件
- const setupBrowserDownloadListener = () => {
- // 监听页面卸载前的下载事件
- window.addEventListener('beforeunload', handleBeforeUnload)
- // 监听点击事件,检测可能的下载链接
- document.addEventListener('click', handleClickEvent, true)
- }
- // 方法2: 监听iframe的postMessage事件
- const setupIframeMessageListener = () => {
- window.addEventListener('message', handleIframeMessage)
- }
- // 方法3: 定期检查下载状态
- const downloadMonitorTimer = ref(null)
- const setupDownloadFolderMonitor = () => {
- // 由于浏览器安全限制,无法直接访问下载文件夹
- // 这里使用模拟的方式,定期检查是否有新的下载
- // downloadMonitorTimer.value = setInterval(() => {
- // checkForNewDownloads()
- // }, 2000) // 每2秒检查一次
- }
- // 方法4: 监听网络请求
- const setupNetworkRequestListener = () => {
- // 拦截fetch请求
- const originalFetch = window.fetch
- window.fetch = async (...args) => {
- const response = await originalFetch(...args)
- // 检查响应头中是否包含下载相关的信息
- const contentType = response.headers.get('content-type')
- const contentDisposition = response.headers.get('content-disposition')
- if (contentType && (
- contentType.includes('application/vnd.openxmlformats-officedocument.presentationml.presentation') ||
- contentType.includes('application/vnd.ms-powerpoint') ||
- contentType.includes('application/pdf')
- )) {
- console.log('检测到可能的PPT下载请求:', args[0])
- handleDetectedDownload(args[0], contentType, contentDisposition)
- }
- return response
- }
- }
- // 处理页面卸载前的下载检测
- const handleBeforeUnload = (event) => {
- console.log('页面即将卸载,检查是否有下载活动')
- // 这里可以记录一些状态信息
- }
- // 处理iframe消息
- const handleIframeMessage = (event) => {
- console.log('收到iframe消息 - 来源:', event.origin, '数据:', event.data)
- // 检查消息来源 - 放宽限制,支持多个WPS域名
- const allowedOrigins = [
- 'https://aippt.wps.cn',
- 'https://web.wps.cn',
- 'https://www.kdocs.cn',
- 'https://docs.wps.cn'
- ]
- if (!allowedOrigins.includes(event.origin)) {
- console.log('消息来源不在允许列表中:', event.origin)
- return
- }
- console.log('消息来源验证通过,处理消息:', event.data)
- console.log('消息类型:', JSON.parse(event.data))
- // 处理WPS AI PPT创建完成的消息
- let messageData = JSON.parse(event.data)
- if (messageData.type === 'AIPPT_BEFORE_CREATEPPT' && messageData.data && messageData.data.fileInfo) {
- const fileInfo = messageData.data.fileInfo
- console.log('文件信息:', fileInfo)
- currentPPTInfo.value = fileInfo
- showOpenPPTButton.value = true
- // 延迟3秒显示第二个遮罩
- setTimeout(() => {
- showSecondMask.value = true
- console.log('延迟3秒后显示第二个遮罩')
- }, 3000)
- }
- }
- // 处理点击事件,检测下载链接
- const handleClickEvent = (event) => {
- const target = event.target
- const link = target.closest('a')
- if (link && link.href) {
- const href = link.href.toLowerCase()
- if (href.includes('.pptx') || href.includes('.ppt') || href.includes('download')) {
- console.log('检测到可能的下载链接:', link.href)
- handleDetectedDownload(link.href, 'application/vnd.openxmlformats-officedocument.presentationml.presentation')
- }
- }
- }
- // 检查新下载
- const checkForNewDownloads = () => {
- // 由于浏览器安全限制,这里使用模拟的方式
- // 在实际应用中,可能需要通过其他方式来实现
- console.log('检查新下载...')
- }
- // 处理检测到的下载
- const handleDetectedDownload = (url, contentType, filename) => {
- const now = new Date()
- const downloadInfo = {
- id: Date.now(),
- url: url,
- contentType: contentType,
- filename: filename || `PPT_${now.getTime()}.pptx`,
- timestamp: now.toLocaleString(),
- source: 'WPS AI PPT'
- }
- console.log('检测到下载:', downloadInfo)
- // 添加到下载历史
- downloadHistory.value.unshift(downloadInfo)
- // 限制历史记录数量
- if (downloadHistory.value.length > 10) {
- downloadHistory.value = downloadHistory.value.slice(0, 10)
- }
- // 更新最后下载时间
- lastDownloadTime.value = now
- // 显示通知
- ElMessage.success(`检测到PPT下载: ${downloadInfo.filename}`)
- // 自动下载到本地
- autoDownloadToLocal(downloadInfo)
- }
- // 自动下载到本地
- const autoDownloadToLocal = async (downloadInfo) => {
- try {
- console.log('开始自动下载到本地:', downloadInfo)
- // 创建下载链接
- const link = document.createElement('a')
- link.href = downloadInfo.url
- link.download = downloadInfo.filename
- link.style.display = 'none'
- // 添加到页面并触发下载
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- ElMessage.success(`已自动下载: ${downloadInfo.filename}`)
- } catch (error) {
- console.error('自动下载失败:', error)
- ElMessage.error('自动下载失败,请手动下载')
- }
- }
- // 下载WPS文件
- const downloadWPSFile = async (fileInfo) => {
- try {
- console.log('开始下载WPS文件:', fileInfo)
- // 构建下载URL - 使用link_url作为下载地址
- const downloadUrl = fileInfo.link_url
- const fileName = fileInfo.file_name || `WPS_PPT_${Date.now()}.pptx`
- // 创建下载信息记录
- const downloadInfo = {
- id: Date.now(),
- url: downloadUrl,
- contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- filename: fileName,
- timestamp: new Date().toLocaleString(),
- source: 'WPS AI PPT',
- fileId: fileInfo.id,
- linkId: fileInfo.link_id
- }
- // 添加到下载历史
- downloadHistory.value.unshift(downloadInfo)
- // 限制历史记录数量
- if (downloadHistory.value.length > 10) {
- downloadHistory.value = downloadHistory.value.slice(0, 10)
- }
- // 更新最后下载时间
- lastDownloadTime.value = new Date()
- // 显示通知
- ElMessage.success(`检测到WPS PPT创建完成,正在下载: ${fileName}`)
- // 尝试多种下载方式
- await tryMultipleDownloadMethods(downloadUrl, fileName, downloadInfo)
- } catch (error) {
- console.error('WPS文件下载失败:', error)
- ElMessage.error('WPS文件下载失败,请手动下载')
- }
- }
- // 尝试多种下载方式
- const tryMultipleDownloadMethods = async (downloadUrl, fileName, downloadInfo) => {
- const methods = [
- () => downloadViaDirectLink(downloadUrl, fileName),
- () => downloadViaProxy(downloadUrl, fileName),
- () => downloadViaNewWindow(downloadUrl, fileName),
- () => downloadViaIframe(downloadUrl, fileName)
- ]
- for (let i = 0; i < methods.length; i++) {
- try {
- console.log(`尝试下载方式 ${i + 1}:`, methods[i].name)
- await methods[i]()
- ElMessage.success(`已自动下载WPS文件: ${fileName}`)
- console.log('WPS文件下载完成:', downloadInfo)
- return // 成功则退出
- } catch (error) {
- console.warn(`下载方式 ${i + 1} 失败:`, error)
- if (i === methods.length - 1) {
- throw error // 所有方式都失败
- }
- }
- }
- }
- // 方式1: 直接链接下载
- const downloadViaDirectLink = (downloadUrl, fileName) => {
- return new Promise((resolve, reject) => {
- const link = document.createElement('a')
- link.href = downloadUrl
- link.download = fileName
- link.style.display = 'none'
- link.onload = () => resolve()
- link.onerror = () => reject(new Error('直接链接下载失败'))
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- // 给一点时间让下载开始
- setTimeout(resolve, 1000)
- })
- }
- // 方式2: 通过代理下载
- const downloadViaProxy = async (downloadUrl, fileName) => {
- try {
- // 尝试通过后端代理下载
- const response = await fetch('/api/proxy-download', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- url: downloadUrl,
- filename: fileName
- })
- })
- if (!response.ok) {
- throw new Error('代理下载失败')
- }
- const blob = await response.blob()
- const url = URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = fileName
- link.style.display = 'none'
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- URL.revokeObjectURL(url)
- } catch (error) {
- throw new Error('代理下载失败: ' + error.message)
- }
- }
- // 方式3: 新窗口下载
- const downloadViaNewWindow = (downloadUrl, fileName) => {
- return new Promise((resolve, reject) => {
- const newWindow = window.open(downloadUrl, '_blank')
- if (!newWindow) {
- reject(new Error('无法打开新窗口'))
- return
- }
- // 监听窗口关闭
- const checkClosed = setInterval(() => {
- if (newWindow.closed) {
- clearInterval(checkClosed)
- resolve()
- }
- }, 1000)
- // 5秒后超时
- setTimeout(() => {
- clearInterval(checkClosed)
- resolve()
- }, 5000)
- })
- }
- // 方式4: iframe下载
- const downloadViaIframe = (downloadUrl, fileName) => {
- return new Promise((resolve, reject) => {
- const iframe = document.createElement('iframe')
- iframe.style.display = 'none'
- iframe.src = downloadUrl
- iframe.onload = () => {
- setTimeout(() => {
- document.body.removeChild(iframe)
- resolve()
- }, 2000)
- }
- iframe.onerror = () => {
- document.body.removeChild(iframe)
- reject(new Error('iframe下载失败'))
- }
- document.body.appendChild(iframe)
- })
- }
- // ==================== PPT查看器功能 ====================
- // 打开PPT查看器
- const openPPTViewer = () => {
- if (!currentPPTInfo.value) {
- ElMessage.error('没有可查看的PPT信息')
- return
- }
- try {
- console.log('打开PPT查看器:', currentPPTInfo.value)
- // 构建PPT查看器URL
- // 使用WPS的在线查看器
- const viewerUrl = `https://www.kdocs.cn/l/${currentPPTInfo.value.linkId}`
- // 直接在当前WPS弹窗中切换iframe内容
- selectedWPSUrl.value = viewerUrl
- ElMessage.success(`正在打开PPT: ${currentPPTInfo.value.file_name}`)
- } catch (error) {
- console.error('打开PPT查看器失败:', error)
- ElMessage.error('打开PPT查看器失败,请重试')
- }
- }
- // 下载当前PPT
- const downloadCurrentPPT = () => {
- if (!currentPPTInfo.value) {
- ElMessage.error('没有可下载的PPT信息')
- return
- }
- try {
- console.log('下载当前PPT:', currentPPTInfo.value)
- // 创建下载信息
- const downloadInfo = {
- id: Date.now(),
- url: currentPPTInfo.value.link_url,
- contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- filename: currentPPTInfo.value.file_name,
- timestamp: new Date().toLocaleString(),
- source: 'PPT查看器',
- fileId: currentPPTInfo.value.fileId,
- linkId: currentPPTInfo.value.linkId
- }
- // 添加到下载历史
- downloadHistory.value.unshift(downloadInfo)
- // 限制历史记录数量
- if (downloadHistory.value.length > 10) {
- downloadHistory.value = downloadHistory.value.slice(0, 10)
- }
- // 尝试下载
- downloadViaDirectLink(currentPPTInfo.value.link_url, currentPPTInfo.value.file_name)
- .then(() => {
- ElMessage.success(`已下载PPT: ${currentPPTInfo.value.file_name}`)
- })
- .catch((error) => {
- console.error('下载失败:', error)
- ElMessage.error('下载失败,请手动下载')
- })
- } catch (error) {
- console.error('下载当前PPT失败:', error)
- ElMessage.error('下载失败,请重试')
- }
- }
- // 处理PPT iframe错误
- const handlePPTIframeError = () => {
- console.log('PPT iframe加载失败')
- ElMessage.warning('PPT加载失败,请检查网络连接或尝试刷新')
- }
- // 测试PPT检测功能
- const testPPTDetection = () => {
- console.log('开始测试PPT检测功能...')
- // 模拟您提供的消息数据
- const testMessage = {
- type: "AIPPT_BEFORE_CREATEPPT",
- data: {
- infoId: "0d51bbc5f7cde18652340593cccc1305",
- fileInfo: {
- id: 450721083297,
- link_id: "ckNZFi6F3Gtv",
- link_url: "https://www.kdocs.cn/l/ckNZFi6F3Gtv",
- cache: 1,
- branch_id: 0,
- file_name: "课堂互动小游戏设计PPT.pptx"
- }
- }
- }
- console.log('模拟消息数据:', testMessage)
- // 直接调用消息处理函数
- const mockEvent = {
- origin: 'https://aippt.wps.cn',
- data: testMessage
- }
- handleIframeMessage(mockEvent)
- ElMessage.success('测试消息已发送,请检查是否出现"打开PPT"按钮')
- }
- // 返回WPS AI PPT
- const backToWPSAI = () => {
- try {
- console.log('返回WPS AI PPT')
- // 切换回WPS AI PPT页面
- selectedWPSUrl.value = 'https://aippt.wps.cn/aippt/'
- ElMessage.success('已返回WPS AI PPT页面')
- } catch (error) {
- console.error('返回WPS AI PPT失败:', error)
- ElMessage.error('返回失败,请重试')
- }
- }
- // 打开测试PPT
- const openTestPPT = () => {
- try {
- console.log('打开测试PPT')
- // 直接打开WPS弹窗并显示测试PPT
- showWPSModal.value = true
- selectedWPSUrl.value = currentPPTInfo.value.link_url
- ElMessage.success('正在打开测试PPT...')
- } catch (error) {
- console.error('打开测试PPT失败:', error)
- ElMessage.error('打开测试PPT失败,请重试')
- }
- }
- const goToStep2 = () => {
- console.log('返回步骤二:编辑大纲')
- // 检查大纲数据是否已加载,如果没有则重新加载
- if (!outlineData.value || outlineData.value.length === 0) {
- console.log('大纲数据未加载,重新加载历史记录数据')
- // 获取当前历史记录
- const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
- if (currentHistoryItem) {
- // 重新加载大纲数据
- if (currentHistoryItem.rawData && currentHistoryItem.rawData.ppt_outline && currentHistoryItem.rawData.ppt_outline.trim()) {
- try {
- const savedOutlineData = JSON.parse(currentHistoryItem.rawData.ppt_outline)
- outlineData.value = savedOutlineData.chapters
- outlineTitle.value = savedOutlineData.title || '安全培训大纲'
- outlineStats.value = calculateOutlineStats(savedOutlineData.chapters)
- outlineId.value = currentHistoryItem.id
- // 更新保存状态
- lastSavedOutlineData.value = {
- title: outlineTitle.value,
- stats: outlineStats.value,
- chapters: JSON.parse(JSON.stringify(savedOutlineData.chapters))
- }
- console.log('重新加载大纲数据成功:', outlineTitle.value)
- } catch (error) {
- console.error('重新加载大纲数据失败:', error)
- }
- } else {
- console.log('当前历史记录没有大纲数据')
- }
- }
- } else {
- console.log('大纲数据已存在,直接跳转')
- }
- currentStep.value = 'step2'
- }
- const prevSlide = () => {
- if (currentSlideIndex.value > 0) {
- currentSlideIndex.value--
- } else {
- currentSlideIndex.value = slideImages.value.length - 1
- }
- }
- const nextSlide = () => {
- if (currentSlideIndex.value < slideImages.value.length - 1) {
- currentSlideIndex.value++
- } else {
- currentSlideIndex.value = 0
- }
- }
- const goToSlide = (index) => {
- currentSlideIndex.value = index
- // 如果在编辑模式下,更新当前编辑的幻灯片
- if (showDownloadOptions.value && pptSlides.value[index]) {
- currentEditingSlide.value = { ...pptSlides.value[index] }
- }
- }
- const goToPPTSlide = (index) => {
- currentPPTSlideIndex.value = index
- console.log('切换到PPT幻灯片:', index)
- // 确保缩略图条滚动到正确位置
- nextTick(() => {
- const thumbnailStripEl = thumbnailStrip.value
- if (thumbnailStripEl) {
- // 计算需要滚动的位置,确保当前缩略图可见
- const thumbnailWidth = 135 + 11 // 缩略图宽度 + 间距
- const scrollPosition = index * thumbnailWidth - thumbnailStripEl.clientWidth / 2 + thumbnailWidth / 2
- thumbnailStripEl.scrollLeft = Math.max(0, scrollPosition)
- console.log(`缩略图条已滚动到第${index + 1}页,位置:${scrollPosition}`)
- }
- })
- }
- // 缩略图滚轮滑动处理
- const handleThumbnailWheel = (event) => {
- event.preventDefault() // 阻止默认滚动行为
- const thumbnailStrip = event.currentTarget
- const scrollAmount = event.deltaY > 0 ? 200 : -200 // 向下滚动时向右,向上滚动时向左
- thumbnailStrip.scrollLeft += scrollAmount
- }
- // PPT编辑相关方法
- const initializePPTEditor = () => {
- // 使用你的本地1.pptx文件内容(2页)
- pptSlides.value = [
- {
- title: '第1页标题',
- content: '第1页内容',
- type: 'cover'
- },
- {
- title: '第2页标题',
- content: '第2页内容',
- type: 'content'
- }
- ]
- // 设置当前编辑的幻灯片
- if (pptSlides.value.length > 0) {
- currentSlideIndex.value = 0
- currentEditingSlide.value = { ...pptSlides.value[0] }
- }
- // 更新缩略图
- updateSlideThumbnails()
- console.log('PPT编辑器初始化完成,页数:', pptSlides.value.length)
- }
- const loadLocalPPT = () => {
- console.log('开始加载本地PPT数据')
- // 优先检查是否有生成的PPT数据
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- console.log('使用已生成的PPT数据:', generatedPPT.value.length, '张幻灯片')
- // 将generatedPPT数据转换为pptSlides格式
- pptSlides.value = generatedPPT.value.map((slide, index) => ({
- title: slide.data?.title || `第${index + 1}页`,
- content: slide.data?.text || slide.data?.items?.map(item => item.title).join('\n') || '内容',
- type: slide.type
- }))
- // 更新缩略图
- updateSlideThumbnails()
- // 重置当前幻灯片索引
- currentSlideIndex.value = 0
- currentEditingSlide.value = { ...pptSlides.value[0] }
- ElMessage.success('生成的PPT数据加载成功')
- console.log('加载的PPT页数:', pptSlides.value.length)
- return
- }
- // 如果没有生成的PPT数据,加载默认的template5内容
- console.log('加载默认template5内容')
- const localPPTData = [
- {
- title: '第1页标题',
- content: '第1页内容',
- type: 'cover'
- },
- {
- title: '第2页标题',
- content: '第2页内容',
- type: 'content'
- },
- {
- title: '第3页标题',
- content: '第3页内容',
- type: 'content'
- },
- {
- title: '第4页标题',
- content: '第4页内容',
- type: 'content'
- },
- {
- title: '第5页标题',
- content: '第5页内容',
- type: 'content'
- }
- ]
- // 更新PPT数据
- pptSlides.value = localPPTData
- // 更新缩略图,显示5页
- updateSlideThumbnails()
- // 重置当前幻灯片索引
- currentSlideIndex.value = 0
- currentEditingSlide.value = { ...localPPTData[0] }
- // ElMessage.success('默认PPT文件加载成功')
- console.log('加载的PPT页数:', localPPTData.length)
- }
- // 从大纲数据生成PPT幻灯片
- const generateSlidesFromOutline = () => {
- const slides = []
- if (outlineData.value) {
- // 封面幻灯片
- slides.push({
- title: outlineTitle.value || '安全培训演示文稿',
- content: '基于AI生成的培训大纲',
- type: 'cover'
- })
- // 目录幻灯片
- const tocContent = outlineData.value.map((chapter, index) =>
- `${index + 1}. ${chapter.title}`
- ).join('\n')
- slides.push({
- title: '目录',
- content: tocContent,
- type: 'toc'
- })
- // 章节内容幻灯片
- outlineData.value.forEach((chapter, chapterIndex) => {
- // 章节标题幻灯片
- slides.push({
- title: chapter.title,
- content: `第${chapterIndex + 1}章`,
- type: 'chapter'
- })
- // 小节内容幻灯片
- chapter.sections.forEach((section, sectionIndex) => {
- if (section.subsections && section.subsections.length > 0) {
- const content = section.subsections.map(sub => sub.title).join('\n')
- slides.push({
- title: section.title,
- content: content,
- type: 'content'
- })
- } else {
- slides.push({
- title: section.title,
- content: '详细内容待补充',
- type: 'content'
- })
- }
- })
- })
- // 总结幻灯片
- slides.push({
- title: '总结与展望',
- content: '培训要点回顾\n安全知识巩固\n持续改进建议',
- type: 'summary'
- })
- }
- return slides
- }
- const updateSlideContent = (event) => {
- const target = event.target
- if (target.classList.contains('slide-title')) {
- currentEditingSlide.value.title = target.textContent
- } else if (target.classList.contains('slide-body')) {
- currentEditingSlide.value.content = target.textContent
- }
- // 更新PPT数据
- if (pptSlides.value[currentSlideIndex.value]) {
- pptSlides.value[currentSlideIndex.value] = { ...currentEditingSlide.value }
- }
- // 实时更新缩略图标题
- updateSlideThumbnails()
- }
- const addNewSlide = () => {
- // 限制最大页数为5页
- if (pptSlides.value.length >= 5) {
- ElMessage.warning('最多只能添加5页')
- return
- }
- const newSlide = {
- title: '新页面',
- content: '点击编辑内容',
- type: 'content'
- }
- pptSlides.value.push(newSlide)
- currentSlideIndex.value = pptSlides.value.length - 1
- currentEditingSlide.value = { ...newSlide }
- updateSlideThumbnails()
- ElMessage.success('新增页面成功')
- }
- const deleteCurrentSlide = async () => {
- if (pptSlides.value.length <= 1) {
- ElMessage.warning('至少保留一个页面')
- return
- }
- try {
- await ElMessageBox.confirm('确定要删除当前页面吗?', '确认删除', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
- } catch {
- return // 用户取消删除
- }
- pptSlides.value.splice(currentSlideIndex.value, 1)
- if (currentSlideIndex.value >= pptSlides.value.length) {
- currentSlideIndex.value = pptSlides.value.length - 1
- }
- currentEditingSlide.value = { ...pptSlides.value[currentSlideIndex.value] }
- updateSlideThumbnails()
- ElMessage.success('删除页面成功')
- }
- const savePPT = () => {
- // 保存PPT数据
- localStorage.setItem('safety-training-ppt', JSON.stringify(pptSlides.value))
- ElMessage.success('PPT保存成功')
- console.log('PPT数据已保存:', pptSlides.value)
- }
- // 下载状态
- const isDownloading = ref(false)
- const exportPPTX = async () => {
- if (isDownloading.value) {
- return // 防止重复点击
- }
- try {
- isDownloading.value = true
- isProcessing.value = true
- console.log('exportPPTX: 设置 isProcessing = true')
- // 根据选中的下载选项执行不同的操作
- switch (selectedDownloadOption.value) {
- case 0: // PowerPoint (PPTX)
- await exportAsPPTX()
- break
- case 1: // 考试工坊
- await generateExamQuestions()
- break
- case 2: // 培训讲义文档
- await generateTrainingDocument()
- break
- default:
- ElMessage.warning('未知的下载选项')
- }
- } catch (error) {
- console.error('导出失败:', error)
- ElMessage.error('导出失败,请重试')
- } finally {
- isDownloading.value = false
- isProcessing.value = false
- console.log('exportPPTX: 重置 isProcessing = false')
- }
- }
- // 生成考试题目
- const generateExamQuestions = async () => {
- try {
- console.log('开始生成考试题目...')
- // 设置loading状态
- isGeneratingExam.value = true
- isProcessing.value = true
- // 构建提示词
- const prompt = `请基于以下安全培训内容生成完整的考试题目:
- 培训主题:${outlineTitle.value || '安全培训'}
- 培训内容:
- ${generatedPPT.value.map((slide, index) => {
- const content = slide.elements?.map(element => {
- if (element.type === 'text') {
- return element.content.replace(/<[^>]*>/g, '') // 移除HTML标签
- }
- return ''
- }).filter(text => text.trim()).join(' ')
- return `第${index + 1}页:${content}`
- }).join('\n')}
- 请严格按照以下格式生成考试题目:
- 一、单选题(每题4分,共60分)
- 1. 题目内容
- A. 选项A
- B. 选项B
- C. 选项C
- D. 选项D
- 正确答案:X
- 解析:详细解析内容
- 2. 题目内容
- A. 选项A
- B. 选项B
- C. 选项C
- D. 选项D
- 正确答案:X
- 解析:详细解析内容
- 二、多选题(每题4分,共20分)
- 1. 题目内容
- A. 选项A
- B. 选项B
- C. 选项C
- D. 选项D
- 正确答案:AB
- 解析:详细解析内容
- 三、判断题(每题2分,共20分)
- 1. 题目内容
- 正确答案:正确/错误
- 解析:详细解析内容
- 四、简答题(每题10分,共20分)
- 1. 题目内容
- 答题要点:详细答案内容和评分标准
- 2. 题目内容
- 答题要点:详细答案内容和评分标准
- 3. 题目内容
- 答案:详细答案内容和评分标准
- 重要要求:
- 1. 必须严格按照上述格式输出,不能省略任何内容
- 2. 单选题15道(每题2分,共30分),多选题10道(每题3分,共30分),判断题10道(每题2分,共20分),简答题2道(每题10分,共20分)
- 3. 总分控制在100分,不包含填空题
- 4. 题目要全面覆盖培训内容的主要知识点
- 5. 每道题都要包含正确答案和详细解析
- 6. 简答题的答案必须详细具体,不能写"未设置"或"待补充"
- 7. 所有答案都要具体详细,不能省略或留空
- 8. 严格按照示例格式,每道简答题后面必须跟"答题要点:"开头的详细内容
- 9. 简答题答案必须基于题目内容提供具体的知识点和实际应用示例
- 10. 答案内容要丰富详实,至少包含3-5个要点,每个要点都要有具体说明
- 11. 每道简答题的答题要点必须包含:核心概念解释、关键步骤分析、实际应用举例、注意事项说明
- 12. 答题要点内容要具体可操作,不能是空泛的指导性语言
- 13. 必须为每道简答题提供完整的答题要点,不能留空或写"未设置"`
- // 调用AI接口
- const response = await apis.reProduceSingleQuestion({
- message: prompt
- })
- if (response && response.data) {
- console.log('AI返回的数据:', response.data)
- // 提取AI回复的内容
- const aiContent = response.data.reply || response.data.content || response.data.message || response.data || 'AI生成的内容为空'
- console.log('AI生成的内容:', aiContent)
- // 将AI回复转换为Word文档并下载
- await downloadAsWord(aiContent, `考试题目-${outlineTitle.value || '安全培训'}`)
- ElMessage.success('考试题目生成成功!')
- } else {
- throw new Error('AI生成考试题目失败')
- }
- } catch (error) {
- console.error('生成考试题目失败:', error)
- ElMessage.error('生成考试题目失败: ' + error.message)
- } finally {
- // 重置loading状态
- isGeneratingExam.value = false
- isProcessing.value = false
- }
- }
- // 生成培训讲义文档
- const generateTrainingDocument = async () => {
- try {
- console.log('开始生成培训讲义文档...')
- // 设置loading状态
- isGeneratingTrainingMaterial.value = true
- isProcessing.value = true
- // 构建提示词
- const prompt = `请基于以下安全培训内容生成培训讲义:
- 培训主题:${outlineTitle.value || '安全培训'}
- 培训内容:
- ${generatedPPT.value.map((slide, index) => {
- const content = slide.elements?.map(element => {
- if (element.type === 'text') {
- return element.content.replace(/<[^>]*>/g, '') // 移除HTML标签
- }
- return ''
- }).filter(text => text.trim()).join(' ')
- return `第${index + 1}页:${content}`
- }).join('\n')}
- 要求:
- 1. 生成完整的培训讲义,使用Markdown格式
- 2. 包含以下结构:
- - 封面页(标题、副标题、日期)
- - 目录页
- - 各章节内容(使用# ## ###等标题层级)
- - 要点列表(使用- 或 1. 2. 等)
- - 重要概念(使用**粗体**标记)
- - 注意事项(使用> 引用格式)
- - 总结页
- 3. 内容要详细、专业、易懂
- 4. 适合作为培训教材使用
- 5. 使用标准的Markdown语法
- 请生成完整的培训讲义文档,使用规范的Markdown格式。`
- // 调用AI接口
- const response = await apis.reProduceSingleQuestion({
- message: prompt
- })
- if (response && response.data) {
- console.log('AI返回的数据:', response.data)
- // 提取AI回复的内容
- const aiContent = response.data.reply || response.data.content || response.data.message || response.data || 'AI生成的内容为空'
- // 将AI回复转换为Word文档并下载
- await downloadAsWord(aiContent, `培训讲义-${outlineTitle.value || '安全培训'}`)
- ElMessage.success('培训讲义生成成功!')
- } else {
- throw new Error('AI生成培训讲义失败')
- }
- } catch (error) {
- console.error('生成培训讲义失败:', error)
- ElMessage.error('生成培训讲义失败: ' + error.message)
- } finally {
- // 重置loading状态
- isGeneratingTrainingMaterial.value = false
- isProcessing.value = false
- }
- }
- // 下载为Word文档 - 培训讲义专用(简洁格式)
- const downloadAsWord = async (content, fileName) => {
- try {
- console.log('开始生成Word文档,内容:', content)
- // 确保content是字符串
- const contentStr = String(content)
- console.log('转换后的内容字符串:', contentStr)
- // 清理内容,移除复杂的格式
- let cleanedContent = contentStr
- // 移除HTML标签
- .replace(/<[^>]*>/g, '')
- // 移除颜色相关的文本
- .replace(/颜色[::].*?[,,。]/g, '')
- .replace(/红色|蓝色|绿色|黄色|紫色|橙色|灰色|黑色|白色/g, '')
- .replace(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g, '')
- .replace(/rgb\([^)]*\)|rgba\([^)]*\)/g, '')
- // 移除代码块标记
- .replace(/```[\s\S]*?```/g, '')
- .replace(/`[^`]*`/g, '')
- // 移除引用标记
- .replace(/^>\s*/gm, '')
- // 移除表格标记
- .replace(/\|.*\|/g, '')
- .replace(/^[-:\s|]+$/gm, '')
- // 清理多余的空行
- .replace(/\n\s*\n\s*\n/g, '\n\n')
- // 创建简洁的HTML文档内容
- const wordContent = createSimpleWordContent(cleanedContent, fileName)
- // 创建Blob对象 - 使用Word兼容的MIME类型
- const blob = new Blob([wordContent], {
- type: 'application/msword'
- })
- // 下载文件
- const url = URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.setAttribute('href', url)
- link.setAttribute('download', `${fileName}-${new Date().toISOString().split('T')[0]}.doc`)
- link.style.visibility = 'hidden'
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- URL.revokeObjectURL(url)
- console.log('Word文档已下载')
- } catch (error) {
- console.error('下载Word文档失败:', error)
- // 备用方案:使用简单的文本下载
- const contentStr = String(content)
- const blob = new Blob([contentStr], { type: 'text/plain;charset=utf-8' })
- const url = window.URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = `${fileName}-${new Date().toISOString().split('T')[0]}.txt`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- window.URL.revokeObjectURL(url)
- console.log('已降级为文本文件下载')
- }
- }
- // 创建简洁的Word文档内容(参考AI写作的格式)
- const createSimpleWordContent = (content, fileName) => {
- // 处理内容,转换为简洁格式
- let processedContent = content
- // 处理Markdown标题
- .replace(/^# (.*?)$/gm, '<h1>$1</h1>')
- .replace(/^## (.*?)$/gm, '<h2>$1</h2>')
- .replace(/^### (.*?)$/gm, '<h3>$1</h3>')
- .replace(/^#### (.*?)$/gm, '<h4>$1</h4>')
- // 处理Markdown加粗
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- // 处理Markdown斜体
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- // 处理Markdown列表
- .replace(/^\- (.*$)/gim, '<div class="list-item">- $1</div>')
- .replace(/^(\d+)\. (.*$)/gim, '<div class="list-item">$1. $2</div>')
- // 处理换行符
- .replace(/\n/g, '<br>')
- // 创建HTML格式的文档内容(兼容Microsoft Office Word)
- return `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word 15">
- <meta name="Originator" content="Microsoft Word 15">
- <title>${fileName || '培训讲义'}</title>
- <style>
- body {
- font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- margin: 24px;
- color: #000;
- }
- .header {
- text-align: center;
- margin-bottom: 14px;
- }
- .doc-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- }
- .content {
- font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- color: #000;
- margin: 0;
- padding: 0;
- }
- .header {
- text-align: center;
- margin-bottom: 30px;
- }
- .doc-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 20px;
- color: #333;
- }
- h1 {
- font-size: 20px;
- font-weight: bold;
- margin: 20px 0 15px 0;
- color: #333;
- }
- h2 {
- font-size: 18px;
- font-weight: bold;
- margin: 18px 0 12px 0;
- color: #333;
- }
- h3 {
- font-size: 16px;
- font-weight: bold;
- margin: 15px 0 10px 0;
- color: #333;
- }
- h4 {
- font-size: 14px;
- font-weight: bold;
- margin: 12px 0 8px 0;
- color: #333;
- }
- p {
- margin: 10px 0;
- text-align: justify;
- }
- .list-item {
- margin: 5px 0;
- padding-left: 20px;
- }
- strong {
- font-weight: bold;
- }
- em {
- font-style: italic;
- }
- </style>
- </head>
- <body>
- <div class='header'>
- <div class='doc-title'>${fileName || '培训讲义'}</div>
- </div>
- <div class='content'>
- ${processedContent}
- </div>
- </body>
- </html>
- `
- }
- // 解析AI生成的内容,提取题目信息
- const parseExamContent = (content, fileName) => {
- const contentStr = String(content)
- console.log('开始解析内容,原始内容长度:', contentStr.length)
- const lines = contentStr.split('\n').filter(line => line.trim())
- console.log('分割后的行数:', lines.length)
- console.log('前10行内容:', lines.slice(0, 10))
- const examData = {
- title: fileName,
- totalScore: 0,
- totalQuestions: 0,
- singleChoice: {
- questions: [],
- scorePerQuestion: 2,
- totalScore: 0
- },
- multiple: {
- questions: [],
- scorePerQuestion: 3,
- totalScore: 0
- },
- judge: {
- questions: [],
- scorePerQuestion: 2,
- totalScore: 0
- },
- short: {
- questions: [],
- scorePerQuestion: 10,
- totalScore: 0
- }
- }
- let currentSection = ''
- let questionIndex = 0
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i].trim()
- if (!line) continue
- // 检测章节
- if (line.includes('一、单选题') || line.includes('单选题') || line.includes('选择题') || line.includes('一、')) {
- currentSection = 'singleChoice'
- continue
- } else if (line.includes('二、多选题') || line.includes('多选题') || line.includes('多项选择题') || line.includes('二、')) {
- currentSection = 'multiple'
- continue
- } else if (line.includes('三、判断题') || line.includes('判断题') || line.includes('三、')) {
- currentSection = 'judge'
- continue
- } else if (line.includes('四、简答题') || line.includes('简答题') || line.includes('问答题') || line.includes('四、')) {
- console.log('检测到简答题章节:', line)
- currentSection = 'short'
- continue
- }
- // 检测题目
- if (line.match(/^\d+[.、]/) || line.match(/^第\d+题/)) {
- questionIndex++
- // 检查是否是填空题(包含空白处)
- const isFillQuestion = line.includes('_______') || line.includes('____') || line.includes('空白')
- // 如果当前是判断题章节但题目是填空题,则归类为填空题
- if (currentSection === 'judge' && isFillQuestion) {
- currentSection = 'fill'
- }
- const question = {
- text: line.replace(/^\d+[.、]/, '').replace(/^第\d+题/, '').trim()
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 转换粗体
- .replace(/\*(.*?)\*/g, '<em>$1</em>'), // 转换斜体
- options: [],
- selectedAnswer: '',
- selectedAnswers: [],
- outline: { keyFactors: '' }
- }
- // 收集选项和答案
- let j = i + 1
- while (j < lines.length) {
- const nextLine = lines[j].trim()
- if (!nextLine) {
- j++
- continue
- }
- // 检测选项
- if (nextLine.match(/^[A-D][.、]/)) {
- question.options.push({
- key: nextLine.charAt(0),
- text: nextLine.substring(2).trim()
- })
- }
- // 检测答案
- else if (nextLine.includes('正确答案:') || nextLine.includes('答案:')) {
- const answerText = nextLine.replace(/正确答案[::]?/, '').replace(/答案[::]?/, '').trim()
- if (currentSection === 'multiple') {
- question.selectedAnswers = answerText.split('').filter(char => /[A-D]/.test(char))
- } else if (currentSection === 'judge') {
- question.selectedAnswer = answerText.includes('正确') ? '正确' : '错误'
- } else if (currentSection === 'fill') {
- question.selectedAnswer = answerText
- } else {
- question.selectedAnswer = answerText
- }
- }
- // 检测解析
- else if (nextLine.includes('解析:') || nextLine.includes('说明:')) {
- let analysisText = nextLine.replace(/解析[::]?/, '').replace(/说明[::]?/, '').trim()
- // 收集多行解析内容
- let k = j + 1
- while (k < lines.length) {
- const nextAnalysisLine = lines[k]?.trim()
- // 如果遇到下一题或新章节,停止收集
- if (nextAnalysisLine.match(/^\d+[.、]/) || nextAnalysisLine.match(/^第\d+题/) ||
- nextAnalysisLine.includes('一、单选题') || nextAnalysisLine.includes('二、多选题') ||
- nextAnalysisLine.includes('三、判断题') || nextAnalysisLine.includes('四、简答题') ||
- nextAnalysisLine.includes('单选题') || nextAnalysisLine.includes('多选题') ||
- nextAnalysisLine.includes('判断题') || nextAnalysisLine.includes('简答题') ||
- nextAnalysisLine.includes('正确答案:') || nextAnalysisLine.includes('答题要点:')) {
- break
- }
- // 如果遇到空行,跳过
- if (nextAnalysisLine === '') {
- k++
- continue
- }
- // 收集解析内容
- if (analysisText) {
- analysisText += '\n' + nextAnalysisLine
- } else {
- analysisText = nextAnalysisLine
- }
- k++
- }
- question.outline.keyFactors = analysisText
- console.log('收集到解析内容:', analysisText)
- j = k - 1 // 更新j的位置
- }
- // 检测答题要点或答案
- else if (nextLine.includes('答题要点:') || nextLine.includes('答案:') || nextLine.includes('答案')) {
- console.log('找到简答题答案:', nextLine)
- let answerText = nextLine.replace(/答题要点[::]?/, '').replace(/答案[::]?/, '').trim()
- // 无论是否包含"未设置",都尝试收集多行答案内容
- let k = j + 1
- while (k < lines.length) {
- const nextAnswerLine = lines[k]?.trim()
- // 如果遇到下一题或新章节,停止收集
- if (nextAnswerLine.match(/^\d+[.、]/) || nextAnswerLine.match(/^第\d+题/) ||
- nextAnswerLine.includes('一、单选题') || nextAnswerLine.includes('二、多选题') ||
- nextAnswerLine.includes('三、判断题') || nextAnswerLine.includes('四、简答题') ||
- nextAnswerLine.includes('单选题') || nextAnswerLine.includes('多选题') ||
- nextAnswerLine.includes('判断题') || nextAnswerLine.includes('简答题') ||
- nextAnswerLine.includes('正确答案:') || nextAnswerLine.includes('解析:')) {
- break
- }
- // 如果遇到空行,跳过
- if (nextAnswerLine === '') {
- k++
- continue
- }
- // 收集答案内容
- if (answerText) {
- answerText += '\n' + nextAnswerLine
- } else {
- answerText = nextAnswerLine
- }
- k++
- }
- // 如果答案包含"未设置",则清空
- if (answerText.includes('未设置')) {
- answerText = ''
- }
- question.outline.keyFactors = answerText
- console.log('收集到完整答题要点:', answerText)
- j = k - 1 // 更新j的位置
- }
- // 如果没有找到答题要点,尝试从题目后面直接收集答案内容
- else if (currentSection === 'short' && nextLine.trim() !== '' && !nextLine.match(/^\d+[.、]/) &&
- !nextLine.includes('一、') && !nextLine.includes('二、') && !nextLine.includes('三、') &&
- !nextLine.includes('四、') && !nextLine.includes('五、') &&
- !nextLine.includes('单选题') && !nextLine.includes('多选题') &&
- !nextLine.includes('判断题') && !nextLine.includes('简答题')) {
- // 如果当前题目还没有答题要点,则收集这行作为答案
- if (!question.outline.keyFactors || question.outline.keyFactors === '') {
- question.outline.keyFactors = nextLine.trim()
- console.log('收集到简答题答案内容:', nextLine.trim())
- }
- }
- // 如果遇到下一题或新章节,停止收集
- else if (nextLine.match(/^\d+[.、]/) || nextLine.match(/^第\d+题/) ||
- nextLine.includes('一、单选题') || nextLine.includes('二、多选题') ||
- nextLine.includes('三、判断题') || nextLine.includes('四、简答题') ||
- nextLine.includes('单选题') || nextLine.includes('多选题') ||
- nextLine.includes('判断题') || nextLine.includes('简答题')) {
- break
- }
- j++
- }
- if (currentSection && examData[currentSection]) {
- examData[currentSection].questions.push(question)
- examData.totalQuestions++
- }
- i = j - 1 // 跳过已处理的行
- }
- }
- // 计算分数
- examData.singleChoice.totalScore = examData.singleChoice.questions.length * examData.singleChoice.scorePerQuestion
- examData.multiple.totalScore = examData.multiple.questions.length * examData.multiple.scorePerQuestion
- examData.judge.totalScore = examData.judge.questions.length * examData.judge.scorePerQuestion
- examData.short.totalScore = examData.short.questions.length * examData.short.scorePerQuestion
- examData.totalScore = examData.singleChoice.totalScore + examData.multiple.totalScore +
- examData.judge.totalScore + examData.short.totalScore
- return examData
- }
- // 自动保存修改后的PPT数据
- const saveModifiedPPTData = () => {
- try {
- // 将修改后的PPT数据保存到localStorage
- const modifiedPPTData = {
- slides: generatedPPT.value,
- timestamp: Date.now(),
- title: outlineTitle.value || '安全培训演示文稿'
- }
- localStorage.setItem('safetyHazardModifiedPPT', JSON.stringify(modifiedPPTData))
- console.log('PPT修改已自动保存')
- } catch (error) {
- console.error('保存PPT数据失败:', error)
- }
- }
- // 检查并跳转到指定步骤
- // 加载修改后的PPT数据
- const loadModifiedPPTData = () => {
- try {
- const savedData = localStorage.getItem('safetyHazardModifiedPPT')
- if (savedData) {
- const data = JSON.parse(savedData)
- // 检查数据是否过期(24小时)
- if (Date.now() - data.timestamp < 24 * 60 * 60 * 1000) {
- generatedPPT.value = data.slides || []
- console.log('已加载修改后的PPT数据')
- return true
- } else {
- localStorage.removeItem('safetyHazardModifiedPPT')
- console.log('修改后的PPT数据已过期,已清除')
- }
- }
- } catch (error) {
- console.error('加载修改后的PPT数据失败:', error)
- }
- return false
- }
- // 导出为真正的PPTX文件(使用修改后的数据)
- const exportAsPPTX = async () => {
- if (generatedPPT.value.length === 0) {
- ElMessage.warning('没有可导出的PPT内容,请先生成PPT')
- return
- }
- try {
- // 动态导入PptxGenJS
- const PptxGenJS = (await import('pptxgenjs')).default
- // 创建新的PPT实例
- const pptx = new PptxGenJS()
- // 设置PPT页面尺寸(16:9比例,无黑边)
- pptx.defineLayout({
- name: 'CUSTOM_16_9',
- width: 10,
- height: 5.625
- })
- pptx.layout = 'CUSTOM_16_9'
- // 设置PPT属性
- pptx.author = '安全培训系统'
- pptx.company = '蜀道科技'
- pptx.subject = '安全培训演示文稿'
- pptx.title = outlineTitle.value || '安全培训演示文稿'
- // 遍历生成的所有幻灯片(使用修改后的数据)
- for (let i = 0; i < generatedPPT.value.length; i++) {
- const slideData = generatedPPT.value[i]
- console.log(`正在转换第 ${i + 1} 页:`, slideData.type)
- await convertSlideToPptx(pptx, slideData)
- }
- // 生成并下载PPTX文件
- const fileName = `安全培训-${outlineTitle.value || '演示文稿'}-${new Date().toISOString().split('T')[0]}.pptx`
- await pptx.writeFile({ fileName })
- console.log('PPTX文件已生成并下载')
- ElMessage.success(`成功导出PPTX文件!\n文件名: ${fileName}`)
- } catch (error) {
- console.error('导出PPTX失败:', error)
- ElMessage.error('导出PPTX失败: ' + error.message)
- }
- }
- // 将单个幻灯片转换为PPTX格式
- const convertSlideToPptx = async (pptx, slideData) => {
- // 添加新幻灯片
- const slide = pptx.addSlide()
- // 设置背景
- if (slideData.background) {
- if (slideData.background.type === 'solid') {
- const bgColor = convertColorForPptx(slideData.background.color || '#FFFFFF')
- slide.background = { color: bgColor }
- } else if (slideData.background.type === 'gradient' && slideData.background.gradient) {
- // PptxGenJS对渐变支持有限,使用第一个颜色作为背景
- const firstColor = slideData.background.gradient.colors[0]?.color || '#FFFFFF'
- const bgColor = convertColorForPptx(firstColor)
- slide.background = { color: bgColor }
- }
- }
- // 处理每个元素
- for (const element of slideData.elements) {
- await addElementToPptxSlide(slide, element)
- }
- }
- // 将元素添加到PPTX幻灯片
- const addElementToPptxSlide = async (slide, element) => {
- try {
- // 转换坐标和尺寸 (从960x540到标准PPT尺寸)
- const scaleX = 10 // PPTX使用英寸,960px ≈ 10英寸
- const scaleY = 5.625 // 540px ≈ 5.625英寸
- const x = (element.left / 960) * scaleX
- const y = (element.top / 540) * scaleY
- const w = (element.width / 960) * scaleX
- const h = (element.height / 540) * scaleY
- switch (element.type) {
- case 'text':
- addTextToPptx(slide, element, x, y, w, h)
- break
- case 'image':
- await addImageToPptx(slide, element, x, y, w, h)
- break
- case 'shape':
- addShapeToPptx(slide, element, x, y, w, h)
- break
- }
- } catch (error) {
- console.warn(`添加元素失败 ${element.type}:`, error)
- }
- }
- // 添加文本到PPTX
- const addTextToPptx = (slide, element, x, y, w, h) => {
- // 提取纯文本内容
- const textContent = extractTextFromHtml(element.content)
- // 提取样式信息
- const style = extractStyleFromHtml(element.content)
- // 优先使用HTML中的颜色,其次使用元素默认颜色
- const textColor = style.color || element.defaultColor || '#000000'
- // 计算透明度(PPTX使用0-100的百分比)
- const opacity = element.opacity !== undefined ? Math.round(element.opacity * 100) : 100
- // 调试文本对齐
- if (element.content && element.content.includes('text-align: center')) {
- console.log(`文本对齐调试 ${element.id || 'unknown'}:`, {
- content: element.content,
- extractedAlign: style.align,
- finalAlign: style.align || 'left'
- })
- }
- slide.addText(textContent, {
- x: x,
- y: y,
- w: w,
- h: h,
- fontSize: style.fontSize || 16,
- color: convertColorForPptx(textColor),
- fontFace: element.defaultFontName || '微软雅黑',
- align: style.align || 'left',
- valign: 'middle',
- bold: style.bold || false,
- wrap: true,
- transparency: 100 - opacity, // PPTX使用transparency(0=不透明,100=完全透明)
- line: null // 禁用文本边框
- })
- }
- // 添加图片到PPTX
- const addImageToPptx = async (slide, element, x, y, w, h) => {
- if (!element.src) {
- console.warn('图片元素没有src属性:', element)
- return
- }
- try {
- // 计算透明度(PPTX使用0-100的百分比)
- const opacity = element.opacity !== undefined ? Math.round(element.opacity * 100) : 100
- const transparency = 100 - opacity
- console.log('处理图片元素:', {
- id: element.id,
- srcType: element.src.startsWith('data:image') ? 'base64' : 'url',
- srcLength: element.src.length,
- opacity: opacity,
- transparency: transparency
- })
- // 如果是base64图片,需要检查是否为SVG格式
- if (element.src.startsWith('data:image')) {
- console.log('使用base64图片数据')
- // 验证base64数据格式
- if (!element.src.includes(',')) {
- throw new Error('Base64数据格式错误:缺少逗号分隔符')
- }
- const [header, data] = element.src.split(',')
- if (!header || !data) {
- throw new Error('Base64数据格式错误:header或data为空')
- }
- // 检查数据长度(base64数据不能为空)
- if (data.length < 100) {
- throw new Error('Base64数据过短,可能已损坏')
- }
- let imageData = element.src
- // 检查是否为SVG格式,如果是则转换为PNG
- if (element.src.includes('data:image/svg+xml')) {
- console.log('检测到SVG图像,开始转换为PNG格式')
- try {
- imageData = await convertSvgToPng(element.src)
- console.log('SVG转PNG成功,使用转换后的PNG数据')
- } catch (svgError) {
- console.error('SVG转PNG失败:', svgError)
- throw new Error('SVG图像转换失败: ' + svgError.message)
- }
- }
- try {
- slide.addImage({
- data: imageData,
- x: x,
- y: y,
- w: w,
- h: h,
- transparency: transparency,
- sizing: {
- type: 'cover', // 填满目标区域,超出部分裁剪
- w: w,
- h: h
- },
- line: null // 禁用图片边框
- })
- console.log('Base64图片添加成功')
- } catch (addImageError) {
- console.error('PptxGenJS添加图片失败:', addImageError)
- // 尝试使用不同的方式添加图片
- try {
- // 移除data:image/xxx;base64,前缀,只保留纯base64数据
- const pureBase64 = imageData.split(',')[1]
- slide.addImage({
- data: pureBase64,
- x: x,
- y: y,
- w: w,
- h: h,
- transparency: transparency,
- sizing: {
- type: 'cover', // 填满目标区域,超出部分裁剪
- w: w,
- h: h
- },
- line: null // 禁用图片边框
- })
- console.log('使用纯base64数据添加成功')
- } catch (retryError) {
- console.error('重试添加图片也失败:', retryError)
- throw addImageError // 抛出原始错误
- }
- }
- } else {
- // 如果是URL,需要转换为base64
- console.log('转换URL图片为base64:', element.src)
- const base64Data = await convertImageToBase64(element.src)
- slide.addImage({
- data: base64Data,
- x: x,
- y: y,
- w: w,
- h: h,
- transparency: transparency,
- sizing: {
- type: 'cover', // 填满目标区域,超出部分裁剪
- w: w,
- h: h
- },
- line: null // 禁用图片边框
- })
- console.log('URL图片转换并添加成功')
- }
- } catch (error) {
- console.error('添加图片失败:', error)
- console.error('图片元素详情:', element)
- // 图片加载失败时,添加一个占位文本
- slide.addText(`图片加载失败: ${error.message}`, {
- x: x,
- y: y,
- w: w,
- h: h,
- fontSize: 10,
- color: 'FF0000',
- align: 'center',
- valign: 'middle',
- bold: true
- })
- }
- }
- // 添加形状到PPTX
- const addShapeToPptx = (slide, element, x, y, w, h) => {
- // 获取颜色值,优先使用fill属性
- const fillColor = element.fill || element.color || '#007bff'
- // 计算透明度
- let transparency = 0
- if (fillColor.startsWith('rgba(')) {
- // 如果颜色是rgba格式,优先使用其alpha通道
- const rgbaMatch = fillColor.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/)
- if (rgbaMatch) {
- const alpha = parseFloat(rgbaMatch[4])
- transparency = Math.round((1 - alpha) * 100)
- }
- } else if (element.opacity !== undefined) {
- // 如果颜色不是rgba但元素有opacity属性,使用它
- transparency = 100 - Math.round(element.opacity * 100)
- }
- const convertedColor = convertColorForPptx(fillColor)
- // 调试overlay元素
- if (element.id && element.id.includes('overlay')) {
- console.log(`Overlay元素调试 ${element.id}:`, {
- originalFill: element.fill,
- fillColor: fillColor,
- convertedColor: convertedColor,
- opacity: element.opacity,
- transparency: transparency,
- alphaFromRgba: fillColor.startsWith('rgba(') ? parseFloat(fillColor.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/)?.[4] || '1') : null
- })
- }
- slide.addShape('rect', {
- x: x,
- y: y,
- w: w,
- h: h,
- fill: {
- type: 'solid',
- color: convertedColor,
- transparency: transparency
- },
- line: null // 完全禁用边框
- })
- }
- // 从HTML提取纯文本
- const extractTextFromHtml = (html) => {
- if (!html) return ''
- // 创建临时DOM元素来解析HTML
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = html
- return tempDiv.textContent || tempDiv.innerText || ''
- }
- // 从HTML提取样式
- const extractStyleFromHtml = (html) => {
- if (!html) return {}
- const style = {}
- // 检查字体大小 - 让导出字号和预览保持一致
- const fontSizeMatch = html.match(/font-size:\s*(\d+)px/)
- if (fontSizeMatch) {
- const previewFontSize = parseInt(fontSizeMatch[1])
- // 为了让导出字号和预览视觉效果一致,我们进一步缩小导出字号
- // 预览字体大小 = 原始字体大小 * (1105/960)
- // 导出时再缩小一点,让效果和预览一致
- const scaleFactor = 1105 / 960
- const additionalScale = 0.7 // 额外缩小30%,让导出和预览视觉效果一致
- style.fontSize = Math.round(previewFontSize / scaleFactor * additionalScale)
- console.log(`字体大小转换: 预览${previewFontSize}px -> 导出${style.fontSize}px (缩放因子: ${scaleFactor.toFixed(3)}, 额外缩放: ${additionalScale})`)
- }
- // 检查颜色
- const colorMatch = html.match(/color:\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|rgb\([^)]+\)|rgba\([^)]+\))/)
- if (colorMatch) {
- style.color = colorMatch[1]
- }
- // 检查对齐方式
- if (html.includes('text-align: center')) {
- style.align = 'center'
- } else if (html.includes('text-align: right')) {
- style.align = 'right'
- } else {
- style.align = 'left' // 默认左对齐
- }
- // 检查粗体
- if (html.includes('<strong>') || html.includes('<b>')) {
- style.bold = true
- }
- return style
- }
- // 将SVG转换为PNG格式的base64
- const convertSvgToPng = (svgDataUrl) => {
- return new Promise((resolve, reject) => {
- try {
- // 创建Image对象来加载SVG
- const img = new Image()
- img.onload = () => {
- try {
- // 创建Canvas
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- // 设置Canvas尺寸
- canvas.width = img.width || 960
- canvas.height = img.height || 540
- // 设置高质量渲染
- ctx.imageSmoothingEnabled = true
- ctx.imageSmoothingQuality = 'high'
- // 绘制SVG到Canvas
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
- // 转换为PNG格式的base64
- const pngDataUrl = canvas.toDataURL('image/png', 1.0)
- console.log('SVG转PNG成功:', {
- originalSvgLength: svgDataUrl.length,
- pngDataUrlLength: pngDataUrl.length,
- canvasSize: `${canvas.width}x${canvas.height}`
- })
- resolve(pngDataUrl)
- } catch (canvasError) {
- console.error('Canvas转换失败:', canvasError)
- reject(new Error('SVG转PNG失败: ' + canvasError.message))
- }
- }
- img.onerror = (error) => {
- console.error('SVG图像加载失败:', error)
- reject(new Error('SVG图像加载失败'))
- }
- // 加载SVG图像
- img.src = svgDataUrl
- } catch (error) {
- console.error('SVG转换初始化失败:', error)
- reject(new Error('SVG转换初始化失败: ' + error.message))
- }
- })
- }
- // 将图片URL转换为base64
- const convertImageToBase64 = (url) => {
- return new Promise((resolve, reject) => {
- const img = new Image()
- img.crossOrigin = 'anonymous'
- img.onload = () => {
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- canvas.width = img.width
- canvas.height = img.height
- ctx.drawImage(img, 0, 0)
- try {
- const dataURL = canvas.toDataURL('image/png')
- resolve(dataURL)
- } catch (error) {
- reject(error)
- }
- }
- img.onerror = () => {
- reject(new Error('图片加载失败'))
- }
- img.src = url
- })
- }
- // 颜色转换函数 - 将各种颜色格式转换为PPTX需要的格式
- const convertColorForPptx = (color) => {
- if (!color) return 'FFFFFF'
- // 如果已经是6位十六进制格式(无#),直接返回
- if (/^[0-9A-Fa-f]{6}$/.test(color)) {
- return color.toUpperCase()
- }
- // 如果是#开头的十六进制格式
- if (color.startsWith('#')) {
- const hex = color.substring(1)
- // 处理3位十六进制颜色(如#f0f)
- if (hex.length === 3) {
- return (hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]).toUpperCase()
- }
- // 处理6位十六进制颜色(如#ff00ff)
- if (hex.length === 6) {
- return hex.toUpperCase()
- }
- }
- // 处理rgb()格式
- const rgbMatch = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
- if (rgbMatch) {
- const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0')
- const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0')
- const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0')
- return (r + g + b).toUpperCase()
- }
- // 处理rgba()格式(忽略透明度)
- const rgbaMatch = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)/)
- if (rgbaMatch) {
- const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, '0')
- const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, '0')
- const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, '0')
- return (r + g + b).toUpperCase()
- }
- // 处理命名颜色
- const namedColors = {
- 'white': 'FFFFFF',
- 'black': '000000',
- 'red': 'FF0000',
- 'green': '008000',
- 'blue': '0000FF',
- 'yellow': 'FFFF00',
- 'orange': 'FFA500',
- 'purple': '800080',
- 'pink': 'FFC0CB',
- 'gray': '808080',
- 'grey': '808080'
- }
- const normalizedColor = color.toLowerCase()
- if (namedColors[normalizedColor]) {
- return namedColors[normalizedColor]
- }
- // 如果无法识别,默认返回黑色
- console.warn(`无法识别的颜色格式: ${color},使用默认黑色`)
- return '000000'
- }
- const updateSlideThumbnails = () => {
- // 优先使用generatedPPT(红色主题模板),如果没有则使用pptSlides(通用类PPT)
- const pptData = generatedPPT.value && generatedPPT.value.length > 0 ? generatedPPT.value : pptSlides.value
- // 如果都没有数据,直接使用template5的5张图片
- if (!pptData || pptData.length === 0) {
- slideImages.value = [
- template5Slide1,
- template5Slide2,
- template5Slide3,
- template5Slide4,
- template5Slide5
- ]
- console.log('PPT内容为空,使用默认template5图片,共', slideImages.value.length, '张')
- return
- }
- // 根据PPT内容生成缩略图
- slideImages.value = pptData.map((slide, index) => {
- // 如果是红色主题模板,使用模板7的图片
- if (selectedTemplateStyle.value === 'red') {
- const template7Images = [
- template7Slide1, // 第1页 - 封面
- template7Slide2, // 第2页 - 目录
- template7Slide3, // 第3页 - 过渡
- template7Slide4, // 第4页 - 内容
- template7Slide5 // 第5页 - 结束
- ]
- // 直接根据索引选择模板7图片,确保每张都不同
- console.log(`幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- return template7Images[index % 5]
- }
- // 如果是蓝色科技主题模板,使用模板8的图片
- if (selectedTemplateStyle.value === 'blueTech') {
- const template8Images = [
- template8Slide1, // 第1页 - 封面
- template8Slide2, // 第2页 - 目录
- template8Slide3, // 第3页 - 过渡
- template8Slide4, // 第4页 - 内容
- template8Slide5 // 第5页 - 结束
- ]
- // 直接根据索引选择模板8图片,确保每张都不同
- console.log(`幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- return template8Images[index % 5]
- }
- // 否则使用template5文件夹中的图片,循环使用5张图片
- const template5Images = [
- template5Slide1,
- template5Slide2,
- template5Slide3,
- template5Slide4,
- template5Slide5
- ]
- return template5Images[index % 5] // 循环使用5张图片
- })
- console.log('缩略图更新完成,共', slideImages.value.length, '页')
- }
- // 根据模板风格和幻灯片类型生成缩略图
- const generateThumbnailForStyle = (slideType, style) => {
- // 根据风格选择背景图片
- let backgroundImage = ''
- if (style === 'redElegant' || style === 'red') {
- backgroundImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0911_1757633045.png'
- } else if (style === 'blueTech') {
- backgroundImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0911_1757633045.png'
- } else {
- // 默认风格使用template5图片
- const template5Images = [
- template5Slide1, // cover
- template5Slide2, // contents
- template5Slide3, // transition
- template5Slide4, // content
- template5Slide5 // end
- ]
- switch (slideType) {
- case 'cover':
- return template5Slide1
- case 'contents':
- return template5Slide2
- case 'transition':
- return template5Slide3
- case 'content':
- return template5Slide4
- case 'end':
- return template5Slide5
- default:
- return template5Images[0]
- }
- }
- // 对于红色和蓝色风格,所有页面都使用对应的背景图片
- return backgroundImage
- }
- // 更新动态模板缩略图
- const updateDynamicTemplateThumbnails = () => {
- if (!generatedPPT.value || generatedPPT.value.length === 0) {
- // 即使没有PPT数据,也要根据风格更新缩略图
- if (selectedTemplateStyle.value === 'redElegant' || selectedTemplateStyle.value === 'red') {
- // 使用模板7的图片
- slideImages.value = [
- template7Slide1,
- template7Slide2,
- template7Slide3,
- template7Slide4,
- template7Slide5
- ]
- } else if (selectedTemplateStyle.value === 'blueTech') {
- // 使用模板8的图片
- slideImages.value = [
- template8Slide1,
- template8Slide2,
- template8Slide3,
- template8Slide4,
- template8Slide5
- ]
- } else {
- slideImages.value = [
- template5Slide1,
- template5Slide2,
- template5Slide3,
- template5Slide4,
- template5Slide5
- ]
- }
- // 强制Vue更新 - 使用数组重新赋值触发响应式
- slideImages.value = [...slideImages.value]
- console.log('动态模板内容为空,使用风格化图片,共', slideImages.value.length, '张')
- console.log('当前风格:', selectedTemplateStyle.value)
- console.log('更新后的缩略图数组:', slideImages.value)
- return
- }
- // 根据动态模板内容生成缩略图
- slideImages.value = generatedPPT.value.map((slide, index) => {
- // 如果是红色主题,使用模板7图片
- if (selectedTemplateStyle.value === 'redElegant' || selectedTemplateStyle.value === 'red') {
- const template7Images = [
- template7Slide1, // 第1页 - 封面
- template7Slide2, // 第2页 - 目录
- template7Slide3, // 第3页 - 过渡
- template7Slide4, // 第4页 - 内容
- template7Slide5 // 第5页 - 结束
- ]
- // 直接根据索引选择模板7图片,确保每张都不同
- console.log(`动态模板幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- return template7Images[index % 5]
- }
- // 如果是蓝色科技主题,使用模板8图片
- if (selectedTemplateStyle.value === 'blueTech') {
- const template8Images = [
- template8Slide1, // 第1页 - 封面
- template8Slide2, // 第2页 - 目录
- template8Slide3, // 第3页 - 过渡
- template8Slide4, // 第4页 - 内容
- template8Slide5 // 第5页 - 结束
- ]
- // 直接根据索引选择模板8图片,确保每张都不同
- console.log(`动态模板幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- return template8Images[index % 5]
- }
- return generateThumbnailForStyle(slide.type, selectedTemplateStyle.value)
- })
- // 强制Vue更新 - 使用数组重新赋值触发响应式
- slideImages.value = [...slideImages.value]
- console.log('动态模板缩略图更新完成,共', slideImages.value.length, '页')
- }
- const selectTemplate = async (index) => {
- selectedTemplate.value = index
- const selectedTemplateData = templateStyles.value[index]
- console.log('选择模板:', selectedTemplateData.title)
- // 设置模板风格
- if (selectedTemplateData.style) {
- selectedTemplateStyle.value = selectedTemplateData.style
- console.log('设置模板风格:', selectedTemplateData.style)
- }
- // 检查是否为动态模板
- if (selectedTemplateData.type === 'dynamic') {
- isDynamicTemplate.value = true
- // 立即更新缩略图以显示风格变化
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- updateDynamicTemplateThumbnails()
- console.log('缩略图已更新为', selectedTemplateData.style, '风格')
- } else {
- // 即使没有生成的PPT,也要更新缩略图显示风格
- console.log('准备更新缩略图,当前风格:', selectedTemplateStyle.value)
- updateDynamicTemplateThumbnails()
- console.log('缩略图已更新为', selectedTemplateData.style, '风格(无PPT数据)')
- console.log('最终缩略图数组:', slideImages.value)
- }
- // 强制触发Vue响应式更新
- nextTick(() => {
- slideImages.value = [...slideImages.value]
- console.log('强制更新缩略图完成')
- })
- // 如果有大纲数据,生成预览
- if (outlineData.value && outlineData.value.length > 0) {
- try {
- const previewResult = previewOutlineMatch(outlineData.value, outlineTitle.value)
- if (previewResult.success) {
- templatePreview.value = previewResult.preview
- templateAnalysis.value = previewResult.preview
- console.log('动态模板预览生成成功:', templatePreview.value)
- } else {
- console.error('动态模板预览生成失败:', previewResult.error)
- ElMessage.error('动态模板预览生成失败: ' + previewResult.error)
- }
- } catch (error) {
- console.error('动态模板预览生成异常:', error)
- ElMessage.error('动态模板预览生成异常: ' + error.message)
- }
- }
- } else {
- isDynamicTemplate.value = false
- templatePreview.value = null
- templateAnalysis.value = null
- // 处理静态模板(如Template7)
- if (selectedTemplateData.type === 'static' && selectedTemplateData.templateData) {
- console.log('选择静态模板:', selectedTemplateData.title, '等待用户点击应用模板')
- // 立即更新缩略图以显示风格变化(与动态模板保持一致)
- console.log('准备更新静态模板缩略图,当前风格:', selectedTemplateStyle.value)
- updateSlideThumbnails()
- console.log('静态模板缩略图已更新为', selectedTemplateData.style, '风格')
- // 为Template7生成预览信息,就像动态模板一样
- if (outlineData.value && outlineData.value.length > 0) {
- try {
- const previewResult = previewOutlineMatch(outlineData.value, outlineTitle.value)
- if (previewResult.success) {
- templatePreview.value = previewResult.preview
- templateAnalysis.value = previewResult.preview
- console.log('Template7预览生成成功:', templatePreview.value)
- } else {
- console.error('Template7预览生成失败:', previewResult.error)
- }
- } catch (error) {
- console.error('Template7预览生成异常:', error)
- }
- }
- }
- }
- }
- const applyTemplate = async () => {
- console.log('应用模板:', templateStyles.value[selectedTemplate.value].title)
- // 设置应用模板状态
- isApplyingTemplate.value = true
- isProcessing.value = true
- try {
- // 检查是否为动态模板
- if (isDynamicTemplate.value) {
- console.log('使用动态模板生成PPT...')
- let testOutline, testTitle, testDescription
- // 根据用户选择决定使用哪个数据源
- if (outlineData.value && outlineData.value.length > 0) {
- // 使用用户大纲数据,并转换为兼容格式
- testOutline = await convertUserOutlineToCompatibleFormat(outlineData.value)
- testTitle = outlineTitle.value || '用户生成的大纲' // 使用大纲标题作为PPT标题
- testDescription = `${testOutline.length}章节结构演示`
- console.log('使用用户大纲数据:', testTitle, `共${testOutline.length}个章节`)
- console.log('转换后的用户大纲数据结构:', JSON.stringify(testOutline, null, 2))
- } else if (selectedMockData.value !== null && selectedMockData.value !== undefined) {
- // 使用选中的mock数据
- const selectedOption = mockDataOptions.value[selectedMockData.value]
- testOutline = selectedOption.data
- testTitle = selectedOption.title
- testDescription = selectedOption.description
- console.log('使用mock数据:', selectedOption.title, selectedOption.description)
- } else {
- // 使用默认测试数据
- testOutline = mockDataOptions.value[0].data
- testTitle = mockDataOptions.value[0].title
- testDescription = mockDataOptions.value[0].description
- console.log('使用默认测试数据:', mockDataOptions.value[0].title)
- }
- // 生成动态模板
- const result = await matchOutlineAndGeneratePPT(testOutline, testTitle, selectedTemplateStyle.value)
- console.log('生成的动态模板结果:', result)
- if (!result.success) {
- throw new Error(result.error || '动态模板生成失败')
- }
- // 获取模板数据
- const dynamicTemplate = result.data.template
- console.log('提取的模板数据:', dynamicTemplate)
- // 验证模板数据格式
- if (!Array.isArray(dynamicTemplate)) {
- console.error('模板数据不是数组格式:', typeof dynamicTemplate, dynamicTemplate)
- throw new Error('模板数据格式不正确,期望数组但得到: ' + typeof dynamicTemplate)
- }
- // 启用预览模式
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果
- await generatePPTWithAnimation(dynamicTemplate, testOutline, testTitle)
- console.log('动态模板应用完成,共', generatedPPT.value.length, '张幻灯片')
- // 显示警告和建议
- if (result.warnings && result.warnings.length > 0) {
- ElMessage.warning('模板应用成功,但有以下建议: ' + result.warnings.join(', '))
- }
- if (result.recommendations && result.recommendations.length > 0) {
- console.log('优化建议:', result.recommendations)
- }
- // 更新缩略图
- updateDynamicTemplateThumbnails()
- // 显示成功消息
- const successMessage = outlineData.value && outlineData.value.length > 0 ?
- `动态模板应用完成!使用用户大纲数据 (${testOutline.length}个章节)` :
- `动态模板应用完成!使用数据: ${testTitle} (${testDescription})`
- ElMessage.success(successMessage)
- // 保存PPT数据到本地存储
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
- console.log('动态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
- }
- // 显示下载选项
- showDownloadOptions.value = true
- // 保存步骤信息到后端
- await saveStepToBackend(true, true) // 强制保存新生成的PPT
- // 更新历史记录状态
- await getHistoryRecordList()
- // 重新设置当前历史记录的选中状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value
- })
- // 确保缩略图条滚动到第一页
- nextTick(() => {
- const thumbnailStripEl = thumbnailStrip.value
- if (thumbnailStripEl) {
- thumbnailStripEl.scrollLeft = 0
- }
- })
- } else {
- console.log('使用静态模板生成PPT...')
- // 检查是否为Template7静态模板
- const selectedTemplateData = templateStyles.value[selectedTemplate.value]
- console.log('检查模板数据:', {
- title: selectedTemplateData.title,
- type: selectedTemplateData.type,
- hasTemplateData: !!selectedTemplateData.templateData,
- templateDataLength: selectedTemplateData.templateData?.length
- })
- if (selectedTemplateData.type === 'static' && selectedTemplateData.templateData) {
- // 检查是否为红色主题模板
- if (selectedTemplateData.title === '红色主题PPT') {
- console.log('应用Template7红色主题模板:', selectedTemplateData.title)
- try {
- // 使用我们精心设计的template_7.json结构,但支持动态多页生成和动画效果
- if (outlineData.value && outlineData.value.length > 0) {
- console.log('开始基于template_7.json生成动态多页结构...')
- // 显示生成进度
- isGeneratingTrainingMaterial.value = true
- // 生成红色主题模板数据
- const redThemeTemplate = generateDynamicTemplate7FromStatic(outlineData.value, outlineTitle.value)
- // 启用PPT预览模式 - 与通用类PPT保持一致
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果(与通用类PPT相同的动画逻辑)
- await generatePPTWithAnimation(redThemeTemplate, outlineData.value, outlineTitle.value)
- isGeneratingTrainingMaterial.value = false
- console.log('Template7动态多页结构生成完成,共', generatedPPT.value.length, '页')
- } else {
- // 如果没有大纲数据,使用默认的5页结构
- const defaultTemplate = JSON.parse(JSON.stringify(selectedTemplateData.templateData))
- // 启用PPT预览模式 - 与通用类PPT保持一致
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果
- await generatePPTWithAnimation(defaultTemplate, [], outlineTitle.value || '默认标题')
- console.log('Template7默认模板加载成功,共', generatedPPT.value.length, '页')
- }
- // 更新缩略图 - 与通用类PPT保持一致的处理流程
- updateDynamicTemplateThumbnails()
- // 显示成功消息
- const successMessage = outlineData.value && outlineData.value.length > 0 ?
- `红色主题模板应用完成!使用用户大纲数据 (${outlineData.value.length}个章节)` :
- `红色主题模板应用完成!使用默认内容`
- ElMessage.success(successMessage)
- // 保存PPT数据到本地存储
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
- console.log('Template7静态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
- }
- // 显示下载选项
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- // 保存步骤信息到后端
- await saveStepToBackend(true, true) // 强制保存新生成的PPT
- // 更新历史记录状态
- await getHistoryRecordList()
- // 重新设置当前历史记录的选中状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value
- })
- // 确保缩略图条滚动到第一页
- nextTick(() => {
- const thumbnailStripEl = thumbnailStrip.value
- if (thumbnailStripEl) {
- thumbnailStripEl.scrollLeft = 0
- }
- })
- } catch (error) {
- console.error('应用Template7静态模板失败:', error)
- ElMessage.error('应用Template7静态模板失败: ' + error.message)
- }
- }
- // 检查是否为蓝色科技主题模板
- if (selectedTemplateData.title === '蓝色科技主题PPT') {
- console.log('应用Template8蓝色科技主题模板:', selectedTemplateData.title)
- try {
- // 使用我们精心设计的template_8.json结构,但支持动态多页生成和动画效果
- if (outlineData.value && outlineData.value.length > 0) {
- console.log('开始基于template_8.json生成动态多页结构...')
- // 显示生成进度
- isGeneratingTrainingMaterial.value = true
- // 生成蓝色科技主题模板数据
- const blueTechThemeTemplate = generateDynamicTemplate8FromStatic(outlineData.value, outlineTitle.value)
- // 启用PPT预览模式 - 与通用类PPT保持一致
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果(与通用类PPT相同的动画逻辑)
- await generatePPTWithAnimation(blueTechThemeTemplate, outlineData.value, outlineTitle.value)
- isGeneratingTrainingMaterial.value = false
- console.log('Template8动态多页结构生成完成,共', generatedPPT.value.length, '页')
- } else {
- // 如果没有大纲数据,使用默认的5页结构
- const defaultTemplate = JSON.parse(JSON.stringify(selectedTemplateData.templateData))
- // 启用PPT预览模式 - 与通用类PPT保持一致
- showDownloadOptions.value = true
- currentPPTSlideIndex.value = 0
- isPPTPreviewMode.value = true
- console.log('已启用PPT预览模式,开始逐页生成效果...')
- // 实现逐页生成效果
- await generatePPTWithAnimation(defaultTemplate, [], outlineTitle.value || '默认标题')
- console.log('Template8默认模板加载成功,共', generatedPPT.value.length, '页')
- }
- // 更新缩略图 - 与通用类PPT保持一致的处理流程
- updateDynamicTemplateThumbnails()
- // 显示成功消息
- const successMessage = outlineData.value && outlineData.value.length > 0 ?
- `蓝色科技主题模板应用完成!使用用户大纲数据 (${outlineData.value.length}个章节)` :
- `蓝色科技主题模板应用完成!使用默认内容`
- ElMessage.success(successMessage)
- // 保存PPT数据到本地存储
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
- console.log('Template8静态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
- }
- } catch (error) {
- isGeneratingTrainingMaterial.value = false
- console.error('应用Template8静态模板失败:', error)
- ElMessage.error('应用Template8静态模板失败: ' + error.message)
- }
- }
- } else if (outlineData.value && outlineData.value.length > 0) {
- // 原有的静态模板逻辑
- console.log('开始生成PPT数据...')
- // 第一步:转换大纲为AIPPT格式(包含空内容)
- const aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
- console.log('生成的AIPPT数据:', aipptData)
- // 第二步:直接让AI填充AIPPT格式的数据
- const filledAIPPTData = await fillAIPPTContent(aipptData, outlineTitle.value)
- console.log('AI填充后的AIPPT数据:', filledAIPPTData)
- // 第三步:直接使用填充后的数据(需要保存步骤)
- await loadAIPPTData(filledAIPPTData, true)
- console.log('模板应用完成,PPT数据已加载')
- // 等待数据完全处理完成
- await nextTick()
- // 确保数据被正确保存到本地存储
- if (generatedPPT.value && generatedPPT.value.length > 0) {
- localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
- console.log('PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
- }
- // 显示成功消息
- ElMessage.success('模板应用完成,内容已填充!')
- // 更新历史记录状态
- await getHistoryRecordList()
- // 重新设置当前历史记录的选中状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value
- })
- // 显示下载选项(模板和大纲结合页面)
- showDownloadOptions.value = true
- // 确保缩略图条滚动到第一页
- nextTick(() => {
- const thumbnailStripEl = thumbnailStrip.value
- if (thumbnailStripEl) {
- thumbnailStripEl.scrollLeft = 0
- console.log('缩略图条已滚动到第一页')
- }
- })
- } else {
- // 如果没有大纲数据,直接加载默认数据
- await loadAIPPTData()
- ElMessage.success('模板应用完成!')
- showDownloadOptions.value = true
- }
- }
- } catch (error) {
- console.error('应用模板失败:', error)
- ElMessage.error('应用模板失败: ' + error.message)
- } finally {
- // 重置应用模板状态
- isApplyingTemplate.value = false
- isProcessing.value = false
- }
- }
- // 填充静态模板内容函数
- const fillStaticTemplateContent = async (pptData, outlineData, title) => {
- console.log('开始填充静态模板内容:', { title, chapters: outlineData.length })
- try {
- // 处理封面页
- const coverSlide = pptData.find(slide => slide.type === 'cover')
- if (coverSlide) {
- // 填充标题
- const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
- if (titleElement) {
- 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>`
- console.log('封面标题已填充:', title)
- }
- // 填充副标题
- const subtitleElement = coverSlide.elements.find(el => el.id === 'subtitle-text')
- if (subtitleElement) {
- const subtitle = outlineData.length > 0 ? `${outlineData.length}个章节的详细内容` : '详细内容介绍'
- subtitleElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: rgba(255,255,255,0.9);">${subtitle}</span></p>`
- console.log('封面副标题已填充:', subtitle)
- }
- }
- // 处理目录页
- const contentsSlide = pptData.find(slide => slide.type === 'contents')
- if (contentsSlide && outlineData.length > 0) {
- // 填充目录项
- for (let i = 0; i < Math.min(outlineData.length, 6); i++) {
- const chapter = outlineData[i]
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
- console.log(`目录项${i + 1}已填充:`, chapter.title)
- }
- }
- // 隐藏多余的目录项
- for (let i = outlineData.length; i < 6; i++) {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
- }
- }
- }
- // 处理过渡页和内容页
- let slideIndex = 0
- for (let chapterIndex = 0; chapterIndex < outlineData.length; chapterIndex++) {
- const chapter = outlineData[chapterIndex]
- // 查找过渡页
- const transitionSlide = pptData.find(slide => slide.type === 'transition' && slideIndex === 0)
- if (transitionSlide) {
- // 填充过渡页标题
- const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
- if (transitionTitleElement) {
- transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- console.log('过渡页标题已填充:', chapter.title)
- }
- // 填充过渡页内容(使用AI生成)
- const transitionContentElement = transitionSlide.elements.find(el => el.id === 'transition-content')
- if (transitionContentElement) {
- try {
- console.log(`🤖 正在为红色PPT过渡页生成章节介绍内容: ${chapter.title}`)
- // 调用AI API生成章节介绍内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT章节"${chapter.title}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
- })
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 红色PPT过渡页章节介绍生成完成: ${aiContent}`)
- transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${aiContent}</span></p>`
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = chapter.sections && chapter.sections.length > 0
- ? `本章将介绍${chapter.title}的相关内容`
- : `${chapter.title}的详细说明`
- transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${fallbackContent}</span></p>`
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- }
- } catch (aiError) {
- console.error(`❌ AI生成红色PPT过渡页内容失败:`, aiError)
- // AI生成失败,使用备用内容
- const fallbackContent = chapter.sections && chapter.sections.length > 0
- ? `本章将介绍${chapter.title}的相关内容`
- : `${chapter.title}的详细说明`
- transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${fallbackContent}</span></p>`
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- }
- }
- slideIndex++
- }
- // 查找内容页
- const contentSlide = pptData.find(slide => slide.type === 'content' && slideIndex === 0)
- if (contentSlide && chapter.sections && chapter.sections.length > 0) {
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- console.log('内容页标题已填充:', chapter.title)
- }
- // 填充内容项
- const sections = chapter.sections.slice(0, 3) // 最多显示3个要点
- for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
- const section = sections[sectionIndex]
- // 填充要点标题(使用AI生成)
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${sectionIndex + 1}`)
- if (itemTitleElement) {
- try {
- console.log(`🤖 正在为红色PPT生成要点标题: ${section.title}`)
- // 使用getTitleForElement函数获取正确的标题,与通用类PPT保持一致
- const titleForElement = getTitleForElement(outlineData, slideIndex, sectionIndex)
- // 检查是否需要AI生成标题
- if (titleForElement.includes('待AI生成子小节')) {
- // 需要AI生成子小节标题
- console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
- // 获取小节标题用于AI生成
- const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, sectionIndex)
- // 根据不同的标记生成不同的提示词
- let prompt = ''
- if (titleForElement.includes('待AI生成子小节1')) {
- prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第一个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
- } else if (titleForElement.includes('待AI生成子小节2')) {
- prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第二个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
- } else if (titleForElement.includes('待AI生成子小节3')) {
- prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第三个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
- } else if (titleForElement.includes('待AI生成子小节4')) {
- prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第四个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
- } else {
- prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成一个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
- }
- // 调用AI生成单个子小节标题(为当前元素生成)
- const aiResponse = await apis.reProduceSingleQuestion({
- message: prompt
- })
- if (aiResponse && aiResponse.data) {
- const aiTitle = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 红色PPT子小节标题生成完成: ${aiTitle}`)
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${aiTitle}</span></strong></p>`
- } else {
- // AI调用失败,使用备用标题
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${section.title}</span></strong></p>`
- console.log(`🔄 使用备用标题: ${section.title}`)
- }
- } else {
- // 直接使用获取到的标题
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${titleForElement}</span></strong></p>`
- console.log(`✅ 使用现有标题: ${titleForElement}`)
- }
- } catch (aiError) {
- console.error(`❌ AI生成红色PPT要点标题失败:`, aiError)
- // AI生成失败,使用备用标题
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${section.title}</span></strong></p>`
- console.log(`🔄 使用备用标题: ${section.title}`)
- }
- }
- // 填充要点内容(使用AI生成)
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${sectionIndex + 1}`)
- if (itemContentElement) {
- try {
- console.log(`🤖 正在为红色PPT生成要点内容: ${section.title}`)
- // 使用getTitleForAIContent函数获取正确的内容标题,与通用类PPT保持一致
- const titleForAI = getTitleForAIContent(outlineData, slideIndex, sectionIndex)
- // 调用AI API生成要点内容
- const aiResponse = await apis.reProduceSingleQuestion({
- message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${outlineTitle.value}"的PPT演示文稿,当前章节是"${titleForAI}"`
- })
- if (aiResponse && aiResponse.data) {
- const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
- console.log(`✅ 红色PPT要点内容生成完成: ${aiContent}`)
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${aiContent}</span></p>`
- } else {
- // AI调用失败,使用备用内容
- const fallbackContent = section.subsections && section.subsections.length > 0
- ? section.subsections.map(sub => sub.title).join('、')
- : section.content || '详细说明内容'
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${fallbackContent}</span></p>`
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- }
- } catch (aiError) {
- console.error(`❌ AI生成红色PPT要点内容失败:`, aiError)
- // AI生成失败,使用备用内容
- const fallbackContent = section.subsections && section.subsections.length > 0
- ? section.subsections.map(sub => sub.title).join('、')
- : section.content || '详细说明内容'
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${fallbackContent}</span></p>`
- console.log(`🔄 使用备用内容: ${fallbackContent}`)
- }
- }
- }
- // 隐藏多余的要点
- for (let sectionIndex = sections.length; sectionIndex < 3; sectionIndex++) {
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${sectionIndex + 1}`)
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${sectionIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;"></span></strong></p>`
- }
- if (itemContentElement) {
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;"></span></p>`
- }
- }
- slideIndex++
- }
- }
- // 处理结束页
- const endSlide = pptData.find(slide => slide.type === 'end')
- if (endSlide) {
- const endTitleElement = endSlide.elements.find(el => el.id === 'end-title')
- if (endTitleElement) {
- 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>`
- console.log('结束页标题已填充')
- }
- const endSubtitleElement = endSlide.elements.find(el => el.id === 'end-subtitle')
- if (endSubtitleElement) {
- endSubtitleElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: rgba(255,255,255,0.9);">感谢您的时间与关注</span></p>`
- console.log('结束页副标题已填充')
- }
- }
- console.log('静态模板内容填充完成')
- } catch (error) {
- console.error('填充静态模板内容失败:', error)
- throw error
- }
- }
- // 切换预览模式
- const togglePreviewMode = () => {
- previewMode.value = previewMode.value === 'edit' ? 'preview' : 'edit'
- console.log('切换预览模式:', previewMode.value)
- if (previewMode.value === 'preview') {
- ElMessage.success('已切换到预览模式')
- } else {
- ElMessage.success('已切换到编辑模式')
- }
- }
- // PPT预览相关方法
- const getCurrentPPTSlide = () => {
- const slide = generatedPPT.value[currentPPTSlideIndex.value] || null
- console.log('获取当前PPT幻灯片:', slide)
- console.log('当前索引:', currentPPTSlideIndex.value)
- console.log('总幻灯片数:', generatedPPT.value.length)
- return slide
- }
- // 获取大图显示的内容(在动画过程中显示"正在生成...")
- const getCurrentPPTSlideForMainView = () => {
- const slide = generatedPPT.value[currentPPTSlideIndex.value] || null
- if (!slide) return null
- // 如果正在生成中,显示"正在生成..."的内容
- if (isGeneratingTrainingMaterial.value && slide.elements) {
- const generatingSlide = JSON.parse(JSON.stringify(slide))
- generatingSlide.elements = generatingSlide.elements.map(el => ({
- ...el,
- content: (el.textType === 'itemContent' && el.content && el.content.includes('待AI填充')) ?
- `<p style="text-align: center;"><span style="font-size: 16px; color: ${el.defaultColor};">正在生成...</span></p>` :
- el.content
- }))
- return generatingSlide
- }
- return slide
- }
- const getCurrentPPTSlideBackground = () => {
- const slide = getCurrentPPTSlideForMainView()
- if (!slide) return '#ffffff'
- if (slide.background?.type === 'gradient') {
- const gradient = slide.background.gradient
- if (gradient.type === 'linear') {
- const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
- return `linear-gradient(135deg, ${colors})`
- } else if (gradient.type === 'radial') {
- const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
- return `radial-gradient(circle, ${colors})`
- }
- }
- return slide.background?.color || '#667eea'
- }
- // 获取幻灯片背景(用于缩略图)
- const getSlideBackground = (slide) => {
- if (!slide) return '#667eea'
- // 检查是否有背景设置
- if (slide.background) {
- if (slide.background.type === 'gradient') {
- const gradient = slide.background.gradient
- if (gradient.type === 'linear') {
- const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
- return `linear-gradient(135deg, ${colors})`
- } else if (gradient.type === 'radial') {
- const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
- return `radial-gradient(circle, ${colors})`
- }
- } else if (slide.background.color) {
- // 确保白色背景能正确显示
- return slide.background.color
- }
- }
- // 如果没有背景设置,检查是否有白色背景的元素
- if (slide.elements && slide.elements.length > 0) {
- const whiteElement = slide.elements.find(el =>
- el.type === 'shape' && el.fill === '#ffffff' ||
- el.type === 'shape' && el.fill === 'white'
- )
- if (whiteElement) {
- return '#ffffff'
- }
- }
- return '#667eea'
- }
- // 获取缩略图元素样式
- const getThumbnailElementStyle = (element) => {
- // 获取当前根字体大小,用于响应amfe-flexible缩放
- const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
- const baseFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
- const scaleFactor = rootFontSize / baseFontSize
- // 使用响应式的缩放比例计算,与主图保持一致
- const scaleX = (1105 * scaleFactor) / 960
- const scaleY = (603 * scaleFactor) / 540
- const style = {
- position: 'absolute',
- left: ((element.left || element.x || 0) * scaleX) + 'px',
- top: ((element.top || element.y || 0) * scaleY) + 'px',
- width: ((element.width || 100) * scaleX) + 'px',
- height: ((element.height || 50) * scaleY) + 'px',
- zIndex: element.zIndex || 1
- }
- // 添加与主图相同的样式属性
- if (element.defaultColor) {
- style.color = element.defaultColor
- }
- if (element.defaultFontName) {
- style.fontFamily = element.defaultFontName
- }
- // 添加透明度支持
- if (element.opacity !== undefined) {
- style.opacity = element.opacity
- }
- // 处理形状元素的背景色
- if (element.type === 'shape' && element.fill) {
- style.backgroundColor = element.fill
- style.borderRadius = element.viewBox ? '8px' : '0'
- }
- // 处理文本元素的对齐方式
- if (element.type === 'text' && element.content) {
- // 从HTML内容中提取text-align样式
- const textAlignMatch = element.content.match(/text-align:\s*([^;]+)/)
- if (textAlignMatch) {
- style.textAlign = textAlignMatch[1].trim()
- }
- // 特殊处理目录项:确保居中显示
- if (element.textType === 'item' && element.content.includes('目录项')) {
- style.textAlign = 'center'
- }
- }
- return style
- }
- // 获取响应式字体大小
- const getResponsiveFontSize = (baseFontSize) => {
- const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
- const baseRootFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
- const scaleFactor = rootFontSize / baseRootFontSize
- return Math.round(baseFontSize * scaleFactor)
- }
- // 监听窗口大小变化,重新计算PPT元素样式
- const handleWindowResize = () => {
- // 强制重新渲染PPT预览
- if (generatedPPT.value.length > 0) {
- // 触发响应式更新
- generatedPPT.value = [...generatedPPT.value]
- }
- }
- // 添加窗口大小变化监听器
- onMounted(() => {
- window.addEventListener('resize', handleWindowResize)
- // 自动启动下载监听(因为默认选中) - 注释掉,因为版本没有步骤四
- // if (isDownloadListenerActive.value) {
- // startDownloadListener()
- // }
- })
- onUnmounted(() => {
- window.removeEventListener('resize', handleWindowResize)
- // 清理下载监听插件 - 注释掉,因为版本没有步骤四
- // if (isDownloadListenerActive.value) {
- // stopDownloadListener()
- // }
- })
- const getElementStyle = (element) => {
- // 获取当前根字体大小,用于响应amfe-flexible缩放
- const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
- const baseFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
- const scaleFactor = rootFontSize / baseFontSize
- // 使用响应式的目标尺寸,考虑amfe-flexible缩放
- const targetWidth = 1105 * scaleFactor // slide-preview 的 max-width
- const targetHeight = 603 * scaleFactor // slide-preview 的 height
- // 原始PPT尺寸 (960x540)
- const originalWidth = 960
- const originalHeight = 540
- // 计算响应式的缩放比例
- const scaleX = targetWidth / originalWidth
- const scaleY = targetHeight / originalHeight
- return {
- position: 'absolute',
- left: ((element.left || element.x) * scaleX) + 'px',
- top: ((element.top || element.y) * scaleY) + 'px',
- width: (element.width * scaleX) + 'px',
- height: (element.height * scaleY) + 'px',
- zIndex: element.zIndex || 1
- }
- }
- const getElementTypeName = (type) => {
- const typeNames = {
- 'text': '文本',
- 'image': '图片',
- 'shape': '形状'
- }
- return typeNames[type] || type
- }
- const getShapeStyle = (element) => {
- return {
- width: '100%',
- height: '100%',
- backgroundColor: element.fill,
- borderRadius: element.viewBox ? '8px' : '0',
- opacity: element.opacity || 1
- }
- }
- const selectPPTElement = (index) => {
- selectedPPTElementIndex.value = index
- console.log('选中PPT元素:', index)
- }
- const handleDoubleClick = (index) => {
- editingPPTElementIndex.value = index
- const element = getCurrentPPTSlide().elements[index]
- editingPPTHtml.value = element.content
- console.log('开始编辑PPT元素:', index)
- }
- // 禁用的双击处理函数(PPT预览模式下不允许文字编辑)
- const handleDoubleClickDisabled = (index) => {
- console.log('PPT预览模式下不允许文字编辑')
- ElMessage.info('PPT预览模式下不允许编辑文字,只能更换图片')
- }
- const onPPTInlineInput = (event) => {
- editingPPTHtml.value = event.target.innerHTML
- }
- const savePPTInlineEdit = (index) => {
- const slide = getCurrentPPTSlide()
- if (slide.elements[index]) {
- slide.elements[index].content = editingPPTHtml.value
- console.log('文本编辑保存成功')
- ElMessage.success('文本编辑已保存')
- // 自动保存修改后的PPT数据
- saveModifiedPPTData()
- }
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- console.log('保存PPT元素编辑:', index)
- }
- const startPPTDrag = (event, index) => {
- // 如果是双击事件,不启动拖拽
- if (event.detail >= 2) {
- return
- }
- // 获取当前元素
- const slide = getCurrentPPTSlide()
- const element = slide.elements[index]
- // PPT预览模式下,文字元素不允许拖拽
- if (element.type === 'text') {
- console.log('PPT预览模式下,文字元素不允许拖拽')
- return
- }
- // 如果当前正在编辑文字元素,不启动拖拽
- if (editingPPTElementIndex.value === index && element.type === 'text' && editingPPTHtml.value !== '') {
- return
- }
- if (selectedPPTElementIndex.value !== index) {
- selectPPTElement(index)
- }
- // 开始拖拽
- const startX = event.clientX
- const startY = event.clientY
- const startLeft = element.left
- const startTop = element.top
- const onMouseMove = (moveEvent) => {
- const deltaX = moveEvent.clientX - startX
- const deltaY = moveEvent.clientY - startY
- element.left = startLeft + deltaX
- element.top = startTop + deltaY
- }
- const onMouseUp = () => {
- document.removeEventListener('mousemove', onMouseMove)
- document.removeEventListener('mouseup', onMouseUp)
- // 自动保存修改
- saveModifiedPPTData()
- console.log('拖拽完成,已自动保存')
- }
- document.addEventListener('mousemove', onMouseMove)
- document.addEventListener('mouseup', onMouseUp)
- event.preventDefault()
- }
- const startPPTResize = (event, index, direction) => {
- if (selectedPPTElementIndex.value !== index) {
- selectPPTElement(index)
- }
- const slide = getCurrentPPTSlide()
- const element = slide.elements[index]
- // PPT预览模式下,文字元素不允许调整大小
- if (element.type === 'text') {
- console.log('PPT预览模式下,文字元素不允许调整大小')
- return
- }
- const startX = event.clientX
- const startY = event.clientY
- const startWidth = element.width
- const startHeight = element.height
- const startLeft = element.left
- const startTop = element.top
- const onMouseMove = (moveEvent) => {
- const deltaX = moveEvent.clientX - startX
- const deltaY = moveEvent.clientY - startY
- switch (direction) {
- case 'se': // 右下角
- element.width = Math.max(20, startWidth + deltaX)
- element.height = Math.max(20, startHeight + deltaY)
- break
- case 'sw': // 左下角
- element.width = Math.max(20, startWidth - deltaX)
- element.height = Math.max(20, startHeight + deltaY)
- element.left = startLeft + (startWidth - element.width)
- break
- case 'ne': // 右上角
- element.width = Math.max(20, startWidth + deltaX)
- element.height = Math.max(20, startHeight - deltaY)
- element.top = startTop + (startHeight - element.height)
- break
- case 'nw': // 左上角
- element.width = Math.max(20, startWidth - deltaX)
- element.height = Math.max(20, startHeight - deltaY)
- element.left = startLeft + (startWidth - element.width)
- element.top = startTop + (startHeight - element.height)
- break
- }
- }
- const onMouseUp = () => {
- document.removeEventListener('mousemove', onMouseMove)
- document.removeEventListener('mouseup', onMouseUp)
- // 自动保存修改
- saveModifiedPPTData()
- console.log('缩放完成,已自动保存')
- }
- document.addEventListener('mousemove', onMouseMove)
- document.addEventListener('mouseup', onMouseUp)
- event.preventDefault()
- }
- // 将图片文件转换为base64
- const convertFileToBase64 = (file) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onload = () => {
- const result = reader.result
- // 验证base64数据
- if (!result || typeof result !== 'string') {
- reject(new Error('图片读取结果无效'))
- return
- }
- if (!result.startsWith('data:image/')) {
- reject(new Error('不是有效的图片数据'))
- return
- }
- if (!result.includes(',')) {
- reject(new Error('Base64数据格式错误'))
- return
- }
- const [, data] = result.split(',')
- if (!data || data.length < 100) {
- reject(new Error('Base64数据过短'))
- return
- }
- console.log('图片转换成功:', {
- fileName: file.name,
- fileSize: file.size,
- base64Length: result.length,
- dataLength: data.length
- })
- resolve(result)
- }
- reader.onerror = () => {
- reject(new Error('图片读取失败: ' + reader.error?.message))
- }
- reader.readAsDataURL(file)
- })
- }
- // 高质量图片转换函数(使用Canvas保持原始质量)
- const convertFileToHighQualityBase64 = (file, targetWidth = null, targetHeight = null) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onload = () => {
- const img = new Image()
- img.onload = () => {
- try {
- // 创建Canvas
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- let canvasWidth, canvasHeight
- if (targetWidth && targetHeight) {
- // 如果有目标尺寸,使用cover模式(填满目标区域,超出部分裁剪)
- canvasWidth = targetWidth
- canvasHeight = targetHeight
- } else {
- // 没有目标尺寸,保持原始尺寸
- canvasWidth = img.width
- canvasHeight = img.height
- }
- // 设置Canvas尺寸
- canvas.width = canvasWidth
- canvas.height = canvasHeight
- // 设置高质量渲染
- ctx.imageSmoothingEnabled = true
- ctx.imageSmoothingQuality = 'high'
- // 绘制图片到Canvas,使用cover模式(填满目标区域,超出部分裁剪)
- if (targetWidth && targetHeight) {
- // 计算cover模式的缩放比例和偏移
- const aspectRatio = img.width / img.height
- const targetAspectRatio = targetWidth / targetHeight
- let sourceWidth, sourceHeight, sourceX, sourceY
- if (aspectRatio > targetAspectRatio) {
- // 图片更宽,以高度为准,裁剪左右
- sourceHeight = img.height
- sourceWidth = img.height * targetAspectRatio
- sourceX = (img.width - sourceWidth) / 2
- sourceY = 0
- } else {
- // 图片更高,以宽度为准,裁剪上下
- sourceWidth = img.width
- sourceHeight = img.width / targetAspectRatio
- sourceX = 0
- sourceY = (img.height - sourceHeight) / 2
- }
- // 绘制裁剪后的图片到目标尺寸
- ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, canvasWidth, canvasHeight)
- } else {
- // 没有目标尺寸,直接绘制原始图片
- ctx.drawImage(img, 0, 0)
- }
- // 转换为base64,使用最高质量
- const base64 = canvas.toDataURL('image/png', 1.0) // 使用PNG格式,质量1.0
- console.log('高质量图片转换成功:', {
- fileName: file.name,
- fileSize: file.size,
- originalSize: `${img.width}x${img.height}`,
- canvasSize: `${canvasWidth}x${canvasHeight}`,
- targetSize: targetWidth && targetHeight ? `${targetWidth}x${targetHeight}` : 'none',
- base64Length: base64.length,
- format: 'PNG'
- })
- resolve(base64)
- } catch (error) {
- console.error('Canvas转换失败,使用原始方法:', error)
- // 如果Canvas转换失败,回退到原始方法
- resolve(reader.result)
- }
- }
- img.onerror = () => {
- console.error('图片加载失败,使用原始方法')
- // 如果图片加载失败,回退到原始方法
- resolve(reader.result)
- }
- img.src = reader.result
- }
- reader.onerror = () => {
- reject(new Error('图片读取失败: ' + reader.error?.message))
- }
- reader.readAsDataURL(file)
- })
- }
- const changePPTImage = async (index) => {
- console.log('更换PPT图片被触发,索引:', index)
- const input = document.createElement('input')
- input.type = 'file'
- input.accept = 'image/*'
- input.onchange = async (event) => {
- const file = event.target.files[0]
- if (file) {
- try {
- // 检查文件大小(限制为5MB)
- const maxSize = 5 * 1024 * 1024 // 5MB
- if (file.size > maxSize) {
- ElMessage.error('图片大小不能超过5MB')
- return
- }
- // 显示处理进度
- ElMessage.info('正在处理图片...')
- // 获取当前图片元素的目标尺寸
- const slide = getCurrentPPTSlide()
- const element = slide.elements[index]
- const targetWidth = element.width
- const targetHeight = element.height
- console.log('图片目标尺寸:', { targetWidth, targetHeight })
- // 将图片转换为高质量base64,保持宽高比
- const base64Data = await convertFileToHighQualityBase64(file, targetWidth, targetHeight)
- console.log('图片转换为高质量base64成功,长度:', base64Data.length)
- // 调试信息:显示base64数据的前100个字符
- console.log('Base64数据预览:', base64Data.substring(0, 100) + '...')
- // 更新PPT元素
- if (element && element.type === 'image') {
- element.src = base64Data
- console.log('图片更换成功,使用base64数据')
- ElMessage.success('图片更换成功')
- // 自动保存修改后的PPT数据
- saveModifiedPPTData()
- // 立即保存到后端(不更新封面图)
- try {
- console.log('开始保存图片更换后的PPT数据到后端...')
- await saveStepToBackend(false) // 传入false,不更新封面图
- console.log('图片更换后的PPT数据已保存到后端')
- // 立即更新历史记录列表中的数据
- const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
- if (currentHistoryItem) {
- currentHistoryItem.ppt_json_content = JSON.stringify(generatedPPT.value)
- console.log('已更新历史记录列表中的PPT数据')
- }
- } catch (error) {
- console.error('保存图片更换后的PPT数据失败:', error)
- ElMessage.warning('图片更换成功,但保存到后端失败')
- }
- }
- } catch (error) {
- console.error('图片处理过程中发生错误:', error)
- ElMessage.error('图片处理失败: ' + error.message)
- }
- }
- }
- input.click()
- }
- // 上传图片到服务器
- const uploadImageToServer = async (file) => {
- try {
- console.log('开始上传图片到服务器...')
- // 创建FormData,后端需要的是 'image' 字段
- const formData = new FormData()
- formData.append('image', file)
- // 使用正确的上传接口
- const response = await apis.uploadImage(formData)
- console.log('图片上传响应:', response)
- if (response.statusCode === 200) {
- console.log('图片上传成功:', response.fileUrl)
- return {
- statusCode: 200,
- fileUrl: response.fileUrl,
- message: '上传成功'
- }
- } else {
- console.error('图片上传失败:', response)
- return {
- statusCode: response.statusCode || 500,
- message: response.message || '上传失败'
- }
- }
- } catch (error) {
- console.error('图片上传过程中发生错误:', error)
- return {
- statusCode: 500,
- message: error.message
- }
- }
- }
- // 处理图片选择
- const handleImageSelect = (event) => {
- const file = event.target.files[0]
- if (!file) return
- // 检查文件类型
- if (!file.type.startsWith('image/')) {
- ElMessage.warning('请选择图片文件')
- event.target.value = ''
- return
- }
- // 检查文件大小(5MB限制)
- const maxSize = 5 * 1024 * 1024
- if (file.size > maxSize) {
- ElMessage.warning('图片大小不能超过5MB')
- event.target.value = ''
- return
- }
- try {
- // 创建本地URL
- const imageUrl = URL.createObjectURL(file)
- // 更新选中的图片元素
- if (selectedImageIndex.value !== null && generatedPPT.value.length > 0) {
- const currentSlide = getCurrentPPTSlide()
- if (currentSlide && currentSlide.elements[selectedImageIndex.value]) {
- currentSlide.elements[selectedImageIndex.value].src = imageUrl
- console.log('图片已更换:', imageUrl)
- ElMessage.success('图片更换成功')
- }
- }
- // 清理选中的图片索引
- selectedImageIndex.value = null
- } catch (error) {
- console.error('图片更换失败:', error)
- ElMessage.error('图片更换失败,请重试')
- } finally {
- event.target.value = ''
- }
- }
- // 将大纲数据转换为AIPPT.json格式
- const convertOutlineToAIPPT = (outlineData, outlineTitle) => {
- try {
- console.log('开始转换大纲为AIPPT.json格式')
- console.log('输入大纲数据:', outlineData)
- console.log('标题:', outlineTitle)
- const aipptSlides = []
- // 1. 添加封面页
- aipptSlides.push({
- type: 'cover',
- data: {
- title: outlineTitle || '安全培训大纲',
- text: '基于AI生成的培训大纲,包含相关内容'
- }
- })
- console.log('已添加封面页')
- // 2. 生成目录项
- const tocItems = outlineData.map((chapter, index) => chapter.title)
- console.log('目录项:', tocItems)
- // 添加目录页
- aipptSlides.push({
- type: 'contents',
- data: {
- items: tocItems
- }
- })
- console.log('已添加目录页')
- // 3. 遍历章节生成幻灯片
- outlineData.forEach((chapter, chapterIndex) => {
- console.log(`处理章节 ${chapterIndex + 1}:`, chapter.title)
- console.log(`章节内容:`, chapter)
- // 添加章节过渡页
- aipptSlides.push({
- type: 'transition',
- data: {
- title: chapter.title,
- text: chapter.content || `本章将介绍${chapter.title}的相关内容`
- }
- })
- console.log(`已添加章节 ${chapterIndex + 1} 的过渡页`)
- // 遍历小节,每个小节生成一个内容页(恢复昨天的逻辑)
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- console.log(`处理小节 ${sectionIndex + 1}:`, section.title)
- console.log(`小节内容:`, section)
- // 添加内容页
- let contentItems = []
- // 添加小节内容(保持昨天的逻辑,让AI填充)
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- contentItems.push({
- title: '',
- text: ''
- })
- })
- } else {
- // 如果没有子小节,使用小节的内容
- contentItems.push({
- title: '',
- text: ''
- })
- }
- // 确保每个content页面至少有4个items(参考AIPPT.json格式)
- while (contentItems.length < 4) {
- // 添加空的items,让AI填充
- contentItems.push({
- title: '',
- text: ''
- })
- }
- // 限制最多4个items(与AIPPT.json保持一致)
- contentItems = contentItems.slice(0, 4)
- aipptSlides.push({
- type: 'content',
- data: {
- title: section.title,
- items: contentItems
- }
- })
- console.log(`已添加小节 ${sectionIndex + 1} 的内容页`)
- })
- } else {
- // 如果章节没有小节,添加空的内容页
- const emptyContentItems = [
- {
- title: '',
- text: ''
- },
- {
- title: '',
- text: ''
- },
- {
- title: '',
- text: ''
- },
- {
- title: '',
- text: ''
- }
- ]
- aipptSlides.push({
- type: 'content',
- data: {
- title: chapter.title,
- items: emptyContentItems
- }
- })
- console.log(`已添加章节 ${chapterIndex + 1} 的空内容页`)
- }
- })
- // 添加结束页
- aipptSlides.push({
- type: 'end'
- })
- console.log('转换完成,生成的AIPPT.json格式数据:')
- console.log(JSON.stringify(aipptSlides, null, 2))
- // 统计各类型幻灯片数量
- const slideTypes = {}
- aipptSlides.forEach(slide => {
- slideTypes[slide.type] = (slideTypes[slide.type] || 0) + 1
- })
- console.log('幻灯片类型统计:', slideTypes)
- console.log('总幻灯片数量:', aipptSlides.length)
- return aipptSlides
- } catch (error) {
- console.error('转换大纲为AIPPT.json格式失败:', error)
- return []
- }
- }
- // 加载AIPPT.json数据
- const loadAIPPTData = async (convertedAIPPTData = null, shouldSaveStep = false) => {
- try {
- console.log('开始加载template5数据...')
- // 使用导入的template5.json数据
- console.log('template5.json数据加载成功:', template5JsonData)
- // 根据当前大纲数据生成AIPPT格式的数据
- let aipptData
- if (convertedAIPPTData) {
- // 使用传入的转换后的AIPPT数据
- aipptData = convertedAIPPTData
- console.log('使用传入的AIPPT数据:', aipptData)
- } else if (outlineData.value && outlineData.value.length > 0) {
- // 使用当前大纲数据生成AIPPT格式
- aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
- console.log('根据大纲生成的AIPPT数据:', aipptData)
- } else {
- // 如果没有大纲数据,使用导入的默认AIPPT.json数据
- aipptData = aipptJsonData
- console.log('加载默认AIPPT.json数据:', aipptData)
- }
- // 将AIPPT.json数据与template5模板结合
- const convertedSlides = aipptData.map((slide, slideIndex) => {
- // 根据slide.type找到对应的template5模板
- const templateSlide = template5JsonData.find(t => t.type === slide.type)
- if (templateSlide) {
- // 复制模板结构,包括背景、图片、形状等
- const newSlide = JSON.parse(JSON.stringify(templateSlide))
- // 更新幻灯片ID
- newSlide.id = `generated-slide-${slideIndex}`
- // 根据不同类型填充内容
- switch (slide.type) {
- case 'cover':
- newSlide.elements.forEach(element => {
- if (element.textType === 'title') {
- 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>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: ${element.defaultColor};">${slide.data.text}</span></p>`
- }
- })
- break
- case 'contents':
- if (slide.data.items) {
- let itemIndex = 0
- newSlide.elements.forEach(element => {
- if (element.textType === 'item' && itemIndex < slide.data.items.length) {
- const itemText = slide.data.items[itemIndex]
- element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${itemIndex + 1}. ${itemText}</span></p>`
- itemIndex++
- }
- })
- }
- break
- case 'content':
- newSlide.elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: ${element.defaultColor};">${slide.data.title}</span></strong></p>`
- }
- })
- if (slide.data.items && Array.isArray(slide.data.items)) {
- let itemIndex = 0
- newSlide.elements.forEach(element => {
- if (element.textType === 'itemTitle' && itemIndex < slide.data.items.length) {
- const item = slide.data.items[itemIndex]
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: ${element.defaultColor};">${item.title}</span></strong></p>`
- } else if (element.textType === 'itemContent' && itemIndex < slide.data.items.length) {
- const item = slide.data.items[itemIndex]
- element.content = `<p style="text-align: center;"><span style="font-size: 14px; color: ${element.defaultColor};">${item.text}</span></p>`
- itemIndex++
- }
- })
- }
- break
- case 'transition':
- newSlide.elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${slide.data.title}</span></strong></p>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${slide.data.text}</span></p>`
- }
- })
- break
- case 'end':
- newSlide.elements.forEach(element => {
- if (element.textType === 'title') {
- element.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: ${element.defaultColor};">谢谢聆听</span></strong></p>`
- } else if (element.textType === 'content') {
- element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">感谢您的时间与关注</span></p>`
- }
- })
- break
- }
- return newSlide
- } else {
- // 如果没有找到对应的模板,使用默认模板
- console.warn(`未找到类型为 ${slide.type} 的模板,使用默认模板`)
- return {
- id: `generated-slide-${slideIndex}`,
- type: slide.type,
- elements: [{
- id: `title-${slideIndex}`,
- type: 'text',
- content: `<h1 style="text-align: center; font-size: 48px; color: #1F2937;">${slide.data.title || '标题'}</h1>`,
- left: 100,
- top: 150,
- width: 760,
- height: 100,
- defaultColor: '#1F2937',
- defaultFontName: 'Arial',
- zIndex: 1
- }],
- background: { color: '#ffffff' }
- }
- }
- })
- // 直接设置生成的PPT数据
- generatedPPT.value = convertedSlides
- console.log('已设置生成的PPT数据:', generatedPPT.value.length, '张幻灯片')
- isPPTPreviewMode.value = true
- currentPPTSlideIndex.value = 0
- console.log('AIPPT数据与template5模板结合完成:', generatedPPT.value)
- console.log('PPT预览模式已启用:', isPPTPreviewMode.value)
- console.log('当前PPT幻灯片索引:', currentPPTSlideIndex.value)
- console.log('生成的PPT数据长度:', generatedPPT.value.length)
- // 强制触发Vue响应式更新
- await nextTick()
- console.log('Vue响应式更新完成')
- // 只有在需要保存步骤时才调用保存API
- if (shouldSaveStep) {
- console.log('需要保存步骤信息到后端')
- await saveStepToBackend(true, true) // 强制保存新生成的PPT
- } else {
- console.log('跳过保存步骤信息,仅加载PPT数据')
- }
- // 确保所有数据处理完成
- return Promise.resolve()
- } catch (error) {
- console.error('加载AIPPT.json或template5.json失败:', error)
- // 如果加载失败,使用默认数据
- generateDefaultPPTData()
- return Promise.reject(error)
- }
- }
- // 生成默认PPT数据(备用方案)
- const generateDefaultPPTData = () => {
- generatedPPT.value = [
- {
- id: 'slide-1',
- background: '#ffffff',
- elements: [
- {
- id: 'title-1',
- type: 'text',
- textType: 'title',
- content: '<p style="text-align: center;"><strong><span style="font-size: 48px; color: #1F2937;">犯罪心理学研究</span></strong></p>',
- x: 100,
- y: 100,
- width: 600,
- height: 100,
- defaultColor: '#1F2937',
- defaultFontName: 'Arial',
- zIndex: 1
- },
- {
- id: 'content-1',
- type: 'text',
- textType: 'content',
- content: '<p style="text-align: center;"><span style="font-size: 20px; color: #6B7280;">探索犯罪心理的成因、特征及干预策略</span></p>',
- x: 100,
- y: 250,
- width: 600,
- height: 50,
- defaultColor: '#6B7280',
- defaultFontName: 'Arial',
- zIndex: 1
- }
- ]
- }
- ]
- isPPTPreviewMode.value = true
- currentPPTSlideIndex.value = 0
- console.log('默认PPT数据生成完成:', generatedPPT.value)
- }
- // 步骤四相关方法
- const selectDownloadOption = (index) => {
- selectedDownloadOption.value = index
- console.log('选择下载选项:', downloadOptions.value[index].title)
- }
- const downloadNow = () => {
- console.log('立即下载:', downloadOptions.value[selectedDownloadOption.value].title)
- // 这里可以添加下载逻辑
- }
- const goBackToTemplate = () => {
- showDownloadOptions.value = false
- selectedDownloadOption.value = 0
- // 清理PPT预览相关状态
- generatedPPT.value = []
- currentPPTSlideIndex.value = 0
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- editingPPTHtml.value = ''
- zoom.value = 1
- selectedImageIndex.value = null
- // 重置为模板预览状态
- currentSlideIndex.value = 0
- // 重新加载模板预览数据
- loadLocalPPT()
- updateSlideThumbnails()
- console.log('已回到模板选择页面,PPT预览状态已清理,模板预览已重新加载')
- }
- // 处理AI回复中的特殊字符和表情符号
- const processAIResponse = (text) => {
- if (!text) return text
- console.log('原始AI回复:', text)
- console.log('原始文本长度:', text.length)
- console.log('原始文本字符码:', Array.from(text).map(char => char.charCodeAt(0)))
- try {
- // 方法1:尝试直接解码
- if (text.includes('%')) {
- const decoded = decodeURIComponent(text)
- console.log('URL解码后:', decoded)
- return decoded
- }
- // 方法2:处理可能的编码问题
- if (text.includes('??')) {
- // 如果包含问号,尝试修复编码
- const cleaned = text.replace(/\?\?/g, '')
- console.log('清理问号后:', cleaned)
- return cleaned
- }
- // 方法3:处理Unicode转义序列
- if (text.includes('\\u')) {
- const unicodeDecoded = text.replace(/\\u[\dA-F]{4}/gi, (match) => {
- return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
- })
- console.log('Unicode解码后:', unicodeDecoded)
- return unicodeDecoded
- }
- // 方法4:处理HTML实体
- if (text.includes('&')) {
- const textarea = document.createElement('textarea')
- textarea.innerHTML = text
- const htmlDecoded = textarea.value
- console.log('HTML解码后:', htmlDecoded)
- return htmlDecoded
- }
- console.log('无需特殊处理,直接返回')
- return text
- } catch (error) {
- console.warn('字符编码处理失败:', error)
- return text
- }
- }
- // 安全的HTML清理函数
- const sanitizeHtml = (html) => {
- if (!html) return html
- // 只允许安全的HTML标签
- const allowedTags = {
- 'br': {},
- 'strong': {},
- 'em': {},
- 'h1': {},
- 'h2': {},
- 'h3': {},
- 'h4': {},
- 'h5': {},
- 'h6': {},
- 'ul': {},
- 'li': {},
- 'code': {}
- }
- // 创建临时DOM元素来清理HTML
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = html
- // 递归清理节点
- const cleanNode = (node) => {
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent
- }
- if (node.nodeType === Node.ELEMENT_NODE) {
- const tagName = node.tagName.toLowerCase()
- // 只保留允许的标签
- if (allowedTags[tagName]) {
- const cleanElement = document.createElement(tagName)
- // 递归处理子节点
- for (let child of node.childNodes) {
- const cleanChild = cleanNode(child)
- if (cleanChild) {
- if (typeof cleanChild === 'string') {
- cleanElement.appendChild(document.createTextNode(cleanChild))
- } else {
- cleanElement.appendChild(cleanChild)
- }
- }
- }
- return cleanElement.outerHTML
- } else {
- // 不允许的标签,只保留文本内容
- let textContent = ''
- for (let child of node.childNodes) {
- textContent += cleanNode(child) || ''
- }
- return textContent
- }
- }
- return ''
- }
- // 清理整个HTML
- let cleanHtml = ''
- for (let child of tempDiv.childNodes) {
- cleanHtml += cleanNode(child) || ''
- }
- return cleanHtml
- }
- // 将Markdown格式转换为HTML格式
- const markdownToHtml = (text) => {
- if (!text) return text
- console.log('开始转换Markdown:', text)
- let html = text
- // 清理可能的HTML标签残留
- html = html.replace(/<\/?[^>]*>/g, '')
- console.log('清理HTML标签后:', html)
- // 处理加粗文本 **text** -> <strong>text</strong>
- html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- // 处理斜体文本 *text* -> <em>text</em>
- html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
- // 处理标题 - 支持行首的标题(井号后可以有空格也可以没有空格)
- html = html.replace(/^#{1,6}\s*(.+?)$/gm, (match, content) => {
- console.log('标题匹配:', match, '内容:', content)
- // 检查井号的数量来确定标题级别
- const hashCount = (match.match(/#/g) || []).length
- const level = Math.min(hashCount, 6)
- const result = `<h${level}>${content.trim()}</h${level}>`
- console.log('标题转换结果:', result)
- return result
- })
- // 处理列表项 - 主列表项用2空格缩进,嵌套列表项用8空格缩进
- html = html.replace(/^- (.*?)$/gm, (match, content) => {
- // 检查是否已经有缩进(嵌套列表)
- if (match.startsWith(' - ') || match.startsWith(' - ')) {
- return ' ' + content // 8空格缩进
- }
- return ' ' + content // 2空格缩进
- })
- // 处理数字列表 1. text -> 主列表项用2空格缩进,嵌套列表项用8空格缩进
- html = html.replace(/^\d+\. (.*?)$/gm, (match, content) => {
- // 检查是否已经有缩进(嵌套列表)
- if (match.startsWith(' ') || match.startsWith(' ')) {
- return ' ' + content // 8空格缩进
- }
- return ' ' + content // 2空格缩进
- })
- // 处理换行符 - 在所有Markdown转换完成后进行
- html = html.replace(/\n/g, '<br>')
- // 处理<br>标签后的标题(换行符转换后)
- html = html.replace(/<br>#{1,6}\s*(.+?)(?=<br>|$)/g, (match, content) => {
- console.log('<br>标签后标题匹配:', match, '内容:', content)
- // 检查井号的数量来确定标题级别
- const hashCount = (match.match(/#/g) || []).length
- const level = Math.min(hashCount, 6)
- const result = `<br><h${level}>${content.trim()}</h${level}>`
- console.log('<br>标签后标题转换结果:', result)
- return result
- })
- // 处理代码块 ```code``` -> <code>code</code>
- html = html.replace(/```(.*?)```/gs, '<code>$1</code>')
- // 处理行内代码 `code` -> <code>code</code>
- html = html.replace(/`(.*?)`/g, '<code>$1</code>')
- // 最终清理:确保没有不完整的HTML标签
- html = html.replace(/<\/?[^>]*$/g, '')
- console.log('Markdown转换后:', html)
- // 使用安全的HTML清理函数
- const finalHtml = sanitizeHtml(html)
- console.log('最终清理后:', finalHtml)
- return finalHtml
- }
- // 复制功能
- const copyToClipboard = async (text) => {
- try {
- // 检查 Clipboard API 是否可用
- if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
- try {
- await navigator.clipboard.writeText(text)
- ElMessage.success('复制成功!')
- return
- } catch (clipboardError) {
- console.warn('Clipboard API 失败,使用降级方案:', clipboardError)
- }
- }
- // 降级方案:使用传统复制方法
- const textArea = document.createElement('textarea')
- textArea.value = text
- textArea.style.position = 'fixed'
- textArea.style.left = '-999999px'
- textArea.style.top = '-999999px'
- document.body.appendChild(textArea)
- textArea.focus()
- textArea.select()
- try {
- const successful = document.execCommand('copy')
- if (successful) {
- ElMessage.success('复制成功!')
- } else {
- throw new Error('execCommand 复制失败')
- }
- } catch (execError) {
- console.error('传统复制方法也失败:', execError)
- ElMessage.error('复制失败,请手动选择文本复制')
- } finally {
- document.body.removeChild(textArea)
- }
- } catch (err) {
- console.error('复制失败:', err)
- ElMessage.error('复制失败,请手动选择文本复制')
- }
- }
- // 复制用户消息
- const copyUserMessage = (message) => {
- copyToClipboard(message.content)
- }
- // 复制AI消息
- const copyAIMessage = (message) => {
- // 优先使用displayContent,如果没有则使用content
- const textToCopy = message.displayContent || message.content
- copyToClipboard(textToCopy)
- }
- // 点赞和点踩功能
- const handleThumbsUp = (message) => {
- console.log('点赞消息:', message.id)
- // 如果已经点赞,则取消点赞
- if (message.userFeedback === 'like') {
- message.userFeedback = null
- } else {
- // 设置点赞,取消点踩
- message.userFeedback = 'like'
- }
- // 可以在这里发送反馈给后端
- // sendFeedbackToBackend(message.id, message.userFeedback)
- }
- const handleThumbsDown = (message) => {
- console.log('点踩消息:', message.id)
- // 如果已经点踩,则取消点踩
- if (message.userFeedback === 'dislike') {
- message.userFeedback = null
- } else {
- // 设置点踩,取消点赞
- message.userFeedback = 'dislike'
- }
- // 可以在这里发送反馈给后端
- // sendFeedbackToBackend(message.id, message.userFeedback)
- }
- // 删除弹窗相关方法
- const handleDeleteClick = async (messageIndex) => {
- console.log('点击删除按钮,消息索引:', messageIndex)
- // 这里可以添加删除确认弹窗逻辑
- try {
- await ElMessageBox.confirm('确定要删除这条消息吗?', '确认删除', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
- chatMessages.value.splice(messageIndex, 1)
- } catch {
- // 用户取消删除
- }
- }
- // 文件输入框引用
- const fileInput = ref(null)
- // 图片输入框引用
- const imageInput = ref(null)
- // 缩略图区域引用
- const thumbnailStrip = ref(null)
- // 语音朗读相关方法
- const handleVoiceRead = (message) => {
- if (speakingMessageId.value === message.id) {
- // 如果正在朗读这条消息,则停止
- stopSpeaking()
- speakingMessageId.value = null
- } else {
- // 如果朗读其他消息,先停止当前朗读
- if (speakingMessageId.value) {
- stopSpeaking()
- }
- // 开始朗读新消息
- const textToRead = message.displayContent || message.content
- if (textToRead && textToRead.trim()) {
- // 清理HTML标签,只朗读纯文本
- const cleanText = textToRead.replace(/<[^>]*>/g, '')
- speakText(cleanText, {
- lang: 'zh-CN',
- rate: 0.9,
- pitch: 1,
- volume: 1
- })
- speakingMessageId.value = message.id
- }
- }
- }
- // Template8蓝色科技主题缩略图生成函数
- const generateTemplate8Thumbnails = () => {
- console.log('开始生成Template8蓝色科技主题缩略图')
- if (!generatedPPT.value || generatedPPT.value.length === 0) {
- console.error('无有效的PPT数据用于生成缩略图')
- return
- }
- // 为Template8使用实际的图片文件作为缩略图
- const template8Images = [
- template8Slide1, // 第1页 - 封面
- template8Slide2, // 第2页 - 目录
- template8Slide3, // 第3页 - 过渡
- template8Slide4, // 第4页 - 内容
- template8Slide5 // 第5页 - 结束
- ]
- const newThumbnails = []
- generatedPPT.value.forEach((slide, index) => {
- // 直接根据索引选择模板8图片,确保每张都不同
- console.log(`Template8缩略图幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- const thumbnailImage = template8Images[index % 5]
- newThumbnails.push(thumbnailImage)
- })
- // 更新slideImages
- slideImages.value = newThumbnails
- console.log('Template8蓝色科技主题缩略图生成完成,共', newThumbnails.length, '张')
- }
- // Template7红色主题缩略图生成函数
- const generateTemplate7Thumbnails = () => {
- console.log('开始生成Template7红色主题缩略图')
- if (!generatedPPT.value || generatedPPT.value.length === 0) {
- console.error('无有效的PPT数据用于生成缩略图')
- return
- }
- // 为Template7使用实际的图片文件作为缩略图
- const template7Images = [
- template7Slide1, // 第1页 - 封面
- template7Slide2, // 第2页 - 目录
- template7Slide3, // 第3页 - 过渡
- template7Slide4, // 第4页 - 内容
- template7Slide5 // 第5页 - 结束
- ]
- const newThumbnails = []
- generatedPPT.value.forEach((slide, index) => {
- // 直接根据索引选择模板7图片,确保每张都不同
- console.log(`Template7缩略图幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
- // 直接使用索引来选择图片,确保每张都不同
- const thumbnailImage = template7Images[index % 5]
- newThumbnails.push(thumbnailImage)
- })
- // 更新slideImages
- slideImages.value = newThumbnails
- console.log('Template7红色主题缩略图生成完成,共', newThumbnails.length, '张')
- }
- // 生成红色主题缩略图的具体实现
- const generateRedThemeThumbnail = (slide, index) => {
- // 根据幻灯片类型和索引生成红色主题的缩略图
- const slideType = slide.type || 'content'
- // 使用Canvas生成红色主题缩略图
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- canvas.width = 160
- canvas.height = 90
- // 设置红色主题背景 - 使用更丰富的红色渐变
- const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
- gradient.addColorStop(0, '#E74C3C') // 红色主题的主色调
- gradient.addColorStop(0.5, '#C0392B') // 中等红色
- gradient.addColorStop(1, '#A93226') // 深红色
- ctx.fillStyle = gradient
- ctx.fillRect(0, 0, canvas.width, canvas.height)
- // 添加红色主题装饰元素
- ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
- ctx.beginPath()
- ctx.arc(canvas.width - 20, 20, 15, 0, Math.PI * 2)
- ctx.fill()
- // 添加文本标识
- ctx.fillStyle = '#ffffff'
- ctx.font = 'bold 14px Arial'
- ctx.textAlign = 'center'
- ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
- ctx.shadowBlur = 2
- ctx.shadowOffsetX = 1
- ctx.shadowOffsetY = 1
- switch (slideType) {
- case 'cover':
- ctx.fillText('封面', canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- break
- case 'contents':
- ctx.fillText('目录', canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- break
- case 'transition':
- ctx.fillText('过渡', canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- break
- case 'content':
- ctx.fillText(`内容${index}`, canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- break
- case 'end':
- ctx.fillText('结束', canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- break
- default:
- ctx.fillText(`页面${index + 1}`, canvas.width / 2, canvas.height / 2 - 5)
- ctx.font = '10px Arial'
- ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
- }
- // 添加装饰元素
- ctx.strokeStyle = 'rgba(255,255,255,0.3)'
- ctx.lineWidth = 1
- ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
- return canvas.toDataURL()
- }
- // 重置幻灯片位置到首页
- const resetSlidePosition = () => {
- console.log('重置幻灯片位置到首页')
- // 重置所有相关的页面索引
- currentSlideIndex.value = 0
- currentPPTSlideIndex.value = 0
- selectedImageIndex.value = null
- selectedPPTElementIndex.value = -1
- editingPPTElementIndex.value = -1
- // 重置缩略图滚动位置
- nextTick(() => {
- const thumbnailStripEl = document.querySelector('.thumbnail-strip')
- if (thumbnailStripEl) {
- thumbnailStripEl.scrollLeft = 0
- }
- })
- console.log('页面位置已重置到首页')
- }
- // 带进度的Template7生成函数
- const generateTemplate7WithProgress = async (outlineData, title) => {
- console.log('开始带进度的Template7生成:', outlineData.length, '个章节')
- const slides = []
- // 1. 封面页
- await new Promise(resolve => setTimeout(resolve, 300))
- const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
- coverSlide.id = 'template7-dynamic-cover'
- const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
- if (titleElement) {
- 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>`
- }
- slides.push(coverSlide)
- generatedPPT.value = [...slides]
- // 2. 目录页
- await new Promise(resolve => setTimeout(resolve, 300))
- const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
- contentsSlide.id = 'template7-dynamic-contents'
- // 填充目录项
- outlineData.forEach((chapter, index) => {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
- }
- })
- // 隐藏多余的目录项
- for (let i = outlineData.length; i < 6; i++) {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
- }
- }
- slides.push(contentsSlide)
- generatedPPT.value = [...slides]
- // 3. 为每个章节生成过渡页和内容页
- for (let chapterIndex = 0; chapterIndex < outlineData.length; chapterIndex++) {
- const chapter = outlineData[chapterIndex]
- // 章节过渡页
- await new Promise(resolve => setTimeout(resolve, 300))
- const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
- transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
- const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
- if (transitionTitleElement) {
- transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- slides.push(transitionSlide)
- generatedPPT.value = [...slides]
- // 为每个小节生成内容页
- if (chapter.sections && chapter.sections.length > 0) {
- for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
- const section = chapter.sections[sectionIndex]
- await new Promise(resolve => setTimeout(resolve, 300))
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
- }
- // 动态生成子小节内容
- const subsections = section.subsections || []
- const actualSubsectionCount = Math.min(subsections.length, 4) // 最多4个子小节
- // 填充实际存在的子小节
- for (let subIndex = 0; subIndex < actualSubsectionCount; subIndex++) {
- const subsection = subsections[subIndex]
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
- }
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
- if (itemContentElement) {
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
- }
- }
- // 隐藏多余的子小节元素
- for (let subIndex = actualSubsectionCount; subIndex < 4; subIndex++) {
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;"></span></strong></p>`
- }
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
- if (itemContentElement) {
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;"></span></p>`
- }
- }
- slides.push(contentSlide)
- generatedPPT.value = [...slides]
- }
- } else {
- // 如果章节没有小节,生成一个默认内容页
- await new Promise(resolve => setTimeout(resolve, 300))
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- slides.push(contentSlide)
- generatedPPT.value = [...slides]
- }
- }
- // 4. 结束页
- await new Promise(resolve => setTimeout(resolve, 300))
- const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
- endSlide.id = 'template7-dynamic-end'
- slides.push(endSlide)
- generatedPPT.value = [...slides]
- console.log('带进度的Template7生成完成,共', slides.length, '页')
- }
- // 基于template_8.json生成动态多页结构(保持我们精心设计的蓝色科技主题)
- const generateDynamicTemplate8FromStatic = (outlineData, title) => {
- console.log('开始基于template_8.json生成动态多页结构:', outlineData.length, '个章节')
- const slides = []
- // 1. 封面页 - 使用template_8.json的封面设计
- const coverSlide = JSON.parse(JSON.stringify(template8JsonData[0]))
- coverSlide.id = 'template8-dynamic-cover'
- // 调试信息:检查封面页的背景
- console.log('封面页背景信息:', coverSlide.background)
- console.log('封面页元素数量:', coverSlide.elements?.length)
- // 填充封面标题
- const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
- if (titleElement) {
- 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>`
- }
- slides.push(coverSlide)
- // 2. 目录页 - 使用template_8.json的目录设计
- const contentsSlide = JSON.parse(JSON.stringify(template8JsonData[1]))
- contentsSlide.id = 'template8-dynamic-contents'
- // 填充目录项
- outlineData.forEach((chapter, index) => {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
- if (itemElement) {
- itemElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
- }
- })
- slides.push(contentsSlide)
- // 3. 生成章节内容
- outlineData.forEach((chapter, chapterIndex) => {
- // 章节过渡页 - 使用template_8.json的过渡页设计
- const transitionSlide = JSON.parse(JSON.stringify(template8JsonData[2]))
- transitionSlide.id = `template8-dynamic-transition-${chapterIndex}`
- // 填充过渡页标题
- const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
- if (transitionTitleElement) {
- transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #47acc5;">${chapter.title}</span></strong></p>`
- }
- // 填充过渡页内容
- const transitionContentElement = transitionSlide.elements.find(el => el.id === 'transition-content')
- if (transitionContentElement) {
- transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${chapter.content || `本章将介绍${chapter.title}的相关内容`}</span></p>`
- }
- slides.push(transitionSlide)
- // 生成小节内容页
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- // 内容页 - 使用template_8.json的内容页设计
- const contentSlide = JSON.parse(JSON.stringify(template8JsonData[3]))
- contentSlide.id = `template8-dynamic-content-${chapterIndex}-${sectionIndex}`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #47acc5;">${section.title}</span></strong></p>`
- }
- // 填充内容项
- if (section.content && Array.isArray(section.content)) {
- section.content.forEach((item, itemIndex) => {
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${itemIndex + 1}`)
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${itemIndex + 1}`)
- if (itemTitleElement && item.title) {
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${item.title}</span></strong></p>`
- }
- if (itemContentElement && item.content) {
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${item.content}</span></p>`
- }
- })
- }
- slides.push(contentSlide)
- })
- } else {
- // 如果章节没有小节,创建默认内容页
- const contentSlide = JSON.parse(JSON.stringify(template8JsonData[3]))
- contentSlide.id = `template8-dynamic-content-${chapterIndex}-default`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #47acc5;">${chapter.title}</span></strong></p>`
- }
- slides.push(contentSlide)
- }
- })
- // 4. 结束页 - 使用template_8.json的结束页设计
- const endSlide = JSON.parse(JSON.stringify(template8JsonData[4]))
- endSlide.id = 'template8-dynamic-end'
- slides.push(endSlide)
- console.log('基于template_8.json的动态多页结构生成完成,共', slides.length, '页')
- return slides
- }
- // 基于template_7.json生成动态多页结构(保持我们精心设计的红色主题)
- const generateDynamicTemplate7FromStatic = (outlineData, title) => {
- console.log('开始基于template_7.json生成动态多页结构:', outlineData.length, '个章节')
- const slides = []
- // 1. 封面页 - 使用template_7.json的封面设计
- const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
- coverSlide.id = 'template7-dynamic-cover'
- // 调试信息:检查封面页的背景
- console.log('封面页背景信息:', coverSlide.background)
- console.log('封面页元素数量:', coverSlide.elements?.length)
- // 填充封面标题
- const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
- if (titleElement) {
- 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>`
- }
- slides.push(coverSlide)
- // 2. 目录页 - 使用template_7.json的目录设计
- const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
- contentsSlide.id = 'template7-dynamic-contents'
- // 填充目录项 - 只填充实际存在的章节
- outlineData.forEach((chapter, index) => {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
- }
- })
- // 隐藏多余的目录项
- for (let i = outlineData.length; i < 6; i++) {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
- itemElement.opacity = 0 // 设置为不可见
- }
- }
- // 根据实际目录项数量调整布局
- const actualItemCount = Math.min(outlineData.length, 6)
- console.log(`目录页实际显示${actualItemCount}个目录项`)
- if (actualItemCount === 2) {
- // 2个目录项:调整位置让它们居中显示
- const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
- const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
- if (item1) item1.top = 250
- if (item2) item2.top = 300
- } else if (actualItemCount === 3) {
- // 3个目录项:调整位置让它们均匀分布
- const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
- const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
- const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
- if (item1) item1.top = 220
- if (item2) item2.top = 270
- if (item3) item3.top = 320
- } else if (actualItemCount === 4) {
- // 4个目录项:保持默认位置
- // 默认位置:item-1(200), item-2(250), item-3(300), item-4(350)
- } else if (actualItemCount === 5) {
- // 5个目录项:调整位置让它们均匀分布
- const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
- const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
- const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
- const item4 = contentsSlide.elements.find(el => el.id === 'item-4')
- const item5 = contentsSlide.elements.find(el => el.id === 'item-5')
- if (item1) item1.top = 180
- if (item2) item2.top = 230
- if (item3) item3.top = 280
- if (item4) item4.top = 330
- if (item5) item5.top = 380
- } else if (actualItemCount === 6) {
- // 6个目录项:调整位置让它们均匀分布
- const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
- const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
- const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
- const item4 = contentsSlide.elements.find(el => el.id === 'item-4')
- const item5 = contentsSlide.elements.find(el => el.id === 'item-5')
- const item6 = contentsSlide.elements.find(el => el.id === 'item-6')
- if (item1) item1.top = 160
- if (item2) item2.top = 210
- if (item3) item3.top = 260
- if (item4) item4.top = 310
- if (item5) item5.top = 360
- if (item6) item6.top = 410
- }
- slides.push(contentsSlide)
- // 3. 为每个章节生成过渡页和内容页
- outlineData.forEach((chapter, chapterIndex) => {
- // 章节过渡页 - 使用template_7.json的过渡页设计
- const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
- transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
- // 填充过渡页标题
- const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
- if (transitionTitleElement) {
- transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- slides.push(transitionSlide)
- // 为每个小节生成内容页
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- // 内容页 - 使用template_7.json的内容页设计
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.textType = 'title'
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
- }
- // 确保子小节数据同步到大纲数据中,与通用PPT保持一致
- let actualSubsections = section.subsections || []
- // 如果没有子小节,生成默认子小节并同步到大纲数据
- if (actualSubsections.length === 0) {
- const randomCount = Math.floor(Math.random() * 3) + 2 // 2-4个
- const defaultTitles = ['待AI生成子小节1', '待AI生成子小节2', '待AI生成子小节3', '待AI生成子小节4']
- actualSubsections = []
- for (let i = 0; i < randomCount; i++) {
- actualSubsections.push({
- title: defaultTitles[i] || `要点${i + 1}`,
- content: '待AI填充'
- })
- }
- // 重要:同步到大纲数据中,确保getTitleForElement能正确访问
- section.subsections = actualSubsections
- }
- const actualSubsectionCount = Math.min(actualSubsections.length, 4) // 最多4个子小节
- console.log(`章节${chapterIndex + 1}小节${sectionIndex + 1}有${actualSubsections.length}个子小节,实际显示${actualSubsectionCount}个`)
- // 填充实际存在的子小节
- for (let subIndex = 0; subIndex < actualSubsectionCount; subIndex++) {
- const subsection = actualSubsections[subIndex]
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.textType = 'itemTitle'
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
- console.log(`填充子小节标题${subIndex + 1}: ${subsection.title}`)
- }
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
- if (itemContentElement) {
- // 确保元素有正确的textType属性,以便动画函数能识别
- itemContentElement.textType = 'itemContent'
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
- console.log(`填充子小节内容${subIndex + 1}: ${subsection.content || '待AI填充'}`)
- }
- }
- // 隐藏多余的子小节元素(完全移除)
- for (let subIndex = actualSubsectionCount; subIndex < 4; subIndex++) {
- // 移除多余的元素
- contentSlide.elements = contentSlide.elements.filter(el =>
- el.id !== `itemTitle-${subIndex + 1}` &&
- el.id !== `itemContent-${subIndex + 1}` &&
- el.id !== `item-bg-${subIndex + 1}` &&
- el.id !== `item-icon-${subIndex + 1}`
- )
- }
- // 根据实际子小节数量调整布局
- if (actualSubsectionCount === 2) {
- // 2个子小节:调整位置,让它们居中显示
- // 默认位置:item-bg-1(140), item-bg-2(240) -> 调整为:item-bg-1(190), item-bg-2(290)
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- if (item1) item1.top = 190 // 向下移动50px
- if (item2) item2.top = 290 // 向下移动50px
- // 调整对应的文字和图标位置
- const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
- const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
- const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
- if (title1) title1.top = 200
- if (content1) content1.top = 230
- if (icon1) icon1.top = 205
- const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
- const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
- const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
- if (title2) title2.top = 300
- if (content2) content2.top = 330
- if (icon2) icon2.top = 305
- // 调整三角形装饰图像位置,让它居中
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算2个子小节的总高度:从190到290,总高度100px
- // 装饰图像应该居中,所以top = 190 + (100/2) - (400/2) = 190 + 50 - 200 = 40
- decorationElement.top = 40
- }
- } else if (actualSubsectionCount === 3) {
- // 3个子小节:明确设置位置,确保间距一致
- // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340) - 间距100px
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
- if (item1) item1.top = 140
- if (item2) item2.top = 240
- if (item3) item3.top = 340
- // 调整对应的文字和图标位置
- const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
- const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
- const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
- if (title1) title1.top = 150
- if (content1) content1.top = 180
- if (icon1) icon1.top = 155
- const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
- const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
- const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
- if (title2) title2.top = 250
- if (content2) content2.top = 280
- if (icon2) icon2.top = 255
- const title3 = contentSlide.elements.find(el => el.id === 'itemTitle-3')
- const content3 = contentSlide.elements.find(el => el.id === 'itemContent-3')
- const icon3 = contentSlide.elements.find(el => el.id === 'item-icon-3')
- if (title3) title3.top = 350
- if (content3) content3.top = 380
- if (icon3) icon3.top = 355
- // 调整三角形装饰图像位置,让它居中
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算3个子小节的总高度:从140到340,总高度200px
- // 装饰图像应该居中,所以top = 140 + (200/2) - (400/2) = 140 + 100 - 200 = 40
- decorationElement.top = 40
- }
- } else if (actualSubsectionCount === 4) {
- // 4个子小节:调整位置,让它们均匀分布,增加间距
- // 默认位置: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)
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
- const item4 = contentSlide.elements.find(el => el.id === 'item-bg-4')
- if (item1) item1.top = 120
- if (item2) item2.top = 220
- if (item3) item3.top = 320
- if (item4) item4.top = 420
- // 调整对应的文字和图标位置
- const elements = [
- { id: 'itemTitle-1', top: 130 }, { id: 'itemContent-1', top: 160 }, { id: 'item-icon-1', top: 135 },
- { id: 'itemTitle-2', top: 230 }, { id: 'itemContent-2', top: 260 }, { id: 'item-icon-2', top: 235 },
- { id: 'itemTitle-3', top: 330 }, { id: 'itemContent-3', top: 360 }, { id: 'item-icon-3', top: 335 },
- { id: 'itemTitle-4', top: 430 }, { id: 'itemContent-4', top: 460 }, { id: 'item-icon-4', top: 435 }
- ]
- elements.forEach(({ id, top }) => {
- const element = contentSlide.elements.find(el => el.id === id)
- if (element) element.top = top
- })
- // 调整三角形装饰图像位置,让它再下来一点
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算4个子小节的总高度:从120到420,总高度300px
- // 装饰图像应该再下来一点,所以top = 120 + (300/2) - (400/2) + 30 = 120 + 150 - 200 + 30 = 100
- decorationElement.top = 100
- }
- }
- slides.push(contentSlide)
- })
- } else {
- // 如果章节没有小节,生成一个默认内容页
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.textType = 'title'
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- // 为默认内容页随机生成2-4个子小节(模板7现在支持4个子小节)
- const randomCount = Math.floor(Math.random() * 3) + 2 // 2-4个
- const defaultTitles = ['主要内容', '详细说明', '补充内容', '总结要点']
- const defaultContents = ['待AI填充', '待AI填充', '待AI填充', '待AI填充']
- const defaultSubsections = []
- for (let i = 0; i < randomCount; i++) {
- defaultSubsections.push({
- title: defaultTitles[i] || `要点${i + 1}`,
- content: defaultContents[i] || '待AI填充'
- })
- }
- // 填充子小节
- for (let subIndex = 0; subIndex < randomCount; subIndex++) {
- const subsection = defaultSubsections[subIndex]
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.textType = 'itemTitle'
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
- }
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
- if (itemContentElement) {
- itemContentElement.textType = 'itemContent'
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content}</span></p>`
- }
- }
- // 移除多余的子小节
- for (let subIndex = randomCount; subIndex < 4; subIndex++) {
- contentSlide.elements = contentSlide.elements.filter(el =>
- el.id !== `itemTitle-${subIndex + 1}` &&
- el.id !== `itemContent-${subIndex + 1}` &&
- el.id !== `item-bg-${subIndex + 1}` &&
- el.id !== `item-icon-${subIndex + 1}`
- )
- }
- // 根据实际子小节数量调整布局
- if (randomCount === 2) {
- // 2个子小节:调整位置,让它们居中显示
- // 默认位置:item-bg-1(140), item-bg-2(240) -> 调整为:item-bg-1(190), item-bg-2(290)
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- if (item1) item1.top = 190 // 向下移动50px
- if (item2) item2.top = 290 // 向下移动50px
- // 调整对应的文字和图标位置
- const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
- const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
- const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
- if (title1) title1.top = 200
- if (content1) content1.top = 230
- if (icon1) icon1.top = 205
- const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
- const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
- const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
- if (title2) title2.top = 300
- if (content2) content2.top = 330
- if (icon2) icon2.top = 305
- // 调整三角形装饰图像位置,让它居中
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算2个子小节的总高度:从190到290,总高度100px
- // 装饰图像应该居中,所以top = 190 + (100/2) - (400/2) = 190 + 50 - 200 = 40
- decorationElement.top = 40
- }
- } else if (randomCount === 3) {
- // 3个子小节:明确设置位置,确保间距一致
- // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340) - 间距100px
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
- if (item1) item1.top = 140
- if (item2) item2.top = 240
- if (item3) item3.top = 340
- // 调整对应的文字和图标位置
- const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
- const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
- const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
- if (title1) title1.top = 150
- if (content1) content1.top = 180
- if (icon1) icon1.top = 155
- const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
- const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
- const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
- if (title2) title2.top = 250
- if (content2) content2.top = 280
- if (icon2) icon2.top = 255
- const title3 = contentSlide.elements.find(el => el.id === 'itemTitle-3')
- const content3 = contentSlide.elements.find(el => el.id === 'itemContent-3')
- const icon3 = contentSlide.elements.find(el => el.id === 'item-icon-3')
- if (title3) title3.top = 350
- if (content3) content3.top = 380
- if (icon3) icon3.top = 355
- // 调整三角形装饰图像位置,让它居中
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算3个子小节的总高度:从140到340,总高度200px
- // 装饰图像应该居中,所以top = 140 + (200/2) - (400/2) = 140 + 100 - 200 = 40
- decorationElement.top = 40
- }
- } else if (randomCount === 4) {
- // 4个子小节:调整位置,让它们均匀分布,增加间距
- // 默认位置: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)
- const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
- const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
- const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
- const item4 = contentSlide.elements.find(el => el.id === 'item-bg-4')
- if (item1) item1.top = 120
- if (item2) item2.top = 220
- if (item3) item3.top = 320
- if (item4) item4.top = 420
- // 调整对应的文字和图标位置
- const elements = [
- { id: 'itemTitle-1', top: 130 }, { id: 'itemContent-1', top: 160 }, { id: 'item-icon-1', top: 135 },
- { id: 'itemTitle-2', top: 230 }, { id: 'itemContent-2', top: 260 }, { id: 'item-icon-2', top: 235 },
- { id: 'itemTitle-3', top: 330 }, { id: 'itemContent-3', top: 360 }, { id: 'item-icon-3', top: 335 },
- { id: 'itemTitle-4', top: 430 }, { id: 'itemContent-4', top: 460 }, { id: 'item-icon-4', top: 435 }
- ]
- elements.forEach(({ id, top }) => {
- const element = contentSlide.elements.find(el => el.id === id)
- if (element) element.top = top
- })
- // 调整三角形装饰图像位置,让它再下来一点
- const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
- if (decorationElement) {
- // 计算4个子小节的总高度:从120到420,总高度300px
- // 装饰图像应该再下来一点,所以top = 120 + (300/2) - (400/2) + 30 = 120 + 150 - 200 + 30 = 100
- decorationElement.top = 100
- }
- }
- slides.push(contentSlide)
- }
- })
- // 4. 结束页 - 使用template_7.json的结束页设计
- const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
- endSlide.id = 'template7-dynamic-end'
- slides.push(endSlide)
- console.log('基于template_7.json的动态多页结构生成完成,共', slides.length, '页')
- return slides
- }
- // 动态生成Template7多页结构(基于红色主题)
- const generateDynamicTemplate7 = (outlineData, title) => {
- console.log('开始动态生成Template7多页结构:', outlineData.length, '个章节')
- const slides = []
- // 1. 封面页 - 使用template_7.json的封面设计
- const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
- coverSlide.id = 'template7-dynamic-cover'
- // 填充封面标题
- const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
- if (titleElement) {
- 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>`
- }
- slides.push(coverSlide)
- // 2. 目录页 - 使用template_7.json的目录设计
- const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
- contentsSlide.id = 'template7-dynamic-contents'
- // 填充目录项
- outlineData.forEach((chapter, index) => {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
- }
- })
- // 隐藏多余的目录项
- for (let i = outlineData.length; i < 6; i++) {
- const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
- if (itemElement) {
- itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
- }
- }
- slides.push(contentsSlide)
- // 3. 为每个章节生成过渡页和内容页
- outlineData.forEach((chapter, chapterIndex) => {
- // 章节过渡页 - 使用template_7.json的过渡页设计
- const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
- transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
- // 填充过渡页标题
- const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
- if (transitionTitleElement) {
- transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- slides.push(transitionSlide)
- // 为每个小节生成内容页
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- // 内容页 - 使用template_7.json的内容页设计
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
- }
- // 填充小节内容
- const subsections = section.subsections || []
- subsections.forEach((subsection, subIndex) => {
- const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
- if (itemTitleElement) {
- itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
- }
- const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
- if (itemContentElement) {
- itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
- }
- })
- slides.push(contentSlide)
- })
- } else {
- // 如果章节没有小节,生成一个默认内容页
- const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
- contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
- // 填充内容页标题
- const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
- if (contentTitleElement) {
- contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
- }
- slides.push(contentSlide)
- }
- })
- // 4. 结束页 - 使用template_7.json的结束页设计
- const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
- endSlide.id = 'template7-dynamic-end'
- slides.push(endSlide)
- console.log('Template7动态多页结构生成完成,共', slides.length, '页')
- return slides
- }
- // 检查消息是否正在朗读
- const isSpeaking = (messageId) => {
- return speakingMessageId.value === messageId
- }
- // 处理输入框输入,限制2000字
- const handleInput = () => {
- if (messageText.value.length > 2000) {
- messageText.value = messageText.value.substring(0, 2000)
- ElMessage.warning('消息长度不能超过2000字')
- }
- }
- // 获取功能卡片数据
- const getFunctionCards = async () => {
- try {
- console.log('开始获取安全培训功能卡片...')
- const response = await apis.getFunctionCard({ function_type: 1 }) // 1为安全培训类型
- console.log('功能卡片响应:', response)
- if (response.statusCode === 200) {
- functionCards.value = response.data
- console.log('功能卡片数据已设置:', functionCards.value)
- } else {
- console.error('获取功能卡片失败:', response.statusCode)
- }
- } catch (error) {
- console.error('获取功能卡片失败:', error)
- }
- }
- // 获取热点问题数据
- const getHotQuestions = async () => {
- try {
- console.log('开始获取安全培训热点问题...')
- const response = await apis.getHotQuestion({ question_type: 1 }) // 1为安全培训类型
- console.log('热点问题响应:', response)
- if (response.statusCode === 200) {
- hotQuestions.value = response.data
- console.log('热点问题数据已设置:', hotQuestions.value)
- } else {
- console.error('获取热点问题失败:', response.statusCode)
- }
- } catch (error) {
- console.error('获取热点问题失败:', error)
- }
- }
- // 获取OSS上传配置
- const getOSSUploadConfig = async () => {
- try {
- console.log('开始获取OSS配置...')
- const response = await apis.uploadOss({})
- console.log('OSS配置响应:', response)
- if (response.statusCode === 200) {
- const res = response
- console.log('OSS配置数据:', res)
- uploadData.OssAccessKeyId = res.accessid
- uploadData.policy = res.policy
- uploadData.Signature = res.signature
- uploadData.host = res.host
- uploadData.dir = res.dir || 'uploads/safety/'
- uploadData.key = uploadData.dir + '${filename}'
- console.log('OSS上传配置已设置:', uploadData)
- return true
- } else {
- console.error('接口返回状态码不是200:', response.statusCode)
- throw new Error('获取OSS配置失败')
- }
- } catch (error) {
- console.error('获取OSS配置失败:', error)
- ElMessage.error('获取上传配置失败,请重试')
- return false
- }
- }
- // 上传文件到阿里云OSS(web直传)
- const uploadFileToOSS = async (file) => {
- try {
- console.log('开始上传文件:', file.name)
- console.log('当前OSS配置状态:', uploadData)
- // 如果还没有OSS配置,先获取
- if (!uploadData.host) {
- console.log('OSS配置为空,开始获取配置...')
- const configResult = await getOSSUploadConfig()
- if (!configResult) {
- throw new Error('获取OSS配置失败')
- }
- }
- // 验证OSS配置是否完整
- if (!uploadData.host || !uploadData.policy || !uploadData.OssAccessKeyId || !uploadData.Signature) {
- console.error('OSS配置不完整:', uploadData)
- throw new Error('OSS配置不完整,请重试')
- }
- // 构建OSS上传参数
- const formData = new FormData()
- formData.append('key', uploadData.dir + file.name)
- formData.append('policy', uploadData.policy)
- formData.append('OSSAccessKeyId', uploadData.OssAccessKeyId)
- formData.append('Signature', uploadData.Signature)
- formData.append('success_action_status', '200')
- formData.append('file', file)
- console.log('开始上传到OSS:', uploadData.host)
- // 直接上传到OSS
- const uploadResponse = await fetch(uploadData.host, {
- method: 'POST',
- body: formData
- })
- console.log('OSS上传响应:', uploadResponse)
- if (uploadResponse.ok) {
- // 上传成功,创建文件信息对象
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
- const ossUrl = uploadData.host + '/' + uploadData.dir + file.name
- selectedFile.value = {
- file,
- name: file.name,
- size: file.size,
- type: fileExtension,
- icon: getFileIcon(fileExtension),
- ossUrl: ossUrl, // OSS访问链接
- ossKey: uploadData.dir + file.name
- }
- ElMessage.success('文件上传成功')
- } else {
- const errorText = await uploadResponse.text()
- console.error('OSS上传失败响应:', errorText)
- throw new Error(`上传失败: ${uploadResponse.status} - ${errorText}`)
- }
- } catch (error) {
- console.error('文件上传失败:', error)
- throw error
- }
- }
- // 获取文件图标
- const getFileIcon = (fileType) => {
- switch (fileType) {
- case '.doc':
- case '.docx':
- return wordDocIcon
- default:
- return '📎'
- }
- }
- // 格式化文件大小
- const formatFileSize = (bytes) => {
- if (bytes === 0) return '0 B'
- const k = 1024
- const sizes = ['B', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
- }
- // 删除选中的文件
- const removeSelectedFile = () => {
- if (selectedFile.value) {
- selectedFile.value = null
- }
- }
- // 触发文件上传
- const triggerFileUpload = () => {
- if (selectedFile.value) {
- ElMessage.warning('只能上传一个文件,请先删除当前文件')
- return
- }
- fileInput.value?.click()
- }
- // 文件处理工具函数
- const validateFile = (file) => {
- // 检查文件大小
- if (file.size > fileConfig.maxSize) {
- throw new Error('文件大小不能超过20MB')
- }
- // 检查文件类型
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
- if (!fileConfig.allowedTypes.includes(fileExtension)) {
- throw new Error('只支持.docx格式的Word文档。如果是.doc格式,请先另存为.docx格式。')
- }
- return fileExtension
- }
- // 读取Word文件内容
- const readWordFile = async (file) => {
- try {
- console.log('开始读取Word文件:', file.name, '文件大小:', file.size)
- // 检查文件是否为空
- if (file.size === 0) {
- throw new Error('Word文件为空')
- }
- // 使用mammoth.js库读取Word文档
- console.log('正在导入mammoth库...')
- const mammoth = await import('mammoth')
- console.log('mammoth库导入成功')
- // 将文件转换为ArrayBuffer
- const arrayBuffer = await file.arrayBuffer()
- console.log('文件转换为ArrayBuffer成功,大小:', arrayBuffer.byteLength)
- // 提取文本内容
- console.log('开始提取文本内容...')
- const result = await mammoth.extractRawText({ arrayBuffer })
- console.log('Word文件读取完成,内容长度:', result.value.length)
- return result.value
- } catch (error) {
- console.error('Word文件读取失败,详细错误:', error)
- console.error('错误堆栈:', error.stack)
- // 提供更具体的错误信息
- if (error.message.includes('Invalid file format')) {
- throw new Error('Word文件格式无效或已损坏')
- } else if (error.message.includes('File is empty')) {
- throw new Error('Word文件为空')
- } else {
- throw new Error(`Word文件读取失败: ${error.message}`)
- }
- }
- }
- // 处理文件标签格式的回显
- const processFileDisplay = (text, file) => {
- if (!file) {
- // 如果没有文件对象,尝试从文本中提取文件信息
- return processFileDisplayFromText(text)
- }
- // 查找文件标签并替换为显示格式
- const fileDisplay = `
- 📄 文件信息:
- 文件名:${file.name}
- 文件大小:${formatFileSize(file.size)}
- 文件类型:${file.type}
- 📝 文件内容:
- ${file.content}
- ---
- `
- // 替换文件标签为显示格式
- let processedText = text
- .replace(/<word>.*?<\/word>/gs, fileDisplay)
- .replace(/<filename>.*?<\/filename>/g, '')
- .replace(/<filesize>.*?<\/filesize>/g, '')
- return processedText
- }
- // 从文本中提取文件信息并转换为显示格式
- const processFileDisplayFromText = (text) => {
- // 提取文件名
- const filenameMatch = text.match(/<filename>(.*?)<\/filename>/)
- const filename = filenameMatch ? filenameMatch[1] : '未知文件'
- // 提取文件大小
- const filesizeMatch = text.match(/<filesize>(.*?)<\/filesize>/)
- const filesize = filesizeMatch ? parseInt(filesizeMatch[1]) : 0
- // 提取文件内容
- const wordMatch = text.match(/<word>(.*?)<\/word>/s)
- const fileContent = wordMatch ? wordMatch[1].trim() : '无内容'
- // 创建文件显示格式
- const fileDisplay = `
- 📄 文件信息:
- 文件名:${filename}
- 文件大小:${formatFileSize(filesize)}
- 文件类型:${filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '未知'}
- 📝 文件内容:
- ${fileContent}
- ---
- `
- // 替换文件标签为显示格式
- let processedText = text
- .replace(/<word>.*?<\/word>/gs, fileDisplay)
- .replace(/<filename>.*?<\/filename>/g, '')
- .replace(/<filesize>.*?<\/filesize>/g, '')
- return processedText
- }
- // 处理文件选择
- const handleFileSelect = async (event) => {
- const file = event.target.files[0]
- if (!file) return
- try {
- // 验证文件
- const fileExtension = validateFile(file)
- isUploadingFile.value = true
- console.log('开始读取文件内容:', file.name)
- // 只处理Word文档
- const extractedContent = await readWordFile(file)
- // 创建文件信息对象
- selectedFile.value = {
- file,
- name: file.name,
- size: file.size,
- type: fileExtension,
- icon: getFileIcon(fileExtension),
- content: extractedContent // 存储提取的内容
- }
- // 显示提取的内容长度
- const contentLength = extractedContent.length
- console.log('文件内容提取完成,字符数:', contentLength)
- ElMessage.success(`文件读取成功,提取了${contentLength}个字符的内容`)
- } catch (error) {
- console.error('文件读取失败:', error)
- ElMessage.error(error.message || '文件读取失败,请重试')
- } finally {
- isUploadingFile.value = false
- event.target.value = ''
- }
- }
- // 语音输入相关方法
- const handleVoiceClick = () => {
- console.log('点击语音按钮')
- if (isListening.value) {
- // 如果正在录音,则停止
- stopVoiceInput()
- } else {
- // 开始语音输入
- startVoiceInput()
- }
- }
- const startVoiceInput = () => {
- console.log('开始语音输入')
- // 开始语音识别
- const success = startListening()
- if (!success) {
- ElMessage.error('语音识别启动失败,请检查麦克风权限')
- }
- }
- const stopVoiceInput = () => {
- console.log('停止语音输入')
- stopListening()
- // 语音识别完成后,将结果填入输入框
- if (transcript.value.trim()) {
- messageText.value = transcript.value
- }
- }
- // 复制整个大纲
- const copyEntireOutline = async () => {
- try {
- if (!outlineData.value || outlineData.value.length === 0) {
- ElMessage.warning('暂无大纲内容可复制')
- return
- }
- // 构建大纲文本(纯文本格式,无井号)
- let outlineText = `${outlineTitle.value || '安全培训大纲'}\n\n`
- outlineData.value.forEach((chapter, chapterIndex) => {
- outlineText += `${chapter.title}\n\n`
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- outlineText += ` ${section.title}\n\n`
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- outlineText += ` ${subsection.title}\n\n`
- if (subsection.subsubsections && subsection.subsubsections.length > 0) {
- subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
- outlineText += ` - ${subsubsection.title}\n`
- if (subsubsection.content) {
- outlineText += ` ${subsubsection.content}\n`
- }
- })
- outlineText += '\n'
- }
- })
- }
- })
- }
- })
- // 不添加统计信息,只复制纯大纲内容
- // 检查 Clipboard API 是否可用
- if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
- try {
- await navigator.clipboard.writeText(outlineText)
- ElMessage.success('复制成功')
- return
- } catch (clipboardError) {
- console.warn('Clipboard API 失败,使用降级方案:', clipboardError)
- }
- }
- // 降级方案:使用传统复制方法
- const textArea = document.createElement('textarea')
- textArea.value = outlineText
- textArea.style.position = 'fixed'
- textArea.style.left = '-999999px'
- textArea.style.top = '-999999px'
- document.body.appendChild(textArea)
- textArea.focus()
- textArea.select()
- try {
- const successful = document.execCommand('copy')
- if (successful) {
- ElMessage.success('大纲已复制到剪贴板')
- } else {
- throw new Error('execCommand 复制失败')
- }
- } catch (execError) {
- console.error('传统复制方法也失败:', execError)
- ElMessage.error('复制失败,请手动选择文本复制')
- } finally {
- document.body.removeChild(textArea)
- }
- } catch (error) {
- console.error('复制大纲失败:', error)
- ElMessage.error('复制失败,请手动选择文本复制')
- }
- }
- // 下载大纲为Word文档
- const downloadOutlineAsWord = async () => {
- try {
- if (!outlineData.value || outlineData.value.length === 0) {
- ElMessage.warning('暂无大纲内容可下载')
- return
- }
- // 构建Word文档内容(HTML格式,兼容Microsoft Office Word)
- let htmlContent = `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word 15">
- <meta name="Originator" content="Microsoft Word 15">
- <title>${outlineTitle.value || '安全培训大纲'}</title>
- <!--[if gte mso 9]>
- <xml>
- <w:WordDocument>
- <w:View>Print</w:View>
- <w:Zoom>100</w:Zoom>
- <w:DoNotPromptForConvert/>
- <w:DoNotShowRevisions/>
- <w:DoNotPrintRevisions/>
- <w:DoNotShowComments/>
- <w:DoNotShowInsertionsAndDeletions/>
- <w:DoNotShowPropertyChanges/>
- <w:Compatibility>
- <w:BreakWrappedTables/>
- <w:SnapToGridInCell/>
- <w:WrapTextWithPunct/>
- <w:UseAsianBreakRules/>
- <w:DontGrowAutofit/>
- </w:Compatibility>
- </w:WordDocument>
- </xml>
- <![endif]-->
- <style>
- body {
- font-family: "Microsoft YaHei", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- margin: 24px;
- color: #000;
- }
- .header {
- text-align: center;
- margin-bottom: 14px;
- }
- .outline-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- }
- h1, h2, h3, h4, h5, h6 {
- color: #000;
- font-weight: bold;
- font-family: "Microsoft YaHei", Arial, sans-serif;
- margin-top: 20px;
- margin-bottom: 15px;
- }
- h1 {
- font-size: 20px;
- border-bottom: 2px solid #000;
- padding-bottom: 10px;
- }
- h2 {
- font-size: 18px;
- margin-top: 20px;
- margin-bottom: 12px;
- }
- h3 {
- font-size: 16px;
- margin-top: 15px;
- margin-bottom: 8px;
- }
- h4 {
- font-size: 14px;
- margin-top: 12px;
- margin-bottom: 6px;
- }
- ul, li {
- color: #000;
- font-family: "Microsoft YaHei", Arial, sans-serif;
- }
- li {
- margin-bottom: 4px;
- }
- .stats {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 8px;
- margin-top: 30px;
- border: 1px solid #ddd;
- }
- .stats h3 {
- color: #2c3e50;
- margin-top: 0;
- font-size: 16px;
- }
- .stats p {
- margin: 8px 0;
- color: #555;
- font-size: 14px;
- }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="outline-title">${outlineTitle.value || '安全培训大纲'}</div>
- </div>
- `
- // 添加章节内容(使用新的结构格式)
- outlineData.value.forEach((chapter, chapterIndex) => {
- htmlContent += `<h2>${chapter.title}</h2>`
- if (chapter.sections && chapter.sections.length > 0) {
- chapter.sections.forEach((section, sectionIndex) => {
- htmlContent += `<h3>${section.title}</h3>`
- if (section.subsections && section.subsections.length > 0) {
- section.subsections.forEach((subsection, subsectionIndex) => {
- htmlContent += `<h4>${subsection.title}</h4>`
- if (subsection.subsubsections && subsection.subsubsections.length > 0) {
- htmlContent += `<ul>`
- subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
- htmlContent += `<li><strong>${subsubsection.title}</strong>`
- if (subsubsection.content) {
- htmlContent += `<br>${subsubsection.content}`
- }
- htmlContent += `</li>`
- })
- htmlContent += `</ul>`
- }
- })
- }
- })
- }
- })
- // 添加统计信息
- if (outlineStats.value) {
- htmlContent += `
- <div class="stats">
- <h3>大纲统计信息</h3>
- <p><strong>总章节数:</strong>${outlineStats.value.totalChapters || '未知'}章</p>
- <p><strong>总小节数:</strong>${outlineStats.value.totalSections || '未知'}小节</p>
- <p><strong>预计PPT页数:</strong>${outlineStats.value.estimatedPages || '未知'}</p>
- <p><strong>预计讲解时长:</strong>${outlineStats.value.estimatedTime || '未知'}</p>
- </div>
- `
- }
- htmlContent += `
- </body>
- </html>
- `
- // 创建Blob对象 - 使用Word兼容的MIME类型
- const blob = new Blob([htmlContent], { type: 'application/msword' })
- // 创建下载链接
- const url = URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = `${outlineTitle.value || '安全培训大纲'}.doc`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- // 清理URL对象
- URL.revokeObjectURL(url)
- ElMessage.success('下载成功')
- } catch (error) {
- console.error('下载大纲失败:', error)
- ElMessage.error('下载失败,请重试')
- }
- }
- </script>
- <style lang="less" scoped>
- // 全局图片无边框样式
- img {
- border: none !important;
- outline: none !important;
- }
- // 删除图标样式
- .delete-icon {
- width: 16px;
- height: 16px;
- }
- body,
- html {
- background-color: #E8F4FD;
- margin: 0;
- padding: 0;
- }
- .chat-container {
- display: flex;
- height: 100vh;
- font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
- }
- /* 中间历史记录栏 */
- .history-sidebar {
- width: 280px;
- min-width: 280px;
- flex-shrink: 0;
- background: #E8F0FF;
- display: flex;
- flex-direction: column;
- }
- /* 中间历史记录栏样式 */
- .history-sidebar {
- padding: 24px 16px 0 16px;
- .history-header {
- background: transparent;
- .section-title {
- font-size: 16px;
- font-weight: 600;
- color: #2C3E50;
- }
- .new-chat-btn {
- width: 248px;
- height: 40px;
- cursor: pointer;
- transition: opacity 0.3s ease;
- object-fit: contain;
- display: block;
- margin-top: 16px;
- margin-bottom: 16px;
- &:hover {
- opacity: 0.8;
- }
- }
- }
- .history-list {
- flex: 1;
- overflow-y: auto;
- width: 248px;
- height: 84px;
- /* 隐藏滚动条 */
- &::-webkit-scrollbar {
- display: none;
- }
- -ms-overflow-style: none;
- /* IE and Edge */
- scrollbar-width: none;
- /* Firefox */
- .history-item {
- background: white;
- border-radius: 8px;
- padding: 12px 15px 12px 15px;
- margin-bottom: 8px;
- cursor: pointer;
- transition: all 0.3s ease;
- border-left: 3px solid transparent;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- display: flex;
- align-items: flex-start;
- gap: 12px;
- position: relative;
- &:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- .delete-btn {
- opacity: 1;
- visibility: visible;
- }
- }
- &.active {
- border-left-color: #3E7BFA;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
- cursor: default;
- opacity: 0.8;
- &:hover {
- transform: none;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
- }
- }
- .history-icon {
- width: 80px;
- height: 60px;
- flex-shrink: 0;
- .history-icon-img {
- width: 100%;
- height: 100%;
- object-fit: contain;
- border-radius: 4px;
- }
- }
- .history-content {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 60px;
- // margin-right: 12px;
- .history-title {
- font-size: 14px;
- line-height: 1.4;
- color: #1F2937;
- overflow: hidden;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- text-overflow: ellipsis;
- flex: 1;
- max-height: calc(14px * 1.4 * 2);
- /* 确保最大高度正好是两行 */
- word-break: break-word;
- }
- .history-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: auto;
- .history-time {
- font-size: 12px;
- color: #7C8DB5;
- }
- .history-pages {
- font-size: 12px;
- color: #6B7280;
- }
- }
- }
- .delete-btn {
- opacity: 0;
- visibility: hidden;
- transition: all 0.2s ease;
- cursor: pointer;
- padding: 2px;
- border-radius: 3px;
- color: #7c8db5;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-left: 8px;
- flex-shrink: 0;
- position: absolute;
- top: 50%;
- right: 2px;
- transform: translateY(-50%);
- &:hover {
- color: #ff4757;
- background-color: rgba(255, 71, 87, 0.05);
- }
- &.always-visible {
- opacity: 1;
- visibility: visible;
- }
- }
- }
- .empty-history {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- margin-top: 236px;
- .empty-icon {
- width: 147px;
- height: 148px;
- object-fit: contain;
- margin-bottom: 16px;
- }
- .empty-text {
- font-size: 16px;
- color: #9C9FA7;
- text-align: center;
- }
- }
- }
- }
- /* 主工作区域 */
- .main-work {
- flex: 1;
- background: #EBF3FF;
- display: flex;
- flex-direction: column;
- }
- /* 工作头部 */
- .work-header {
- background: transparent;
- padding: 30px 0px 0px 18px;
- h2 {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #2C3E50;
- }
- }
- /* 工作内容区域 */
- .work-content {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
- margin-top: 24px;
- padding: 0px 30px;
- /* 隐藏滚动条样式 */
- &::-webkit-scrollbar {
- width: 0;
- background: transparent;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: transparent;
- }
- }
- /* 聊天内容区域 */
- .chat-content {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-top: 24px;
- width: 1060px;
- }
- /* 旋转动画 */
- .rotating {
- animation: rotate 1s linear infinite;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- /* 大纲内容禁用状态 */
- .outline-content.disabled {
- position: relative;
- pointer-events: none;
- opacity: 0.8;
- }
- /* 生成中遮罩层 */
- .generating-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.9);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 100;
- border-radius: 12px;
- }
- .generating-content {
- text-align: center;
- padding: 40px;
- }
- .generating-content p {
- font-size: 18px;
- color: #6B7280;
- margin: 0;
- font-weight: 500;
- }
- /* 应用模板中遮罩层 */
- .applying-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.7);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- border-radius: 12px;
- overflow: hidden;
- }
- /* 禁用状态样式 */
- .history-sidebar.disabled {
- pointer-events: none;
- opacity: 0.8;
- }
- .history-sidebar.disabled .new-chat-btn {
- cursor: not-allowed;
- opacity: 0.8;
- }
- .history-item.disabled {
- cursor: not-allowed !important;
- opacity: 0.8;
- }
- .applying-content {
- text-align: center;
- padding: 40px;
- }
- .applying-content p {
- font-size: 18px;
- color: #6B7280;
- margin: 0;
- font-weight: 500;
- }
- /* 模板内容禁用状态 */
- .template-content.disabled {
- position: relative;
- pointer-events: none;
- opacity: 0.8;
- }
- /* step3内容禁用状态 */
- .step3-content.disabled {
- position: relative;
- pointer-events: none;
- opacity: 0.8;
- }
- /* 下载内容禁用状态 */
- .download-content.disabled {
- position: relative;
- pointer-events: none;
- opacity: 0.8;
- }
- /* 加载状态样式 */
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.95);
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- border-radius: 12px;
- backdrop-filter: blur(2px);
- transition: all 0.3s ease;
- }
- .loading-content {
- text-align: center;
- padding: 40px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 16px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
- border: 1px solid rgba(255, 255, 255, 0.2);
- }
- .loading-spinner {
- width: 48px;
- height: 48px;
- border: 4px solid #f3f3f3;
- border-top: 4px solid #409eff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 24px auto;
- }
- .loading-text {
- color: #6B7280;
- font-size: 18px;
- margin: 0;
- font-weight: 500;
- }
- /* 历史记录加载状态样式 */
- .history-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- min-height: 200px;
- }
- .history-loading .loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid #f3f3f3;
- border-top: 3px solid #409eff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 16px auto;
- }
- .history-loading .loading-text {
- color: #6B7280;
- font-size: 14px;
- margin: 0;
- font-weight: 400;
- }
- .loading-subtitle {
- color: #9CA3AF;
- font-size: 14px;
- margin-top: 8px;
- font-weight: 400;
- }
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- @keyframes pulse {
- 0%,
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- 50% {
- transform: scale(1.05);
- opacity: 0.8;
- }
- }
- @keyframes blink {
- 0%,
- 50% {
- opacity: 1;
- }
- 51%,
- 100% {
- opacity: 0.3;
- }
- }
- /* AI生成的幻灯片样式 */
- .ai-generated-slide {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .slide-content {
- width: 960px;
- height: 540px;
- position: relative;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- .slide-element {
- position: absolute;
- }
- /* 按钮禁用状态样式 */
- .action-btn:disabled,
- .eval-btn:disabled {
- cursor: not-allowed;
- }
- .action-btn:disabled:hover,
- .eval-btn:disabled:hover {
- background: inherit;
- border-color: inherit;
- transform: none;
- }
- /* AI助手介绍 */
- .ai-intro {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-bottom: 96px;
- .ai-avatar {
- width: 80px;
- height: 80px;
- border-radius: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 20px;
- .ai-avatar-img {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- }
- .ai-greeting {
- text-align: center;
- h3 {
- font-size: 24px;
- font-weight: 600;
- color: #2C3E50;
- margin: 0 0 8px 0;
- }
- p {
- font-size: 16px;
- color: #7C8DB5;
- margin: 0;
- }
- }
- }
- /* 聊天消息区域 */
- .chat-messages {
- width: 100%;
- max-width: 1060px;
- padding: 0 75px 0 35px;
- display: flex;
- flex-direction: column;
- gap: 24px;
- padding-bottom: 40px;
- /* 添加底部间距 */
- }
- /* 消息项样式 */
- .message-item {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- /* 用户消息样式 */
- .user-message {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 8px;
- .message-content {
- background: #004BFF20;
- color: #000000;
- padding: 16px 20px;
- border-radius: 18px 0px 18px 18px;
- max-width: 900px;
- word-wrap: break-word;
- line-height: 1.5;
- font-size: 16px;
- .message-file {
- margin-bottom: 12px;
- .file-display {
- display: flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.8);
- border: 1px solid #E5E7EB;
- border-radius: 12px;
- padding: 16px;
- max-width: 400px;
- .file-icon {
- font-size: 32px;
- margin-right: 16px;
- width: 48px;
- text-align: center;
-
- .file-icon-img {
- width: 32px;
- height: 32px;
- object-fit: contain;
- }
- }
- .file-details {
- flex: 1;
- .file-name {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- margin-bottom: 4px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 200px;
- }
- .file-size {
- font-size: 12px;
- color: #6B7280;
- }
- }
- }
- }
- .message-text {
- // margin-top: 8px;
- }
- }
- .message-actions {
- display: flex;
- // gap: 8px;
- .action-btn {
- background: none;
- border: none;
- padding: 6px 4px;
- border-radius: 4px;
- font-size: 12px;
- color: #6b7280;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- gap: 4px;
- &:hover {
- color: #374151;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- object-fit: contain;
- }
- }
- }
- }
- /* AI消息样式 */
- .ai-message {
- display: flex;
- gap: 12px;
- .ai-avatar-small {
- width: 40px;
- height: 40px;
- flex-shrink: 0;
- .ai-icon {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- }
- .message-content {
- background: white;
- color: #374151;
- padding: 16px 20px;
- border-radius: 0px 18px 18px 18px;
- max-width: 900px;
- width: fit-content;
- min-width: 200px;
- word-wrap: break-word;
- line-height: 1.5;
- font-size: 16px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- white-space: pre-line;
- transition: width 0.3s ease;
- .ai-text {
- min-height: 20px;
- line-height: 1.5;
- margin-bottom: 16px;
- .typing-indicator {
- color: #9CA3AF;
- font-style: italic;
- font-size: 14px;
- display: flex;
- align-items: center;
- gap: 8px;
- white-space: nowrap;
- .thinking-animation {
- display: flex;
- gap: 4px;
- .dot {
- width: 6px;
- height: 6px;
- background: #9CA3AF;
- border-radius: 50%;
- animation: thinking 1.4s infinite ease-in-out;
- &:nth-child(1) {
- animation-delay: -0.32s;
- }
- &:nth-child(2) {
- animation-delay: -0.16s;
- }
- &:nth-child(3) {
- animation-delay: 0s;
- }
- }
- }
- }
- .ai-content {
- line-height: 1.6;
- // 加粗文本样式
- strong {
- font-weight: 600;
- color: #1f2937;
- }
- // 斜体文本样式
- em {
- font-style: italic;
- color: #4b5563;
- }
- // 标题样式
- h3 {
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- margin: 16px 0 8px 0;
- border-bottom: 2px solid #e5e7eb;
- padding-bottom: 4px;
- }
- h4 {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 12px 0 6px 0;
- }
- // 列表样式
- ul {
- margin: 8px 0;
- padding-left: 20px;
- li {
- margin: 4px 0;
- line-height: 1.5;
- }
- }
- // 代码样式
- code {
- background: #f3f4f6;
- padding: 2px 6px;
- border-radius: 4px;
- font-family: 'Courier New', monospace;
- font-size: 14px;
- color: #dc2626;
- }
- // 换行样式
- br {
- margin: 4px 0;
- }
- }
- }
- .divider {
- height: 1px;
- background: #e5e7eb;
- margin: 16px 0 12px 0;
- }
- .message-actions {
- display: flex;
- justify-content: space-between;
- align-items: center;
- .left-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- }
- .right-actions {
- display: flex;
- }
- .action-btn {
- background: none;
- border: none;
- padding: 6px 5px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 12px;
- color: #6b7280;
- &:hover {
- color: #374151;
- background: rgba(59, 130, 246, 0.1);
- }
- .action-icon {
- width: 16px;
- height: 16px;
- object-fit: contain;
- }
- // 点赞和点踩按钮的特殊样式
- &.thumbs-up-btn {
- transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
- &:hover {
- color: #059669;
- transform: scale(1.1);
- }
- &.active {
- color: #059669;
- transform: scale(1.2);
- .action-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(142deg) brightness(104%) contrast(97%);
- }
- }
- &:active {
- transform: scale(0.9);
- transition: all 0.1s ease;
- }
- }
- &.thumbs-down-btn {
- transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
- &:hover {
- color: #dc2626;
- transform: scale(1.1);
- }
- &.active {
- color: #dc2626;
- transform: scale(1.2);
- .action-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(0deg) brightness(104%) contrast(97%) !important;
- }
- }
- &:active {
- transform: scale(0.9);
- transition: all 0.1s ease;
- }
- }
- }
- }
- }
- }
- /* 打字动画 */
- @keyframes thinking {
- 0%,
- 80%,
- 100% {
- transform: scale(0.8);
- opacity: 0.5;
- }
- 40% {
- transform: scale(1);
- opacity: 1;
- }
- }
- /* 功能卡片 */
- .function-cards {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16px;
- margin-top: 56px;
- max-width: 592px;
- .function-card {
- background: white;
- width: 288px;
- height: 112px;
- padding: 20px 0px;
- padding-left: 20px;
- border-radius: 12px;
- border: 1px solid #E5E8EB;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- &:hover {
- border-color: #667eea;
- transform: translateY(-2px);
- box-shadow: 0 4px 20px rgba(102, 126, 234, 0.1);
- }
- .card-header {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-bottom: 8px;
- .card-icon {
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #F3F5FF;
- border-radius: 8px;
- flex-shrink: 0;
- .card-icon-img {
- width: 40px;
- height: 40px;
- object-fit: contain;
- }
- }
- h4 {
- font-size: 16px;
- font-weight: 600;
- color: #2C3E50;
- margin: 0;
- line-height: 1.5;
- flex: 1;
- }
- }
- .card-description {
- margin-left: 55px;
- p {
- font-size: 14px;
- color: #7C8DB5;
- margin: 0;
- line-height: 1.4;
- }
- }
- }
- }
- /* 步骤二:培训大纲界面 */
- .step2-content {
- width: 100%;
- // max-width: 1200px;
- margin: 0 auto;
- .outline-container {
- display: flex;
- gap: 32px;
- height: 100%;
- }
- .outline-main {
- flex: 1;
- background: white;
- border-radius: 12px;
- width: 100px;
- min-height: 943px;
- height: calc(100vh - 80px);
- /* 自适应浏览器高度,减去头部和边距 */
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- margin-bottom: 20px;
- .outline-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24px;
- border-bottom: 1px solid #E5E7EB;
- .outline-title-container {
- flex: 1;
- min-width: 0;
- margin-right: 16px;
- }
- .outline-title {
- font-size: 22px;
- font-weight: 600;
- color: #1F2937;
- margin: 0;
- cursor: pointer;
- transition: all 0.2s ease;
- word-break: break-word;
- &:hover:not(.disabled) {
- color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- border-radius: 4px;
- padding: 4px 8px;
- margin: -4px -8px;
- }
- &.disabled {
- cursor: not-allowed;
- }
- }
- .outline-actions {
- display: flex;
- gap: 6px;
- // margin-right: 24px;
- .action-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- color: #374151;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.3s ease;
- background: none;
- border: none;
- &:hover {
- background: none;
- border-color: transparent;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- .exam-btn {
- color: #22B850;
- font-weight: 500;
- &:hover {
- background: none;
- color: #22B850;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- }
- .edit-input-container {
- flex: 1;
- min-width: 0;
- margin-right: 16px;
- }
- .edit-textarea {
- width: 100%;
- border: 2px solid #3E7BFA;
- border-radius: 6px;
- padding: 8px 12px;
- font-size: 12px;
- color: #6B7280;
- background: white;
- outline: none;
- transition: all 0.2s ease;
- resize: vertical;
- min-height: 60px;
- font-family: inherit;
- line-height: 1.6;
- &:focus {
- border-color: #3E7BFA;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- &.title-edit-textarea {
- font-size: 22px;
- font-weight: 600;
- color: #1F2937;
- min-height: 60px;
- }
- }
- .edit-input {
- width: 100%;
- border: 2px solid #3E7BFA;
- border-radius: 6px;
- padding: 8px 12px;
- font-size: inherit;
- font-weight: inherit;
- color: inherit;
- background: white;
- outline: none;
- transition: all 0.2s ease;
- &:focus {
- border-color: #2563EB;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- &.title-edit-input {
- font-size: 22px;
- font-weight: 600;
- color: #1F2937;
- }
- }
- }
- .outline-content {
- padding: 32px;
- height: calc(100% - 80px);
- /* 自适应高度,减去头部高度 */
- overflow-y: auto;
- position: relative;
- .outline-content-scrollable {
- height: 100%;
- /* 占满父容器高度 */
- overflow-y: auto;
- padding-right: 16px;
- /* 自定义滚动条样式 */
- &::-webkit-scrollbar {
- width: 8px;
- }
- &::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 4px;
- }
- &::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 4px;
- &:hover {
- background: #a8a8a8;
- }
- }
- }
- .zoom-controls {
- position: absolute;
- top: 16px;
- right: 16px;
- display: flex;
- gap: 4px;
- z-index: 10;
- margin-right: 12px;
- .zoom-btn {
- border: none;
- background: none;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- .zoom-icon {
- width: 16px;
- height: 16px;
- }
- }
- }
- .edit-input-container {
- flex: 1;
- margin-right: 8px;
- }
- .edit-input-wrapper {
- width: 100%;
- position: relative;
- }
- .edit-options-inline {
- position: absolute;
- right: 8px;
- top: 50%;
- transform: translateY(-50%);
- display: flex;
- gap: 4px;
- z-index: 10;
- .edit-option-btn {
- background: none;
- border: none;
- padding: 4px;
- border-radius: 3px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- &:hover {
- background: rgba(62, 123, 250, 0.1);
- transform: scale(1.1);
- }
- &.delete-btn:hover {
- background: rgba(239, 68, 68, 0.05);
- }
- .edit-icon {
- width: 16px;
- height: 16px;
- object-fit: contain;
- }
- }
- }
- .add-chapter-container {
- margin-top: 16px;
- padding: 16px;
- text-align: center;
- .add-chapter-btn {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 12px 24px;
- background: linear-gradient(135deg, #3E7BFA 0%, #5A9BFF 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(62, 123, 250, 0.4);
- background: linear-gradient(135deg, #2D6BFA 0%, #4A8BFF 100%);
- }
- &:active {
- transform: translateY(0);
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
- }
- .add-icon {
- width: 16px;
- height: 16px;
- object-fit: contain;
- filter: brightness(0) invert(1);
- }
- }
- }
- .edit-input {
- width: 100%;
- border: 2px solid #3E7BFA;
- border-radius: 6px;
- padding: 8px 12px;
- font-size: inherit;
- font-weight: inherit;
- color: inherit;
- background: white;
- outline: none;
- transition: all 0.2s ease;
- &:focus {
- border-color: #2563EB;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- &.title-edit-input {
- font-size: 22px;
- font-weight: 600;
- color: #1F2937;
- }
- &.chapter-edit-input {
- font-size: 20px;
- font-weight: 700;
- color: #111827;
- padding-right: 80px;
- /* 为图标留出空间 */
- }
- &.section-edit-input {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- padding-left: 24px;
- padding-right: 80px;
- /* 为图标留出空间 */
- }
- &.subsection-edit-input {
- font-size: 14px;
- font-weight: 400;
- color: #6B7280;
- padding-left: 48px;
- padding-right: 50px;
- /* 为图标留出空间,子小节只有删除按钮 */
- }
- &.subsubsection-edit-input {
- font-size: 13px;
- font-weight: 500;
- color: #9CA3AF;
- padding-right: 50px;
- }
- }
- .edit-textarea {
- width: 100%;
- border: 2px solid #3E7BFA;
- border-radius: 6px;
- padding: 8px 12px;
- font-size: 12px;
- color: #6B7280;
- background: white;
- outline: none;
- transition: all 0.2s ease;
- resize: vertical;
- min-height: 60px;
- font-family: inherit;
- line-height: 1.6;
- &:focus {
- border-color: #3E7BFA;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- &.title-edit-textarea {
- font-size: 22px;
- font-weight: 600;
- color: #1F2937;
- min-height: 50px;
- }
- &.chapter-edit-textarea {
- font-size: 18px;
- font-weight: 500;
- color: #374151;
- min-height: 60px;
- }
- &.section-edit-textarea {
- font-size: 16px;
- font-weight: 500;
- color: #4B5563;
- min-height: 50px;
- }
- &.subsection-edit-textarea {
- font-size: 14px;
- font-weight: 400;
- color: #6B7280;
- min-height: 45px;
- }
- &.subsubsection-edit-textarea {
- font-size: 12px;
- font-weight: 400;
- color: #6B7280;
- min-height: 40px;
- }
- }
- .outline-chapter {
- // margin-bottom: 32px;
- position: relative;
- cursor: move;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- border-radius: 8px;
- background: white;
- padding: 8px 0px 0px 8px;
- &:hover {
- border-color: #3E7BFA;
- box-shadow: 0 4px 12px rgba(62, 123, 250, 0.15);
- }
- &.dragging {
- opacity: 0.5;
- transform: rotate(2deg);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
- z-index: 1000;
- }
- &.drag-over {
- border-color: #10B981;
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(16, 185, 129, 0.1) 100%);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
- transform: scale(1.02);
- }
- // 拖拽提示点
- &::before {
- content: '';
- position: absolute;
- top: 12px;
- left: 12px;
- width: 4px;
- height: 4px;
- background: #9CA3AF;
- border-radius: 50%;
- opacity: 0;
- transition: opacity 0.3s ease;
- }
- &:hover::before {
- opacity: 1;
- }
- &.dragging::before {
- opacity: 0;
- }
- .chapter-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
- position: relative;
- }
- .chapter-title {
- font-size: 20px;
- font-weight: 700;
- color: #111827;
- margin: 0;
- cursor: pointer;
- transition: all 0.2s ease;
- flex: 1;
- &:hover {
- color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- border-radius: 4px;
- }
- }
- .outline-section {
- .section-container {
- margin-bottom: 16px;
- position: relative;
- }
- .section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- position: relative;
- }
- .section-title {
- font-size: 16px;
- color: #374151;
- margin: 0;
- line-height: 1.5;
- font-weight: 600;
- padding-left: 24px;
- position: relative;
- cursor: pointer;
- transition: all 0.2s ease;
- flex: 1;
- &:hover {
- color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- border-radius: 4px;
- }
- }
- .section-subsection {
- .subsection-container {
- margin-bottom: 8px;
- position: relative;
- }
- .subsection-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- }
- .subsection-title {
- font-size: 14px;
- color: #6B7280;
- margin: 0;
- line-height: 1.5;
- font-weight: 700;
- padding-left: 48px;
- position: relative;
- cursor: pointer;
- transition: all 0.2s ease;
- flex: 1;
- &:hover {
- color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- border-radius: 4px;
- }
- }
- // 具体内容要点样式(-开头)
- .subsubsection-container {
- margin-left: 20px;
- margin-top: 8px;
- .subsubsection-item {
- margin-bottom: 12px;
- border-left: 2px solid #E5E7EB;
- padding-left: 16px;
- }
- .subsubsection-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- margin-bottom: 8px;
- }
- .subsubsection-title {
- font-size: 13px;
- color: #6B7280;
- margin: 0;
- line-height: 1.4;
- font-weight: 400;
- cursor: pointer;
- transition: all 0.2s ease;
- flex: 1;
- &:hover {
- color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- border-radius: 4px;
- }
- }
- .subsubsection-content {
- margin-top: 0;
- .subsubsection-text {
- font-size: 12px;
- color: #6B7280;
- line-height: 1.6;
- cursor: pointer;
- padding: 0;
- background: transparent;
- border-radius: 0;
- border: none;
- transition: all 0.2s ease;
- min-height: 0;
- &:hover {
- background: transparent;
- border-color: transparent;
- }
- &:empty::before {
- content: "点击添加正文内容...";
- color: #9CA3AF;
- font-style: italic;
- }
- }
- }
- }
- }
- }
- // 编辑相关样式
- .edit-options {
- display: flex;
- gap: 4px;
- opacity: 0;
- transition: opacity 0.2s ease;
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- background: white;
- border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- padding: 4px;
- z-index: 10;
- }
- .outline-chapter:hover .edit-options,
- .section-container:hover .edit-options,
- .subsection-container:hover .edit-options {
- opacity: 1;
- }
- .edit-input-container {
- flex: 1;
- margin-right: 8px;
- }
- }
- }
- }
- }
- .outline-sidebar {
- width: 400px;
- display: flex;
- flex-direction: column;
- gap: 24px;
- .sidebar-section {
- background: white;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- .section-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 20px 20px 0px 20px;
- // background: #E8F0FF;
- .section-icon {
- width: 20px;
- height: 20px;
- }
- h5 {
- font-size: 16px;
- font-weight: 600;
- color: #1F2937;
- margin: 0;
- }
- }
- .section-content {
- padding: 20px 20px 12px 20px;
- .stat-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- .stat-label {
- font-size: 14px;
- color: #6B7280;
- }
- .stat-value {
- font-size: 14px;
- font-weight: 600;
- color: #1F2937;
- }
- }
- .tip-list {
- list-style: none;
- padding: 0;
- margin: 0;
- li {
- font-size: 14px;
- color: #6B7280;
- line-height: 1.6;
- margin-bottom: 8px;
- padding-left: 16px;
- position: relative;
- &:before {
- content: "•";
- position: absolute;
- left: 0;
- color: #3E7BFA;
- }
- }
- }
- .evaluation-question {
- font-size: 14px;
- color: #374151;
- margin: 0 0 16px 0;
- line-height: 1.5;
- }
- .evaluation-buttons {
- display: flex;
- gap: 12px;
- .eval-btn {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: 10px 16px;
- border: 1px solid #D1D5DB;
- border-radius: 8px;
- background: #F3F4F6;
- color: #4B5563;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.3s ease;
- &.satisfied {
- &.active {
- background: rgba(34, 184, 80, 0.1);
- color: #22B850;
- border-color: #22B850;
- }
- &:hover {
- background: rgba(34, 184, 80, 0.15);
- border-color: #22B850;
- }
- }
- &.unsatisfied {
- &.active {
- background: rgba(255, 77, 79, 0.1);
- color: #FF4D4F;
- border-color: #FF4D4F;
- }
- &:hover {
- background: rgba(255, 77, 79, 0.15);
- border-color: #FF4D4F;
- }
- }
- .eval-icon {
- width: 16px;
- height: 16px;
- filter: brightness(0) saturate(100%) invert(45%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(0%) contrast(0%);
- transition: filter 0.3s ease;
- }
- &.satisfied.active .eval-icon {
- filter: brightness(0) saturate(100%) invert(67%) sepia(61%) saturate(1237%) hue-rotate(86deg) brightness(95%) contrast(89%);
- }
- &.unsatisfied.active .eval-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2875%) hue-rotate(340deg) brightness(104%) contrast(107%);
- }
- }
- }
- }
- }
- .sidebar-actions {
- display: flex;
- gap: 12px;
- justify-content: center;
- margin-top: 12px;
- .action-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 16px 24px;
- border: none;
- border-radius: 8px;
- font-size: 15px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- &.secondary {
- background: white;
- color: #3E7BFA;
- border: 1px solid #3E7BFA;
- &:hover {
- background: none;
- border-color: transparent;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- &.primary {
- background: #3E7BFA;
- color: white;
- &:hover {
- background: #2563EB;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- &.exam {
- background: #10B981;
- color: white;
- &:hover {
- background: #059669;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- &.wps {
- background: #FF6B35;
- color: white;
- &:hover {
- background: #E55A2B;
- }
- .action-icon {
- width: 16px;
- height: 16px;
- }
- }
- }
- }
- }
- /* 推荐问题 */
- .recommended-questions {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- justify-content: center;
- max-width: 1000px;
- margin: 0 auto 0 auto;
- padding: 0 30px;
- .question-tag {
- background: white;
- padding: 9px 16px;
- border-radius: 20px;
- border: 1px solid #E5E8EB;
- font-size: 14px;
- color: #5A6C7D;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- gap: 8px;
- white-space: nowrap;
- &:hover {
- border-color: #667eea;
- color: #667eea;
- transform: translateY(-1px);
- }
- .question-icon {
- width: 16px;
- height: 16px;
- object-fit: contain;
- flex-shrink: 0;
- }
- }
- }
- /* 文件预览区域 */
- .file-preview-section {
- margin-bottom: 12px;
- .file-preview {
- position: relative;
- display: flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid #E5E7EB;
- border-radius: 12px;
- padding: 12px;
- max-width: 400px;
- .file-icon {
- font-size: 32px;
- margin-right: 12px;
- width: 48px;
- text-align: center;
-
- .file-icon-img {
- width: 32px;
- height: 32px;
- object-fit: contain;
- }
- }
- .file-info {
- flex: 1;
- .file-name {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- margin-bottom: 4px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 200px;
- }
- .file-size {
- font-size: 12px;
- color: #6B7280;
- }
- }
- .remove-file-btn {
- width: 24px;
- height: 24px;
- border: none;
- background: rgba(239, 68, 68, 0.1);
- color: #DC2626;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- &:hover {
- background: rgba(239, 68, 68, 0.2);
- transform: scale(1.1);
- }
- .remove-icon {
- font-size: 16px;
- font-weight: bold;
- }
- }
- }
- }
- /* 底部输入区域 */
- .chat-input-section {
- background: transparent;
- padding: 9px 30px;
- .input-container {
- max-width: 900px;
- margin: 0 auto;
- .input-box {
- display: flex;
- align-items: center;
- // gap: 12px;
- background: white;
- border-radius: 16px;
- padding: 8px 20px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transition: box-shadow 0.3s ease;
- border: 1px solid #3E7BFA;
- height: 57px;
- &:focus-within {
- box-shadow: 0 2px 12px rgba(62, 123, 250, 0.2);
- }
- .attach-btn,
- .voice-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 8px;
- border-radius: 6px;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- &:hover:not(:disabled) {
- background: rgba(102, 126, 234, 0.1);
- }
- &:disabled {
- cursor: not-allowed;
- }
- &.recording {
- background: rgba(239, 68, 68, 0.1);
- animation: pulse 1.5s ease-in-out infinite;
- }
- .icon-container {
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- position: relative;
- }
- .recording-indicator {
- position: absolute;
- top: -2px;
- right: -2px;
- width: 8px;
- height: 8px;
- background: #ef4444;
- border-radius: 50%;
- animation: blink 1s ease-in-out infinite;
- }
- .action-icon {
- width: 20px;
- height: 20px;
- height: 20px !important;
- max-width: 20px !important;
- max-height: 20px !important;
- object-fit: contain;
- flex-shrink: 0;
- }
- }
- .message-input {
- flex: 1;
- border: none;
- background: transparent;
- font-size: 16px;
- color: #2C3E50;
- outline: none;
- transition: opacity 0.3s ease;
- &::placeholder {
- color: #A0A6B8;
- }
- &:disabled {
- cursor: not-allowed;
- }
- }
- .divider {
- width: 1px;
- height: 31px;
- background-color: #D6D5DE;
- margin: 0 4px;
- }
- .send-btn {
- background: none;
- border: none;
- cursor: pointer;
- border-radius: 6px;
- margin-left: 12px;
- transition: background 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- &:hover:not(:disabled) {
- background: rgba(102, 126, 234, 0.1);
- }
- &:disabled {
- cursor: not-allowed;
- }
- .send-icon {
- width: 90px;
- height: 40px;
- object-fit: contain;
- }
- }
- }
- }
- }
- /* 步骤三:PPT模板选择界面 */
- .step3-content {
- width: 100%;
- min-height: 888px;
- height: calc(100vh - 160px);
- /* 自适应浏览器高度,减去头部和边距 */
- margin: 0 auto;
- .preview-actions {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- margin-top: 24px;
- padding-right: 347px;
- .action-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px 20px;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- &.secondary {
- background: white;
- color: #3E7BFA;
- border: 1px solid #3E7BFA;
- &:hover {
- background: rgba(62, 123, 250, 0.05);
- }
- }
- &.primary {
- background: #3E7BFA;
- color: white;
- &:hover {
- background: #2563EB;
- }
- &:disabled {
- background: #9CA3AF;
- cursor: not-allowed;
- opacity: 0.6;
- }
- }
- }
- }
- .template-container {
- display: flex;
- gap: 18px;
- height: 100%;
- }
- .template-preview {
- flex: 1;
- background: white;
- border-radius: 12px;
- padding: 21px 41px 0px 25px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- .preview-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24px;
- .preview-title {
- font-size: 16px;
- font-weight: 600;
- color: #1F2937;
- margin: 0;
- }
- .save-status {
- font-size: 14px;
- color: #6B7280;
- }
- }
- .main-carousel {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 27px;
- }
- // PPT编辑工作台样式
- .ppt-editor-workspace {
- width: 100%;
- .editor-canvas {
- // background: white;
- // border: 2px solid #E5E7EB;
- // border-radius: 12px;
- // padding: 40px;
- // min-height: 603px;
- box-shadow: none;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 603px;
- // PPT预览模式样式
- .slide-preview {
- width: 100%;
- max-width: 1105px;
- height: 603px;
- position: relative;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
- background: transparent;
- .ppt-preview-tip {
- position: absolute;
- top: 10px;
- left: 10px;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 8px 12px;
- border-radius: 6px;
- font-size: 12px;
- z-index: 1000;
- p {
- margin: 0;
- }
- }
- .preview-element {
- position: absolute;
- cursor: pointer;
- // border: 2px solid transparent;
- transition: border-color 0.2s ease;
- user-select: none;
- &.selected {
- border: 2px solid transparent;
- box-shadow: none;
- }
- &:hover {
- border: 2px solid transparent;
- }
- .resize-handle {
- display: none;
- }
- .drag-handle {
- display: none;
- }
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- div {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding: 10px;
- box-sizing: border-box;
- }
- .inline-editor {
- outline: 2px dashed #4c9ffe;
- background: rgba(255, 255, 255, 0.2);
- direction: ltr !important;
- text-align: center !important;
- unicode-bidi: normal !important;
- }
- .shape {
- width: 100%;
- height: 100%;
- }
- }
- .preview-element {
- position: absolute;
- cursor: pointer;
- transition: all 0.2s ease;
- &.selected {
- outline: none;
- outline-offset: 0;
- }
- .resize-handle {
- display: none;
- }
- .drag-handle {
- display: none;
- }
- .inline-editor {
- width: 100%;
- height: 100%;
- outline: none;
- border: 1px solid #3E7BFA;
- background: rgba(255, 255, 255, 0.9);
- padding: 4px;
- border-radius: 4px;
- }
- .shape {
- width: 100%;
- height: 100%;
- }
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: 0;
- }
- }
- }
- // 编辑模式样式
- .edit-mode {
- width: 100%;
- height: 100%;
- .slide-editor {
- width: 100%;
- height: 100%;
- .slide-content {
- outline: none;
- .slide-title {
- font-size: 32px;
- font-weight: 700;
- color: #1F2937;
- margin: 0 0 24px 0;
- text-align: center;
- border-bottom: 3px solid #3E7BFA;
- padding-bottom: 16px;
- &:focus {
- background: rgba(62, 123, 250, 0.05);
- border-radius: 8px;
- padding: 8px;
- margin: -8px -8px 16px -8px;
- }
- }
- .slide-body {
- font-size: 18px;
- color: #374151;
- line-height: 1.6;
- p {
- margin: 16px 0;
- &:focus {
- background: rgba(62, 123, 250, 0.05);
- border-radius: 6px;
- padding: 8px;
- margin: 8px -8px;
- }
- }
- }
- }
- }
- }
- }
- }
- .carousel-btn {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- background: #FFFFFF80;
- border: 1px solid #D1D5DB;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s ease;
- z-index: 10;
- &:hover {
- background: none;
- border-color: transparent;
- }
- &.prev {
- left: 18px;
- }
- &.next {
- right: 18px;
- }
- .carousel-icon {
- width: 6px;
- height: 10px;
- }
- }
- .main-slide {
- width: 100%;
- max-width: 1105px;
- height: 603px;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- .slide-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
- }
- .thumbnail-nav {
- display: flex;
- flex-direction: column;
- align-items: center;
- .slide-counter {
- font-size: 14px;
- color: #6B7280;
- text-align: center;
- margin-bottom: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 16px;
- .progress-dots {
- display: flex;
- gap: 4px;
- align-items: center;
- .progress-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #D1D5DB;
- cursor: pointer;
- transition: all 0.3s ease;
- &:hover {
- background: #D1D5DB;
- transform: scale(1.2);
- }
- &.active {
- background: #3E7BFA;
- width: 16px;
- border-radius: 8px;
- transform: scale(1);
- box-shadow: 0 0 6px rgba(62, 123, 250, 0.4);
- }
- }
- }
- }
- .thumbnail-strip {
- display: flex;
- gap: 11px;
- max-width: 1050px;
- /* 限制最大宽度 */
- margin: 0 auto;
- /* 居中显示 */
- overflow-x: auto;
- /* 横向滚动 */
- justify-content: flex-start;
- /* 从左侧开始排列 */
- // padding: 0 10px;
- /* 自定义滚动条样式 */
- &::-webkit-scrollbar {
- height: 6px;
- }
- &::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 3px;
- }
- &::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 3px;
- &:hover {
- background: #a8a8a8;
- }
- }
- .thumbnail-item {
- width: 135px;
- height: 79px;
- border-radius: 6px;
- overflow: hidden;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s ease;
- position: relative;
- flex-shrink: 0;
- /* 防止缩略图被压缩 */
- margin: 0 auto;
- /* 确保单个缩略图也能居中 */
- &:hover {
- border-color: #3E7BFA;
- transform: translateY(-2px);
- }
- &.active {
- border-color: #3E7BFA;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
- }
- .thumbnail-preview {
- width: 100%;
- height: 100%;
- position: relative;
- overflow: hidden;
- border-radius: 4px;
- }
- .thumbnail-content {
- width: 100%;
- height: 100%;
- position: relative;
- transform: scale(0.22);
- transform-origin: -70px -20px;
- }
- .thumbnail-element {
- position: absolute;
- transform: scale(1);
- }
- .thumbnail-element img {
- max-width: 100%;
- max-height: 100%;
- object-fit: contain;
- }
- .thumbnail-element div {
- font-size: 6px;
- line-height: 1.1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- word-break: break-word;
- /* 确保文本样式与主图一致 */
- font-weight: inherit;
- text-align: inherit;
- text-shadow: inherit;
- }
- .thumbnail-element div p {
- margin: 0;
- padding: 0;
- font-size: inherit;
- line-height: inherit;
- color: inherit;
- font-weight: inherit;
- text-align: inherit;
- text-shadow: inherit;
- }
- .thumbnail-element div strong {
- font-weight: bold;
- }
- .thumbnail-element div span {
- font-size: inherit;
- color: inherit;
- font-weight: inherit;
- }
- .thumbnail-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .thumbnail-number {
- position: absolute;
- bottom: 4px;
- right: 4px;
- background: rgba(0, 0, 0, 0.5);
- color: #FFFFFF;
- font-size: 12px;
- font-weight: 500;
- padding: 2px 6px;
- border-radius: 3.81px;
- line-height: 1;
- min-width: 16px;
- text-align: center;
- }
- }
- }
- }
- }
- .template-sidebar {
- background: #FFFFFF;
- border-radius: 8px;
- // width: 320px;
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 20px 19px 16px 19px;
- .sidebar-title {
- font-size: 15px;
- font-weight: 600;
- color: #1F2937;
- margin: 0 0 16px 0;
- }
- .template-list {
- display: flex;
- flex-direction: column;
- gap: 20px;
- .template-item {
- background: white;
- border-radius: 12px;
- padding: 16px;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid #E4E4E4;
- &:hover {
- border-color: #E5E7EB;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- &.active {
- border-color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- }
- .template-thumbnail {
- width: 100%;
- height: 144px;
- width: 256px;
- border-radius: 8px;
- overflow: hidden;
- margin-bottom: 12px;
- border: none;
- position: relative;
- .template-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border: none !important;
- outline: none !important;
- }
- .dynamic-badge {
- position: absolute;
- top: 8px;
- right: 8px;
- background: #10b981;
- color: white;
- font-size: 12px;
- padding: 4px 8px;
- border-radius: 4px;
- font-weight: 600;
- z-index: 10;
- }
- }
- .template-info {
- .template-title {
- font-size: 15px;
- font-weight: 600;
- color: #1F2937;
- margin: 0 0 8px 0;
- line-height: 1.4;
- }
- .template-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- .update-time {
- font-size: 13px;
- color: #6B7280;
- }
- .page-count {
- font-size: 13px;
- color: #6B7280;
- font-weight: 500;
- }
- }
- .template-description {
- font-size: 13px;
- color: #4a5568;
- margin-top: 8px;
- line-height: 1.4;
- }
- }
- }
- }
- .dynamic-preview {
- margin-top: 16px;
- padding: 16px;
- background: #f7fafc;
- border-radius: 8px;
- border: 1px solid #e2e8f0;
- .preview-title {
- font-size: 14px;
- font-weight: 600;
- color: #2d3748;
- margin: 0 0 12px 0;
- }
- .preview-stats {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- margin-bottom: 12px;
- .stat-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 6px 8px;
- background: white;
- border-radius: 4px;
- border: 1px solid #e2e8f0;
- .stat-label {
- font-size: 12px;
- color: #718096;
- }
- .stat-value {
- font-size: 12px;
- font-weight: 600;
- color: #2d3748;
- &.complexity-simple {
- color: #10b981;
- }
- &.complexity-medium {
- color: #f59e0b;
- }
- &.complexity-complex {
- color: #ef4444;
- }
- }
- }
- }
- .recommendations,
- .warnings {
- margin-top: 12px;
- .recommendations-title,
- .warnings-title {
- font-size: 12px;
- font-weight: 600;
- color: #2d3748;
- margin: 0 0 8px 0;
- }
- .recommendations-list,
- .warnings-list {
- margin: 0;
- padding-left: 16px;
- li {
- font-size: 12px;
- color: #4a5568;
- margin-bottom: 4px;
- line-height: 1.3;
- }
- }
- &.warnings {
- .warnings-title {
- color: #ef4444;
- }
- .warnings-list li {
- color: #dc2626;
- }
- }
- }
- }
- }
- .template-sidebar {
- background: #FFFFFF;
- border-radius: 8px;
- // width: 320px;
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 20px 19px 16px 19px;
- .sidebar-title {
- font-size: 15px;
- font-weight: 600;
- color: #1F2937;
- // margin: 0 0 16px 0;
- }
- .template-list {
- display: flex;
- flex-direction: column;
- gap: 20px;
- .template-item {
- background: white;
- border-radius: 12px;
- padding: 16px;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid #E4E4E4;
- &:hover {
- border-color: #E5E7EB;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- &.active {
- border-color: #3E7BFA;
- background: rgba(62, 123, 250, 0.05);
- }
- .template-thumbnail {
- width: 100%;
- height: 144px;
- width: 256px;
- border-radius: 8px;
- overflow: hidden;
- margin-bottom: 12px;
- .template-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
- .template-info {
- .template-title {
- font-size: 15px;
- font-weight: 600;
- color: #1F2937;
- margin: 0 0 8px 0;
- line-height: 1.4;
- }
- .template-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- .update-time {
- font-size: 13px;
- color: #6B7280;
- }
- .page-count {
- font-size: 13px;
- color: #6B7280;
- font-weight: 500;
- }
- }
- }
- }
- }
- // 下载选项样式
- .download-content {
- .download-options {
- display: flex;
- flex-direction: column;
- gap: 16px;
- .download-option {
- background: #F9FAFB;
- border-radius: 8px;
- padding: 13px;
- width: 292px;
- height: 66px;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- display: flex;
- align-items: center;
- gap: 12px;
- position: relative;
- &:hover {
- background: none;
- border-color: transparent;
- }
- &.active {
- background: white;
- border-color: #3E7BFA;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.1);
- }
- .option-icon {
- width: 40px;
- height: 40px;
- flex-shrink: 0;
- .option-img {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- }
- .option-info {
- flex: 1;
- .option-title {
- font-size: 16px;
- font-weight: 600;
- color: #111827;
- margin: 0 0 4px 0;
- }
- .option-description {
- font-size: 12px;
- color: #6B7280;
- margin: 0;
- line-height: 1.4;
- }
- }
- .option-check {
- width: 24px;
- height: 24px;
- flex-shrink: 0;
- .check-icon {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- }
- }
- }
- .download-actions {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 485px;
- .action-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px 20px;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- &.secondary {
- background: white;
- color: #3E7BFA;
- border: 1px solid #3E7BFA;
- &:hover {
- background: rgba(62, 123, 250, 0.05);
- }
- }
- &.primary {
- background: #3E7BFA;
- color: white;
- &:hover {
- background: #2563EB;
- }
- .download-icon {
- width: 16px;
- height: 16px;
- }
- }
- }
- }
- }
- }
- </style>
|