HazardDetection.vue 178 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147
  1. <template>
  2. <div class="chat-container">
  3. <!-- 最左侧边栏 -->
  4. <Sidebar />
  5. <!-- 中间历史记录区域 -->
  6. <div class="history-sidebar">
  7. <div class="history-header">
  8. <span class="section-title">历史记录</span>
  9. <img
  10. src="@/assets/Chat/2.png"
  11. alt="新建任务"
  12. class="new-chat-btn"
  13. @click="createNewChat"
  14. />
  15. </div>
  16. <div class="history-list">
  17. <!-- 历史记录加载状态 -->
  18. <div
  19. v-if="isLoadingHistory && historyTotal === 0"
  20. class="history-loading"
  21. >
  22. <div class="loading-spinner"></div>
  23. <div class="loading-text">正在加载历史记录...</div>
  24. </div>
  25. <!-- 有数据时显示历史记录列表 -->
  26. <div
  27. v-else-if="historyTotal > 0"
  28. v-for="(item, index) in historyData"
  29. :key="item.id"
  30. :class="['history-item', { active: item.isActive }]"
  31. @click="handleHistoryItem(item, $event)"
  32. >
  33. <div class="history-content">
  34. <div class="title-row">
  35. <div class="history-title">{{ item.title }}</div>
  36. <span
  37. :class="[
  38. 'history-tag',
  39. getTagClass(item.tagType),
  40. ]"
  41. >{{ getTagText(item.tagType) }}</span
  42. >
  43. </div>
  44. <div class="time-row">
  45. <span class="history-time">{{ item.time }}</span>
  46. </div>
  47. <div class="desc-row">
  48. <div class="history-icon">
  49. <img
  50. :src="item.originalImageUrl"
  51. alt="隐患图标"
  52. class="history-icon-img"
  53. @error="handleHistoryImageError($event, item)"
  54. />
  55. </div>
  56. <div class="history-desc">
  57. {{ item.description }}
  58. </div>
  59. </div>
  60. </div>
  61. <div
  62. class="delete-btn"
  63. @click.stop="deleteHistoryItem(item, index)"
  64. :class="{ 'always-visible': item.isActive }"
  65. >
  66. <img
  67. src="/src/assets/AIWriting/8.png"
  68. alt="删除"
  69. class="delete-icon"
  70. />
  71. </div>
  72. </div>
  73. <!-- 无数据时显示空状态 -->
  74. <div v-else class="empty-history">
  75. <img
  76. src="@/assets/Chat/22.png"
  77. alt="暂无数据"
  78. class="empty-icon"
  79. />
  80. <div class="empty-text">暂无数据</div>
  81. </div>
  82. </div>
  83. </div>
  84. <!-- 右侧工作区域 -->
  85. <div class="main-work">
  86. <!-- 头部 -->
  87. <div class="work-header">
  88. <h2>隐患提示</h2>
  89. </div>
  90. <!-- 工作内容区域 -->
  91. <div class="work-content">
  92. <!-- 主界面:隐患提示系统 -->
  93. <div v-if="currentView === 'main'" class="main-layout">
  94. <!-- 左侧:隐患提示系统 -->
  95. <div
  96. class="left-section"
  97. :class="{ transitioning: isTransitioning }"
  98. >
  99. <div class="hazard-system">
  100. <div class="system-header">
  101. <h3>智能隐患提示系统</h3>
  102. <p>
  103. 基于AI技术的工程安全智能隐患提示系统,实时检测分析,提供专业评估和预防建议
  104. </p>
  105. </div>
  106. <!-- 步骤一:上传图片 -->
  107. <div class="step-section">
  108. <h4>步骤一:上传需要识别的场景图片</h4>
  109. <p class="step-description">
  110. 系统将自动识别场景与关键要素,无需手动选择
  111. </p>
  112. <div
  113. class="upload-area"
  114. @click="triggerFileUpload"
  115. @drop="handleDrop"
  116. @dragover="handleDragOver"
  117. @dragleave="handleDragLeave"
  118. :class="{ 'drag-over': isDragOver }"
  119. >
  120. <!-- 显示上传的图片 -->
  121. <div
  122. v-if="uploadedImageUrl"
  123. class="uploaded-image-container"
  124. >
  125. <img
  126. :src="uploadedImageUrl"
  127. alt="已上传的图片"
  128. class="uploaded-image"
  129. />
  130. <div class="image-overlay">
  131. <button
  132. class="change-image-btn"
  133. @click.stop="reselectImage"
  134. >
  135. 更换图片
  136. </button>
  137. </div>
  138. </div>
  139. <!-- 显示上传区域 -->
  140. <div v-else class="upload-content">
  141. <img
  142. src="@/assets/Hazard/5.png"
  143. alt="上传图标"
  144. class="upload-icon"
  145. />
  146. <p class="upload-text">点击上传图片</p>
  147. <p class="upload-format">
  148. 支持JPG、PNG格式,单个文件不超过5MB
  149. </p>
  150. <!-- <p class="drag-hint">或拖拽图片到此处</p> -->
  151. <button
  152. class="select-file-btn"
  153. @click.stop="triggerFileUpload"
  154. >
  155. 选择图片文件
  156. </button>
  157. </div>
  158. <!-- 上传状态指示器 -->
  159. <div
  160. v-if="isUploading"
  161. class="upload-status"
  162. >
  163. <div class="loading-spinner"></div>
  164. <p>正在上传...</p>
  165. </div>
  166. <!-- 识别状态指示器 -->
  167. <div
  168. v-if="isIdentifying"
  169. class="upload-status"
  170. >
  171. <div class="loading-spinner"></div>
  172. <p>正在识别隐患...</p>
  173. </div>
  174. <input
  175. ref="fileInput"
  176. type="file"
  177. accept="image/*"
  178. @change="handleFileUpload"
  179. style="display: none"
  180. />
  181. </div>
  182. </div>
  183. <!-- 开始识别按钮 -->
  184. <div class="action-section">
  185. <button
  186. class="start-identify-btn"
  187. @click="startIdentification"
  188. :disabled="isIdentifying"
  189. :class="{ 'btn-disabled': isIdentifying }"
  190. >
  191. <img
  192. :src="
  193. uploadedImageUrl
  194. ? startIdentifyActiveImg
  195. : startIdentifyImg
  196. "
  197. alt="开始识别"
  198. class="btn-bg"
  199. />
  200. </button>
  201. </div>
  202. </div>
  203. </div>
  204. <!-- 右侧:使用流程 -->
  205. <div
  206. class="right-section"
  207. :class="{ 'slide-out': isTransitioning }"
  208. >
  209. <div class="process-card">
  210. <h3>使用流程</h3>
  211. <div class="process-section">
  212. <div class="process-flow">
  213. <!-- 流程步骤1 -->
  214. <div class="process-step">
  215. <div class="step-icon-wrapper">
  216. <el-icon
  217. class="step-icon"
  218. :size="40"
  219. color="#3E7BFA"
  220. >
  221. <Upload />
  222. </el-icon>
  223. <div class="step-number">1</div>
  224. </div>
  225. <div class="step-content">
  226. <div class="step-title">
  227. 上传图片
  228. </div>
  229. <div class="step-desc">
  230. 选择相关场景的图片
  231. </div>
  232. </div>
  233. </div>
  234. <!-- 连接线 -->
  235. <div class="step-connector"></div>
  236. <!-- 流程步骤2 -->
  237. <div class="process-step">
  238. <div class="step-icon-wrapper">
  239. <el-icon
  240. class="step-icon"
  241. :size="40"
  242. color="#3E7BFA"
  243. >
  244. <View />
  245. </el-icon>
  246. <div class="step-number">2</div>
  247. </div>
  248. <div class="step-content">
  249. <div class="step-title">
  250. 场景识别
  251. </div>
  252. <div class="step-desc">
  253. 智能识别场景要素
  254. </div>
  255. </div>
  256. </div>
  257. <!-- 连接线 -->
  258. <div class="step-connector"></div>
  259. <!-- 流程步骤3 -->
  260. <div class="process-step">
  261. <div class="step-icon-wrapper">
  262. <el-icon
  263. class="step-icon"
  264. :size="40"
  265. color="#3E7BFA"
  266. >
  267. <DataAnalysis />
  268. </el-icon>
  269. <div class="step-number">3</div>
  270. </div>
  271. <div class="step-content">
  272. <div class="step-title">
  273. 场景分析
  274. </div>
  275. <div class="step-desc">
  276. 智能分析常见隐患
  277. </div>
  278. </div>
  279. </div>
  280. <!-- 连接线 -->
  281. <div class="step-connector"></div>
  282. <!-- 流程步骤4 -->
  283. <div class="process-step">
  284. <div class="step-icon-wrapper">
  285. <el-icon
  286. class="step-icon"
  287. :size="40"
  288. color="#3E7BFA"
  289. >
  290. <Bell />
  291. </el-icon>
  292. <div class="step-number">4</div>
  293. </div>
  294. <div class="step-content">
  295. <div class="step-title">
  296. 隐患提示
  297. </div>
  298. <div class="step-desc">
  299. 提示场景常见隐患
  300. </div>
  301. </div>
  302. </div>
  303. </div>
  304. </div>
  305. </div>
  306. </div>
  307. </div>
  308. <!-- 详情页:隐患提示结果 -->
  309. <div v-if="currentView === 'detail'" class="detail-view">
  310. <!-- 顶部标题栏 -->
  311. <div class="detail-header">
  312. <div class="header-left">
  313. <svg
  314. class="header-icon-svg"
  315. viewBox="0 0 1024 1024"
  316. xmlns="http://www.w3.org/2000/svg"
  317. fill="currentColor"
  318. >
  319. <path
  320. d="M957.217391 86.372174C957.217391 86.372174 957.217391 608.211478 957.217391 608.211478 957.217391 639.510261 949.782261 670.630957 934.956522 701.573565 920.086261 732.605217 900.674783 762.568348 876.633043 791.685565 852.591304 820.758261 825.121391 848.317217 794.267826 874.273391 763.369739 900.274087 732.070957 923.425391 700.326957 943.727304 668.538435 964.073739 637.68487 980.992 607.721739 994.437565 577.758609 1007.88313 551.490783 1017.09913 528.918261 1022.130087 528.918261 1022.130087 518.233043 1024 518.233043 1024 518.233043 1024 508.438261 1022.130087 508.438261 1022.130087 485.286957 1017.09913 458.440348 1007.88313 427.853913 994.437565 397.267478 980.992 365.523478 964.073739 332.577391 943.727304 299.631304 923.425391 267.308522 900.274087 235.52 874.273391 203.776 848.317217 175.415652 820.758261 150.483478 791.685565 125.551304 762.568348 105.382957 732.605217 89.978435 701.573565 74.48487 670.630957 66.782609 639.510261 66.782609 608.211478 66.782609 608.211478 66.782609 86.372174 66.782609 86.372174 66.782609 86.372174 103.290435 80.717913 103.290435 80.717913 103.290435 80.717913 512.890435 0 512.890435 0 512.890435 0 930.504348 80.717913 930.504348 80.717913 930.504348 80.717913 957.217391 86.372174 957.217391 86.372174 957.217391 86.372174 957.217391 86.372174 957.217391 86.372174ZM513.024 75.553391C513.024 75.553391 508.082087 74.529391 508.082087 74.529391 508.082087 74.529391 156.538435 137.527652 156.538435 137.527652 156.538435 137.527652 156.538435 466.765913 156.538435 466.765913 156.538435 466.765913 513.024 466.765913 513.024 466.765913 513.024 466.765913 513.024 75.553391 513.024 75.553391 513.024 75.553391 513.024 75.553391 513.024 75.553391ZM867.461565 466.765913C867.461565 466.765913 513.024 466.765913 513.024 466.765913 513.024 466.765913 513.024 935.401739 513.024 935.401739 535.81913 929.881043 560.617739 921.466435 587.419826 910.113391 614.177391 898.760348 640.623304 885.359304 666.713043 869.865739 692.847304 854.372174 717.957565 837.186783 742.13287 818.265043 766.308174 799.343304 787.634087 778.99687 806.288696 757.314783 824.898783 735.677217 839.724522 713.149217 850.810435 689.730783 861.94087 666.35687 867.461565 642.582261 867.461565 618.496 867.461565 618.496 867.461565 466.765913 867.461565 466.765913 867.461565 466.765913 867.461565 466.765913 867.461565 466.765913Z"
  321. />
  322. </svg>
  323. <div class="header-text">
  324. <span class="main-title">{{
  325. detectionResult?.scene_name
  326. ? scenarios[detectionResult.scene_name]
  327. ?.name
  328. : "隐患提示结果"
  329. }}</span>
  330. </div>
  331. </div>
  332. <div class="header-right">
  333. <div class="current-time">
  334. {{
  335. selectedHistoryItem?.time ||
  336. getCurrentTime()
  337. }}
  338. </div>
  339. </div>
  340. </div>
  341. <!-- 主要内容区域 -->
  342. <div class="detail-content">
  343. <!-- 加载状态 -->
  344. <div
  345. v-if="isLoadingDetail || isImageLoading"
  346. class="loading-overlay"
  347. >
  348. <div class="loading-spinner"></div>
  349. <p>
  350. {{
  351. isLoadingDetail
  352. ? "正在加载详情..."
  353. : "正在加载图片..."
  354. }}
  355. </p>
  356. </div>
  357. <!-- 图片显示区域 -->
  358. <div class="image-section">
  359. <!-- 点评状态 -->
  360. <div
  361. class="evaluation-status"
  362. @click="openEvaluationModal"
  363. >
  364. <span
  365. :class="[
  366. 'status-badge',
  367. selectedHistoryItem?.effect_evaluation >
  368. 0
  369. ? 'evaluated'
  370. : 'not-evaluated',
  371. ]"
  372. >
  373. {{
  374. selectedHistoryItem?.effect_evaluation >
  375. 0
  376. ? "已点评"
  377. : "未点评"
  378. }}
  379. </span>
  380. </div>
  381. <div class="image-container" ref="imageContainerRef">
  382. <!-- 显示图片:扫描时显示原图,扫描完成后显示标注图 -->
  383. <img
  384. ref="mainImageRef"
  385. :src="
  386. showScanningEffect
  387. ? uploadedImageUrl
  388. : annotatedImageUrl
  389. "
  390. :alt="
  391. showScanningEffect
  392. ? '用户上传图片'
  393. : '隐患提示图片'
  394. "
  395. class="main-image"
  396. @click="openImagePreview()"
  397. style="
  398. cursor: pointer;
  399. transform: none !important;
  400. "
  401. @load="handleMainImageLoad"
  402. @error="handleMainImageError"
  403. />
  404. <div
  405. v-if="
  406. !showScanningEffect &&
  407. selectedKeyElement &&
  408. elementOverlayStyle
  409. "
  410. ref="elementCardRef"
  411. class="element-overlay-card"
  412. :style="elementOverlayStyle"
  413. @click.stop
  414. >
  415. <div class="element-card-title">
  416. 当前选中:{{ selectedKeyElement }}
  417. </div>
  418. <ul
  419. v-if="filteredHazards.length"
  420. class="element-card-list"
  421. >
  422. <li
  423. v-for="(
  424. hazard, index
  425. ) in filteredHazards"
  426. :key="index"
  427. >
  428. {{ hazard }}
  429. </li>
  430. </ul>
  431. <div v-else class="element-card-empty">
  432. 暂无对应隐患
  433. </div>
  434. </div>
  435. <!-- 扫描效果覆盖层 -->
  436. <div
  437. v-if="showScanningEffect"
  438. class="scanning-overlay"
  439. >
  440. <div class="scanning-line"></div>
  441. </div>
  442. </div>
  443. </div>
  444. <!-- 识别结果分析 -->
  445. <div class="analysis-section">
  446. <div class="analysis-header">
  447. <div class="robot-avatar">
  448. <img
  449. src="@/assets/Hazard/21.png"
  450. alt="蜀安AI助手"
  451. class="robot-img"
  452. />
  453. </div>
  454. <div class="header-title">
  455. <!-- 扫描期间显示分析提示 -->
  456. <div
  457. v-if="showAnalysisPrompt"
  458. class="analysis-prompt"
  459. >
  460. <div class="typing-indicator">
  461. <span class="dot"></span>
  462. <span class="dot"></span>
  463. <span class="dot"></span>
  464. </div>
  465. <span class="prompt-text"
  466. >蜀安AI助手正在为您智能分析图片,请稍候…</span
  467. >
  468. </div>
  469. <!-- 扫描完成后显示标题 -->
  470. <h3 v-else class="analysis-title">
  471. 蜀道安全管理AI智能助手慧眼识图分析出以下结果
  472. </h3>
  473. </div>
  474. </div>
  475. <!-- 分析结果内容区域 -->
  476. <div
  477. class="analysis-body"
  478. v-if="!showAnalysisPrompt"
  479. >
  480. <div class="analysis-text">
  481. <span
  482. v-if="isStreamingAnalysis"
  483. v-html="streamingAnalysis"
  484. class="streaming-text"
  485. ></span>
  486. <span
  487. v-else-if="
  488. !isStreamingAnalysis &&
  489. detectionResult
  490. "
  491. >
  492. 我识别到这是一个<span
  493. class="scene-tag"
  494. >{{
  495. detectionResult?.scene_name
  496. ? scenarios[
  497. detectionResult
  498. .scene_name
  499. ]?.name
  500. : "未知场景"
  501. }}</span
  502. >场景,检测到的关键要素为<span
  503. v-for="(label, index) in displayLabels"
  504. :key="index"
  505. class="label-tag"
  506. >{{ label }}</span
  507. >
  508. </span>
  509. </div>
  510. <p
  511. class="hazards-intro"
  512. v-if="
  513. !isStreamingAnalysis && detectionResult
  514. "
  515. >
  516. 根据安全规范和施工标准,我为您梳理出以下需要重点关注的安全隐患
  517. </p>
  518. <div
  519. v-if="
  520. !isStreamingAnalysis &&
  521. detectionResult &&
  522. keyElements.length
  523. "
  524. class="key-elements-section"
  525. >
  526. <div class="key-elements-title">
  527. 关键要素
  528. </div>
  529. <div class="key-elements-buttons">
  530. <button
  531. v-for="element in keyElements"
  532. :key="element"
  533. :class="[
  534. 'key-element-btn',
  535. {
  536. active:
  537. selectedKeyElement ===
  538. element,
  539. },
  540. ]"
  541. @click="
  542. toggleKeyElement(element)
  543. "
  544. >
  545. {{ element }}
  546. </button>
  547. </div>
  548. <div
  549. v-if="!selectedKeyElement"
  550. class="key-elements-hint"
  551. >
  552. 点击关键要素可查看对应隐患
  553. </div>
  554. </div>
  555. <!-- 场景隐患列表 -->
  556. <div
  557. class="hazards-section"
  558. v-if="!showAnalysisPrompt"
  559. >
  560. <div
  561. class="hazards-content"
  562. :class="{
  563. 'scanning-mode': showScanningEffect,
  564. }"
  565. >
  566. <!-- 扫描期间的遮罩层 -->
  567. <div
  568. v-if="showScanningEffect"
  569. class="hazards-loading-overlay"
  570. >
  571. <div class="loading-spinner"></div>
  572. <p>正在分析场景隐患...</p>
  573. </div>
  574. <!-- 隐患内容 -->
  575. <div
  576. v-else
  577. class="hazard-cards-container"
  578. >
  579. <div
  580. v-if="!filteredHazards.length"
  581. class="hazard-empty"
  582. >
  583. 暂无对应隐患
  584. </div>
  585. <div
  586. v-for="(
  587. hazard, index
  588. ) in filteredHazards"
  589. :key="index"
  590. class="hazard-card"
  591. :class="{
  592. show: visibleHazardCards[
  593. index
  594. ],
  595. }"
  596. >
  597. <div class="hazard-number">
  598. {{ index + 1 }}
  599. </div>
  600. <div
  601. class="hazard-text-container"
  602. >
  603. <p class="hazard-desc">
  604. {{ hazard }}
  605. </p>
  606. <a
  607. href="javascript:void(0);"
  608. class="example-link"
  609. @click.prevent="
  610. openExampleModal({
  611. number:
  612. index + 1,
  613. description:
  614. hazard,
  615. })
  616. "
  617. >
  618. 示例
  619. </a>
  620. </div>
  621. </div>
  622. </div>
  623. </div>
  624. </div>
  625. </div>
  626. </div>
  627. </div>
  628. <!-- 图片预览弹窗 -->
  629. <teleport to="body">
  630. <div
  631. v-if="showImagePreview"
  632. class="image-preview-overlay"
  633. @click="closeImagePreview"
  634. >
  635. <img
  636. :src="previewImageUrl || annotatedImageUrl"
  637. alt="预览图片"
  638. class="preview-image"
  639. style="transform: none !important"
  640. />
  641. </div>
  642. </teleport>
  643. <!-- 示例对比弹窗 -->
  644. <div
  645. v-if="showExampleModal"
  646. class="modal-backdrop"
  647. @click="closeExampleModal"
  648. >
  649. <div class="example-comparison-modal" @click.stop>
  650. <!-- 弹窗头部 -->
  651. <div class="modal-header-section">
  652. <div class="modal-title-area">
  653. <div class="hazard-info">
  654. <span class="hazard-number">{{
  655. selectedHazard?.number
  656. }}</span>
  657. <span class="hazard-text">{{
  658. selectedHazard?.description
  659. }}</span>
  660. </div>
  661. </div>
  662. <button
  663. class="close-button"
  664. @click="closeExampleModal"
  665. >
  666. <svg
  667. width="24"
  668. height="24"
  669. viewBox="0 0 24 24"
  670. fill="none"
  671. >
  672. <path
  673. d="M18 6L6 18M6 6L18 18"
  674. stroke="currentColor"
  675. stroke-width="2"
  676. stroke-linecap="round"
  677. />
  678. </svg>
  679. </button>
  680. </div>
  681. <!-- 弹窗内容 -->
  682. <div class="modal-content-section">
  683. <!-- 加载状态 -->
  684. <div
  685. v-if="isLoadingExample"
  686. class="loading-state"
  687. >
  688. <div class="spinner"></div>
  689. <p>正在加载示例图片...</p>
  690. </div>
  691. <!-- 对比内容 -->
  692. <div v-else class="comparison-container">
  693. <!-- 正确示例 -->
  694. <div class="example-card correct-example">
  695. <div class="card-header">
  696. <div class="status-badge correct">
  697. <svg
  698. width="16"
  699. height="16"
  700. viewBox="0 0 24 24"
  701. fill="none"
  702. >
  703. <path
  704. d="M20 6L9 17L4 12"
  705. stroke="currentColor"
  706. stroke-width="2"
  707. stroke-linecap="round"
  708. stroke-linejoin="round"
  709. />
  710. </svg>
  711. 正确示例
  712. </div>
  713. </div>
  714. <div class="card-content">
  715. <div
  716. v-if="
  717. imageLoadingStates.correct
  718. "
  719. class="image-loading"
  720. >
  721. <div class="spinner"></div>
  722. <p>加载中...</p>
  723. </div>
  724. <img
  725. v-else-if="
  726. exampleImages.correctImageUrl
  727. "
  728. :src="
  729. exampleImages.correctImageUrl
  730. "
  731. alt="正确示例"
  732. class="example-image clickable-image"
  733. @error="
  734. handleImageError(
  735. $event,
  736. 'correct'
  737. )
  738. "
  739. @load="
  740. handleImageLoad(
  741. $event,
  742. 'correct'
  743. )
  744. "
  745. @click="
  746. openImagePreview(
  747. exampleImages.correctImageUrl
  748. )
  749. "
  750. />
  751. <div v-else class="no-image">
  752. <svg
  753. width="48"
  754. height="48"
  755. viewBox="0 0 24 24"
  756. fill="none"
  757. >
  758. <rect
  759. x="3"
  760. y="3"
  761. width="18"
  762. height="18"
  763. rx="2"
  764. ry="2"
  765. stroke="currentColor"
  766. stroke-width="2"
  767. />
  768. <circle
  769. cx="8.5"
  770. cy="8.5"
  771. r="1.5"
  772. stroke="currentColor"
  773. stroke-width="2"
  774. />
  775. <path
  776. d="M21 15L16 10L5 21"
  777. stroke="currentColor"
  778. stroke-width="2"
  779. />
  780. </svg>
  781. <p>暂无示例图片</p>
  782. </div>
  783. </div>
  784. </div>
  785. <!-- 错误示例 -->
  786. <div class="example-card error-example">
  787. <div class="card-header">
  788. <div class="status-badge error">
  789. <svg
  790. width="16"
  791. height="16"
  792. viewBox="0 0 24 24"
  793. fill="none"
  794. >
  795. <path
  796. d="M18 6L6 18M6 6L18 18"
  797. stroke="currentColor"
  798. stroke-width="2"
  799. stroke-linecap="round"
  800. />
  801. </svg>
  802. 错误示例
  803. </div>
  804. </div>
  805. <div class="card-content">
  806. <div
  807. v-if="imageLoadingStates.error"
  808. class="image-loading"
  809. >
  810. <div class="spinner"></div>
  811. <p>加载中...</p>
  812. </div>
  813. <img
  814. v-else-if="
  815. exampleImages.errorImageUrl
  816. "
  817. :src="
  818. exampleImages.errorImageUrl
  819. "
  820. alt="错误示例"
  821. class="example-image clickable-image"
  822. @error="
  823. handleImageError(
  824. $event,
  825. 'error'
  826. )
  827. "
  828. @load="
  829. handleImageLoad(
  830. $event,
  831. 'error'
  832. )
  833. "
  834. @click="
  835. openImagePreview(
  836. exampleImages.errorImageUrl
  837. )
  838. "
  839. />
  840. <div v-else class="no-image">
  841. <svg
  842. width="48"
  843. height="48"
  844. viewBox="0 0 24 24"
  845. fill="none"
  846. >
  847. <rect
  848. x="3"
  849. y="3"
  850. width="18"
  851. height="18"
  852. rx="2"
  853. ry="2"
  854. stroke="currentColor"
  855. stroke-width="2"
  856. />
  857. <circle
  858. cx="8.5"
  859. cy="8.5"
  860. r="1.5"
  861. stroke="currentColor"
  862. stroke-width="2"
  863. />
  864. <path
  865. d="M21 15L16 10L5 21"
  866. stroke="currentColor"
  867. stroke-width="2"
  868. />
  869. </svg>
  870. <p>暂无示例图片</p>
  871. </div>
  872. </div>
  873. </div>
  874. </div>
  875. </div>
  876. </div>
  877. </div>
  878. </div>
  879. </div>
  880. </div>
  881. <!-- 点评弹窗 -->
  882. <teleport to="body">
  883. <div
  884. v-if="showEvaluationModal"
  885. class="evaluation-modal-overlay"
  886. @click="closeEvaluationModal"
  887. >
  888. <div class="evaluation-modal" @click.stop>
  889. <div class="modal-header">
  890. <span class="modal-title">点评确认</span>
  891. <img
  892. src="@/assets/Hazard/11.png"
  893. alt="关闭"
  894. class="close-icon"
  895. @click="closeEvaluationModal"
  896. />
  897. </div>
  898. <div class="modal-body">
  899. <!-- 问题1:场景是否匹配 -->
  900. <div class="question-section">
  901. <div class="question-title">1.场景是否匹配?</div>
  902. <div class="answer-buttons">
  903. <button
  904. :class="[
  905. 'answer-btn',
  906. {
  907. active:
  908. evaluationData.sceneMatch ===
  909. true,
  910. disabled:
  911. selectedHistoryItem?.effect_evaluation >
  912. 0,
  913. },
  914. ]"
  915. @click="
  916. selectedHistoryItem?.effect_evaluation >
  917. 0
  918. ? null
  919. : (evaluationData.sceneMatch = true)
  920. "
  921. >
  922. </button>
  923. <button
  924. :class="[
  925. 'answer-btn',
  926. {
  927. active:
  928. evaluationData.sceneMatch ===
  929. false,
  930. disabled:
  931. selectedHistoryItem?.effect_evaluation >
  932. 0,
  933. },
  934. ]"
  935. @click="
  936. selectedHistoryItem?.effect_evaluation >
  937. 0
  938. ? null
  939. : (evaluationData.sceneMatch = false)
  940. "
  941. >
  942. </button>
  943. </div>
  944. </div>
  945. <!-- 问题2:提示是否准确 -->
  946. <div class="question-section">
  947. <div class="question-title">2.提示是否准确?</div>
  948. <div class="answer-buttons">
  949. <button
  950. :class="[
  951. 'answer-btn',
  952. {
  953. active:
  954. evaluationData.promptAccurate ===
  955. true,
  956. disabled:
  957. selectedHistoryItem?.effect_evaluation >
  958. 0,
  959. },
  960. ]"
  961. @click="
  962. selectedHistoryItem?.effect_evaluation >
  963. 0
  964. ? null
  965. : (evaluationData.promptAccurate = true)
  966. "
  967. >
  968. </button>
  969. <button
  970. :class="[
  971. 'answer-btn',
  972. {
  973. active:
  974. evaluationData.promptAccurate ===
  975. false,
  976. disabled:
  977. selectedHistoryItem?.effect_evaluation >
  978. 0,
  979. },
  980. ]"
  981. @click="
  982. selectedHistoryItem?.effect_evaluation >
  983. 0
  984. ? null
  985. : (evaluationData.promptAccurate = false)
  986. "
  987. >
  988. </button>
  989. </div>
  990. </div>
  991. <!-- 问题3:效果评价 -->
  992. <div class="question-section">
  993. <div class="question-title">3.效果评价</div>
  994. <div class="star-rating">
  995. <span
  996. v-for="star in 5"
  997. :key="star"
  998. :class="[
  999. 'star',
  1000. {
  1001. active:
  1002. star <= evaluationData.rating,
  1003. disabled:
  1004. selectedHistoryItem?.effect_evaluation >
  1005. 0,
  1006. },
  1007. ]"
  1008. @click="
  1009. selectedHistoryItem?.effect_evaluation >
  1010. 0
  1011. ? null
  1012. : (evaluationData.rating = star)
  1013. "
  1014. >
  1015. </span>
  1016. </div>
  1017. </div>
  1018. <!-- 问题4:用户意见 -->
  1019. <div
  1020. v-if="shouldShowRemarkSection"
  1021. class="question-section"
  1022. >
  1023. <div class="question-title">4.您的意见</div>
  1024. <div class="remark-input-container">
  1025. <textarea
  1026. v-model="evaluationData.userRemark"
  1027. :disabled="
  1028. selectedHistoryItem?.effect_evaluation >
  1029. 0
  1030. "
  1031. placeholder="请输入您的意见和建议(最多200字)"
  1032. class="remark-textarea"
  1033. maxlength="200"
  1034. @input="handleRemarkInput"
  1035. ></textarea>
  1036. <div class="character-count">
  1037. <span
  1038. :class="{
  1039. 'over-limit':
  1040. evaluationData.userRemark
  1041. .length > 200,
  1042. }"
  1043. >
  1044. {{
  1045. evaluationData.userRemark.length
  1046. }}/200
  1047. </span>
  1048. </div>
  1049. </div>
  1050. </div>
  1051. </div>
  1052. <div
  1053. class="modal-footer"
  1054. v-if="
  1055. !selectedHistoryItem?.effect_evaluation ||
  1056. selectedHistoryItem.effect_evaluation === 0
  1057. "
  1058. >
  1059. <button class="submit-btn" @click="submitEvaluation">
  1060. <img
  1061. src="@/assets/Hazard/13.png"
  1062. alt="提交反馈"
  1063. class="submit-icon"
  1064. />
  1065. </button>
  1066. </div>
  1067. </div>
  1068. </div>
  1069. </teleport>
  1070. <!-- 删除确认弹窗 -->
  1071. <DeleteConfirmModal
  1072. :visible="showDeleteModal"
  1073. title="删除历史记录"
  1074. :message="deleteConfirmMessage"
  1075. @confirm="confirmDeleteHistory"
  1076. @cancel="cancelDeleteHistory"
  1077. @close="cancelDeleteHistory"
  1078. />
  1079. </div>
  1080. </template>
  1081. <script setup>
  1082. import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from "vue";
  1083. import { ElMessage } from "element-plus";
  1084. import { Upload, View, DataAnalysis, Bell } from "@element-plus/icons-vue";
  1085. import Sidebar from "@/components/Sidebar.vue";
  1086. import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
  1087. import { apis } from "@/request/apis.js";
  1088. import startIdentifyImg from "@/assets/Hazard/2.png";
  1089. import startIdentifyActiveImg from "@/assets/Hazard/3.png";
  1090. // 响应式数据
  1091. const messageText = ref("");
  1092. const selectedScenario = ref(""); // 自动识别出的场景
  1093. const uploadedImage = ref(null);
  1094. const uploadedImageUrl = ref(""); // 新增:存储上传后的图片URL
  1095. const fileInput = ref(null);
  1096. const currentView = ref("main"); // 当前视图:main-主界面,detail-详情页
  1097. const isTransitioning = ref(false); // 控制过渡动画状态
  1098. const selectedHistoryItem = ref(null); // 选中的历史记录项
  1099. const showExampleModal = ref(false); // 控制示例弹窗显示
  1100. const selectedHazard = ref(null); // 选中的隐患项目
  1101. const exampleImages = ref({}); // 存储示例图数据
  1102. const isLoadingExample = ref(false); // 控制示例图加载状态
  1103. // 移除了 lastClickTime,不再需要防抖
  1104. const imageLoadingStates = ref({
  1105. correct: false, // 正确示例图加载状态
  1106. error: false, // 错误示例图加载状态
  1107. }); // 控制每张图片的加载状态
  1108. // 删除相关状态
  1109. const showDeleteModal = ref(false); // 控制是否显示删除确认弹窗
  1110. const deleteTargetItem = ref(null); // 要删除的目标项
  1111. const isUploading = ref(false); // 新增:上传状态标识
  1112. const isDragOver = ref(false); // 新增:拖拽上传区域是否悬停
  1113. const isIdentifying = ref(false); // 新增:识别状态标识
  1114. // 隐患提示结果相关数据
  1115. const detectionResult = ref(null); // 存储识别结果
  1116. const annotatedImageUrl = ref(""); // 存储标注后的图片URL
  1117. // 图片预览相关数据
  1118. const showImagePreview = ref(false); // 控制图片预览弹窗显示
  1119. const previewImageUrl = ref(""); // 预览图片的URL
  1120. const showScanningEffect = ref(false); // 控制扫描效果显示
  1121. const isLoadingLabels = ref(false); // 控制标签加载状态
  1122. const isStreamingLabels = ref(false); // 控制标签流式输出状态
  1123. const streamingLabels = ref(""); // 流式输出的标签内容
  1124. const isStreamingAnalysis = ref(false); // 控制整个分析文本流式输出状态
  1125. const streamingAnalysis = ref(""); // 流式输出的完整分析文本
  1126. const showAnalysisPrompt = ref(false); // 控制分析提示显示
  1127. const visibleHazardCards = ref({}); // 控制每个隐患卡片的显示状态
  1128. const selectedKeyElement = ref(null); // 当前选中的关键要素
  1129. const imageContainerRef = ref(null); // 图片容器引用
  1130. const mainImageRef = ref(null); // 主图引用
  1131. const elementCardRef = ref(null); // 关键要素卡片引用
  1132. const elementOverlayStyle = ref(null); // 关键要素卡片定位样式
  1133. // 历史记录相关数据
  1134. const historyData = ref([]); // 存储历史记录数据
  1135. const historyTotal = ref(0); // 历史记录总数
  1136. const isLoadingDetail = ref(false); // 控制详情加载状态
  1137. const isImageLoading = ref(false); // 控制图片加载状态
  1138. const isLoadingHistory = ref(false); // 是否正在加载历史记录
  1139. // 点评弹窗相关数据
  1140. const showEvaluationModal = ref(false); // 控制点评弹窗显示
  1141. const evaluationData = ref({
  1142. sceneMatch: null, // 场景是否匹配
  1143. promptAccurate: null, // 提示是否准确
  1144. rating: 0, // 效果评价评分 1-5
  1145. userRemark: "", // 用户意见
  1146. });
  1147. // ===== 已删除:getUserId - 不再需要,改用token =====
  1148. // import { getUserId } from "@/utils/userManager.js";
  1149. // 删除确认消息
  1150. const deleteConfirmMessage = computed(() => {
  1151. const title = deleteTargetItem.value?.item?.title || "";
  1152. return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`;
  1153. });
  1154. // 控制是否显示备注区域
  1155. const shouldShowRemarkSection = computed(() => {
  1156. // 如果是已点评状态,只有当有备注内容时才显示
  1157. if (selectedHistoryItem.value?.effect_evaluation > 0) {
  1158. return (
  1159. evaluationData.value.userRemark &&
  1160. evaluationData.value.userRemark.trim() !== ""
  1161. );
  1162. }
  1163. // 如果是未点评状态,始终显示(让用户可以输入)
  1164. return true;
  1165. });
  1166. // 场景配置
  1167. const scenarios = {
  1168. tunnel: { name: "隧道工程", color: "#3366E6" },
  1169. simple_supported_bridge: { name: "桥梁工程", color: "#22B850" },
  1170. gas_station: { name: "加油站", color: "#FF4D4F" },
  1171. special_equipment: { name: "特种设备", color: "#0080FF" },
  1172. operate_highway: { name: "运营高速公路", color: "#722ED1" },
  1173. };
  1174. const autoSceneOrder = [
  1175. "tunnel",
  1176. "simple_supported_bridge",
  1177. "gas_station",
  1178. "special_equipment",
  1179. "operate_highway",
  1180. ];
  1181. // 历史记录数据结构 - 初始为空数组,等待后端数据
  1182. // const historyData = ref([]);
  1183. // 标签类型配置,便于后端动态管理
  1184. const tagTypeConfig = {
  1185. tunnel: {
  1186. class: "tag-tunnel",
  1187. background: "rgba(62, 123, 250, 0.1)",
  1188. color: "#3366E6",
  1189. text: "隧道",
  1190. },
  1191. simple_supported_bridge: {
  1192. class: "tag-bridge",
  1193. background: "rgba(34, 184, 80, 0.1)",
  1194. color: "#22B850",
  1195. text: "桥梁",
  1196. },
  1197. special_equipment: {
  1198. class: "tag-equipment",
  1199. background: "rgba(0, 128, 255, 0.1)",
  1200. color: "#0080FF",
  1201. text: "特种设备",
  1202. },
  1203. operate_highway: {
  1204. class: "tag-highway",
  1205. background: "rgba(114, 46, 209, 0.1)",
  1206. color: "#722ED1",
  1207. text: "运营高速公路",
  1208. },
  1209. gas_station: {
  1210. class: "tag-gas-station",
  1211. background: "rgba(255, 77, 79, 0.1)",
  1212. color: "#FF4D4F",
  1213. text: "加油站",
  1214. },
  1215. };
  1216. // 根据标签类型获取样式类名
  1217. const getTagClass = (tagType) => {
  1218. return tagTypeConfig[tagType]?.class || "tag-tunnel";
  1219. };
  1220. // 根据标签类型获取显示文字
  1221. const getTagText = (tagType) => {
  1222. return tagTypeConfig[tagType]?.text || "隧道";
  1223. };
  1224. const getUniqueLabels = (labels) => {
  1225. const seen = new Set();
  1226. return labels.filter((label) => {
  1227. if (!label || seen.has(label)) return false;
  1228. seen.add(label);
  1229. return true;
  1230. });
  1231. };
  1232. const displayLabels = computed(() => {
  1233. const labels =
  1234. detectionResult.value?.display_labels || detectionResult.value?.labels;
  1235. if (!labels) return [];
  1236. const normalizedLabels = (Array.isArray(labels) ? labels : String(labels).split(/[,,、]/))
  1237. .map((label) => normalizeLabel(String(label).trim()))
  1238. .filter((label) => label);
  1239. return getUniqueLabels(normalizedLabels);
  1240. });
  1241. const keyElements = computed(() => displayLabels.value);
  1242. const normalizeLabel = (label) => {
  1243. if (!label) return "";
  1244. const parts = String(label).split("_").filter((part) => part);
  1245. if (parts.length <= 1) return String(label);
  1246. return parts.slice(1).join("_");
  1247. };
  1248. const hazardMatchesElement = (hazard, element) => {
  1249. if (!hazard || !element) return false;
  1250. return hazard.includes(element) || element.includes(hazard);
  1251. };
  1252. const hazardsMap = computed(() => {
  1253. const map = {};
  1254. keyElements.value.forEach((element) => {
  1255. map[element] = [];
  1256. });
  1257. const hazards = detectionResult.value?.third_scenes || [];
  1258. hazards.forEach((hazard) => {
  1259. let matched = false;
  1260. for (const element of keyElements.value) {
  1261. if (hazardMatchesElement(hazard, element)) {
  1262. map[element].push(hazard);
  1263. matched = true;
  1264. break;
  1265. }
  1266. }
  1267. if (!matched) {
  1268. if (!map.__unmatched) map.__unmatched = [];
  1269. map.__unmatched.push(hazard);
  1270. }
  1271. });
  1272. return map;
  1273. });
  1274. const filteredHazards = computed(() => {
  1275. if (!detectionResult.value) return [];
  1276. if (!selectedKeyElement.value) {
  1277. return detectionResult.value?.third_scenes || [];
  1278. }
  1279. const backendHazards =
  1280. detectionResult.value?.element_hazards?.[selectedKeyElement.value];
  1281. if (Array.isArray(backendHazards)) {
  1282. return backendHazards;
  1283. }
  1284. return hazardsMap.value[selectedKeyElement.value] || [];
  1285. });
  1286. const selectedDetection = computed(() => {
  1287. if (!selectedKeyElement.value) return null;
  1288. const detections = detectionResult.value?.detections || [];
  1289. return (
  1290. detections.find((detection) => {
  1291. const label = normalizeLabel(detection?.label || "");
  1292. return (
  1293. label === selectedKeyElement.value ||
  1294. label.includes(selectedKeyElement.value) ||
  1295. selectedKeyElement.value.includes(label)
  1296. );
  1297. }) || null
  1298. );
  1299. });
  1300. const resetKeyElementState = () => {
  1301. selectedKeyElement.value = null;
  1302. elementOverlayStyle.value = null;
  1303. };
  1304. const toggleKeyElement = async (element) => {
  1305. if (selectedKeyElement.value === element) {
  1306. resetKeyElementState();
  1307. } else {
  1308. selectedKeyElement.value = element;
  1309. }
  1310. await nextTick();
  1311. updateElementOverlayPosition();
  1312. };
  1313. const setVisibleHazardCards = (hazards = filteredHazards.value) => {
  1314. const nextVisibleHazards = {};
  1315. hazards.forEach((_, index) => {
  1316. nextVisibleHazards[index] = true;
  1317. });
  1318. visibleHazardCards.value = nextVisibleHazards;
  1319. };
  1320. const getAutoSceneCandidates = () => {
  1321. if (
  1322. selectedScenario.value &&
  1323. autoSceneOrder.includes(selectedScenario.value)
  1324. ) {
  1325. return [
  1326. selectedScenario.value,
  1327. ...autoSceneOrder.filter(
  1328. (sceneKey) => sceneKey !== selectedScenario.value
  1329. ),
  1330. ];
  1331. }
  1332. return autoSceneOrder;
  1333. };
  1334. const detectSceneAutomatically = async (baseRequestData) => {
  1335. let lastErrorMessage = "";
  1336. for (const sceneKey of getAutoSceneCandidates()) {
  1337. try {
  1338. const response = await apis.hazardDetection({
  1339. ...baseRequestData,
  1340. scene_name: sceneKey,
  1341. });
  1342. const isSuccess =
  1343. response.code === 200 || response.statusCode === 200;
  1344. if (isSuccess) {
  1345. return {
  1346. response,
  1347. sceneKey,
  1348. };
  1349. }
  1350. lastErrorMessage =
  1351. response.msg || response.message || lastErrorMessage;
  1352. } catch (error) {
  1353. lastErrorMessage =
  1354. error?.msg || error?.message || lastErrorMessage;
  1355. }
  1356. }
  1357. throw new Error(
  1358. lastErrorMessage || "暂未识别到支持的场景,请尝试更换更清晰的图片"
  1359. );
  1360. };
  1361. const updateElementOverlayPosition = async () => {
  1362. if (
  1363. !selectedKeyElement.value ||
  1364. showScanningEffect.value ||
  1365. currentView.value !== "detail"
  1366. ) {
  1367. elementOverlayStyle.value = null;
  1368. return;
  1369. }
  1370. const container = imageContainerRef.value;
  1371. const imageElement = mainImageRef.value;
  1372. const detection = selectedDetection.value;
  1373. if (
  1374. !container ||
  1375. !imageElement ||
  1376. !detection ||
  1377. !Array.isArray(detection.box) ||
  1378. detection.box.length < 4 ||
  1379. !imageElement.naturalWidth ||
  1380. !imageElement.naturalHeight
  1381. ) {
  1382. elementOverlayStyle.value = null;
  1383. return;
  1384. }
  1385. const containerWidth = container.clientWidth;
  1386. const containerHeight = container.clientHeight;
  1387. const naturalWidth = imageElement.naturalWidth;
  1388. const naturalHeight = imageElement.naturalHeight;
  1389. if (!containerWidth || !containerHeight) {
  1390. elementOverlayStyle.value = null;
  1391. return;
  1392. }
  1393. const scale = Math.min(
  1394. containerWidth / naturalWidth,
  1395. containerHeight / naturalHeight
  1396. );
  1397. const renderedWidth = naturalWidth * scale;
  1398. const renderedHeight = naturalHeight * scale;
  1399. const offsetX = (containerWidth - renderedWidth) / 2;
  1400. const offsetY = (containerHeight - renderedHeight) / 2;
  1401. const [x1, y1, x2, y2] = detection.box.map((value) => Number(value) || 0);
  1402. const boxLeft = offsetX + x1 * scale;
  1403. const boxTop = offsetY + y1 * scale;
  1404. const boxRight = offsetX + x2 * scale;
  1405. let cardLeft = boxRight + 12;
  1406. let cardTop = boxTop;
  1407. const cardWidth = elementCardRef.value?.offsetWidth || 260;
  1408. const cardHeight = elementCardRef.value?.offsetHeight || 148;
  1409. const safePadding = 12;
  1410. if (cardLeft + cardWidth > containerWidth - safePadding) {
  1411. cardLeft = Math.max(safePadding, boxLeft - cardWidth - 12);
  1412. }
  1413. if (cardTop + cardHeight > containerHeight - safePadding) {
  1414. cardTop = Math.max(
  1415. safePadding,
  1416. containerHeight - cardHeight - safePadding
  1417. );
  1418. }
  1419. elementOverlayStyle.value = {
  1420. left: `${cardLeft}px`,
  1421. top: `${Math.max(safePadding, cardTop)}px`,
  1422. };
  1423. await nextTick();
  1424. };
  1425. const handleMainImageLoad = async () => {
  1426. await nextTick();
  1427. updateElementOverlayPosition();
  1428. };
  1429. // 删除历史记录
  1430. const deleteHistoryItem = (historyItem, index) => {
  1431. console.log("准备删除隐患提示历史记录:", historyItem);
  1432. // 设置要删除的项目并显示确认弹窗
  1433. deleteTargetItem.value = { item: historyItem, index: index };
  1434. showDeleteModal.value = true;
  1435. };
  1436. // 确认删除历史记录
  1437. const confirmDeleteHistory = async () => {
  1438. if (!deleteTargetItem.value) return;
  1439. const { item: historyItem, index } = deleteTargetItem.value;
  1440. try {
  1441. // 调用删除接口
  1442. const response = await apis.deleteRecognitionRecord({
  1443. recognition_record_id: historyItem.id,
  1444. });
  1445. if (response.statusCode === 200) {
  1446. // 删除成功,从列表中移除
  1447. historyData.value.splice(index, 1);
  1448. historyTotal.value = historyTotal.value - 1;
  1449. // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
  1450. if (historyItem.isActive) {
  1451. await createNewChat();
  1452. }
  1453. console.log("隐患提示历史记录删除成功");
  1454. ElMessage.success("删除成功");
  1455. } else {
  1456. console.error("删除隐患提示历史记录失败:", response.msg);
  1457. ElMessage.error(response.msg || "删除失败");
  1458. }
  1459. } catch (error) {
  1460. console.error("删除隐患提示历史记录失败:", error);
  1461. ElMessage.error("删除失败,请稍后重试");
  1462. } finally {
  1463. // 关闭弹窗并清除目标项
  1464. showDeleteModal.value = false;
  1465. deleteTargetItem.value = null;
  1466. }
  1467. };
  1468. // 取消删除
  1469. const cancelDeleteHistory = () => {
  1470. showDeleteModal.value = false;
  1471. deleteTargetItem.value = null;
  1472. };
  1473. // 处理历史记录图片加载错误
  1474. const handleHistoryImageError = (event, item) => {
  1475. console.log("历史记录图片加载失败:", item.originalImageUrl);
  1476. // 避免无限循环,设置一个简单的占位符
  1477. event.target.onerror = null; // 移除错误处理,防止再次触发
  1478. event.target.src = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRjVGNUY1Ii8+CjxwYXRoIGQ9Ik0yMCAyNkMyMy4zMTM3IDI2IDI2IDIzLjMxMzcgMjYgMjBDMjYgMTYuNjg2MyAyMy4zMTM3IDE0IDIwIDE0QzE2LjY4NjMgMTQgMTQgMTYuNjg2MyAxNCAyMEMxNCAyMy4zMTM3IDE2LjY4NjMgMjYgMjAgMjZaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjx0ZXh0IHg9IjIwIiB5PSIzMiIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjgiIGZpbGw9IiM5OTk5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiPuWbvueJhzwvdGV4dD4KPHN2Zz4K";
  1479. };
  1480. // 方法
  1481. const createNewChat = () => {
  1482. console.log("createNewChat 被调用");
  1483. // 重置所有状态
  1484. currentView.value = "main";
  1485. selectedScenario.value = "";
  1486. uploadedImage.value = null;
  1487. uploadedImageUrl.value = "";
  1488. selectedHistoryItem.value = null;
  1489. showExampleModal.value = false;
  1490. selectedHazard.value = null;
  1491. isUploading.value = false;
  1492. isDragOver.value = false; // 重置拖拽状态
  1493. isIdentifying.value = false; // 重置识别状态
  1494. annotatedImageUrl.value = "";
  1495. exampleImages.value = {}; // 清空示例图数据
  1496. isLoadingExample.value = false; // 重置示例图加载状态
  1497. imageLoadingStates.value = { correct: false, error: false }; // 重置图片加载状态
  1498. // 移除了防抖时间重置
  1499. isImageLoading.value = false; // 重置图片加载状态
  1500. showScanningEffect.value = false; // 重置扫描效果状态
  1501. isLoadingLabels.value = false; // 重置标签加载状态
  1502. isStreamingLabels.value = false; // 重置标签流式输出状态
  1503. streamingLabels.value = ""; // 清空流式输出内容
  1504. isStreamingAnalysis.value = false; // 重置分析文本流式输出状态
  1505. streamingAnalysis.value = ""; // 清空分析文本流式输出内容
  1506. showAnalysisPrompt.value = false; // 重置分析提示状态
  1507. detectionResult.value = null;
  1508. resetKeyElementState();
  1509. // 清空文件输入
  1510. if (fileInput.value) {
  1511. fileInput.value.value = "";
  1512. console.log("文件输入已清空");
  1513. } else {
  1514. console.log("fileInput 引用为空,无法清空");
  1515. }
  1516. // 重置所有历史记录的active状态
  1517. if (historyData.value.length > 0) {
  1518. historyData.value.forEach((item) => {
  1519. item.isActive = false;
  1520. });
  1521. }
  1522. // ElMessage.success("已创建新的隐患提示任务");
  1523. console.log("新任务创建完成");
  1524. };
  1525. // 点击历史记录
  1526. const handleHistoryItem = async (historyItem, event) => {
  1527. try {
  1528. console.log("handleHistoryItem 被调用,历史记录:", historyItem);
  1529. // 如果当前记录已经是选中状态,则不执行任何操作
  1530. if (historyItem.isActive) {
  1531. console.log("当前记录已经是选中状态,忽略点击");
  1532. return;
  1533. }
  1534. selectedHistoryItem.value = historyItem;
  1535. currentView.value = "detail";
  1536. isDragOver.value = false; // 重置拖拽状态
  1537. isLoadingDetail.value = true; // 开始加载详情
  1538. isImageLoading.value = true; // 开始加载图片
  1539. // 调用详情接口获取完整数据
  1540. console.log("开始获取记录详情,ID:", historyItem.id);
  1541. const detailResponse = await apis.getRecognitionRecordDetail({
  1542. recognition_record_id: historyItem.id,
  1543. });
  1544. if (detailResponse.statusCode === 200 || detailResponse.code === 200) {
  1545. const detailData = detailResponse.data;
  1546. console.log("获取详情成功:", detailData);
  1547. // 设置识别结果数据
  1548. detectionResult.value = {
  1549. scene_name:
  1550. detailData.tag_type ||
  1551. getTagTypeFromLabels(detailData.labels),
  1552. labels: detailData.labels,
  1553. display_labels: detailData.display_labels || [],
  1554. total_detections: detailData.labels
  1555. ? Array.isArray(detailData.labels)
  1556. ? detailData.labels.length
  1557. : 0
  1558. : 0,
  1559. third_scenes: detailData.third_scenes || [],
  1560. element_hazards: detailData.element_hazards || {},
  1561. detections: detailData.detections || [],
  1562. };
  1563. selectedScenario.value = detectionResult.value.scene_name || "";
  1564. resetKeyElementState();
  1565. // 设置图片URL并检测加载完成
  1566. const newImageUrl =
  1567. detailData.recognition_image_url ||
  1568. detailData.original_image_url;
  1569. annotatedImageUrl.value = newImageUrl;
  1570. // 检测图片是否加载完成
  1571. if (newImageUrl) {
  1572. await waitForImageLoad(newImageUrl);
  1573. }
  1574. // 更新历史记录中的标签类型(优先使用后端返回的tag_type,如果没有则根据labels推断)
  1575. const tagType =
  1576. detailData.tag_type || getTagTypeFromLabels(detailData.labels);
  1577. historyItem.tagType = tagType;
  1578. // 显示所有隐患卡片(历史记录不需要动画效果)
  1579. setVisibleHazardCards(
  1580. detectionResult.value?.third_scenes || []
  1581. );
  1582. } else {
  1583. console.error("获取详情失败:", detailResponse.message);
  1584. ElMessage.error("获取记录详情失败");
  1585. // 使用历史记录中的基础数据作为后备
  1586. detectionResult.value = {
  1587. scene_name: historyItem.tagType || "simple_supported_bridge",
  1588. labels: historyItem.labels,
  1589. display_labels: [],
  1590. total_detections: 0,
  1591. third_scenes: [],
  1592. element_hazards: {},
  1593. detections: [],
  1594. };
  1595. const fallbackImageUrl =
  1596. historyItem.recognitionImageUrl || historyItem.originalImageUrl;
  1597. annotatedImageUrl.value = fallbackImageUrl;
  1598. // 检测后备图片是否加载完成
  1599. if (fallbackImageUrl) {
  1600. await waitForImageLoad(fallbackImageUrl);
  1601. }
  1602. // 显示所有隐患卡片
  1603. setVisibleHazardCards([]);
  1604. }
  1605. // 更新数据层的active状态
  1606. historyData.value.forEach((item) => {
  1607. item.isActive = item.id === historyItem.id;
  1608. });
  1609. console.log("历史记录状态已更新");
  1610. } catch (error) {
  1611. console.error("处理历史记录失败:", error);
  1612. ElMessage.error("获取记录详情失败");
  1613. isDragOver.value = false; // 重置拖拽状态
  1614. } finally {
  1615. isLoadingDetail.value = false; // 结束加载详情
  1616. isImageLoading.value = false; // 结束加载图片
  1617. }
  1618. };
  1619. // 触发文件上传
  1620. const triggerFileUpload = () => {
  1621. try {
  1622. console.log("triggerFileUpload 被调用");
  1623. console.log("fileInput.value:", fileInput.value);
  1624. isDragOver.value = false; // 重置拖拽状态
  1625. if (fileInput.value) {
  1626. fileInput.value.click();
  1627. console.log("已触发文件选择器");
  1628. } else {
  1629. console.error("fileInput 引用为空");
  1630. }
  1631. } catch (error) {
  1632. console.error("触发文件上传失败:", error);
  1633. isDragOver.value = false; // 重置拖拽状态
  1634. }
  1635. };
  1636. // 新增:上传文件到服务器
  1637. const uploadFileToServer = async (file) => {
  1638. try {
  1639. console.log("uploadFileToServer 被调用,文件:", file);
  1640. isUploading.value = true;
  1641. // 创建FormData对象
  1642. const formData = new FormData();
  1643. formData.append("image", file);
  1644. console.log("FormData 已创建:", formData);
  1645. // 调用后端上传接口
  1646. console.log("开始调用后端API...");
  1647. const response = await apis.uploadImage(formData);
  1648. console.log("后端API响应:", response);
  1649. if (response.statusCode === 200) {
  1650. uploadedImageUrl.value = response.fileUrl || response.fileURL;
  1651. console.log("上传成功:", uploadedImageUrl.value);
  1652. ElMessage.success("图片上传成功!");
  1653. } else {
  1654. throw new Error(response.message || "上传失败");
  1655. }
  1656. } catch (error) {
  1657. console.error("上传失败:", error);
  1658. ElMessage.error("图片上传失败: " + (error.message || "未知错误"));
  1659. uploadedImage.value = null;
  1660. uploadedImageUrl.value = "";
  1661. isDragOver.value = false; // 重置拖拽状态
  1662. } finally {
  1663. isUploading.value = false;
  1664. isDragOver.value = false; // 重置拖拽状态
  1665. }
  1666. };
  1667. // 开始识别
  1668. const startIdentification = async () => {
  1669. try {
  1670. console.log("startIdentification 被调用");
  1671. // 防抖检查:如果正在识别中,直接返回
  1672. if (isIdentifying.value) {
  1673. console.log("识别正在进行中,忽略重复点击");
  1674. ElMessage.warning("识别正在进行中,请勿重复点击");
  1675. return;
  1676. }
  1677. if (!uploadedImageUrl.value) {
  1678. // 使用 uploadedImageUrl.value 判断
  1679. console.log("未上传图片");
  1680. ElMessage.warning("请先上传图片");
  1681. isDragOver.value = false; // 重置拖拽状态
  1682. return;
  1683. }
  1684. // 检查用户最新识别记录是否已点评
  1685. try {
  1686. console.log("检查最新识别记录是否已点评");
  1687. const latestRecordResponse = await apis.getLatestRecognitionRecord({
  1688. // ===== 已删除:user_id - 后端从token解析 =====
  1689. });
  1690. if (
  1691. latestRecordResponse.statusCode === 200 &&
  1692. latestRecordResponse.data
  1693. ) {
  1694. const latestRecord = latestRecordResponse.data;
  1695. console.log("最新识别记录:", latestRecord);
  1696. // 如果最新记录存在且未点评,提示用户
  1697. if (
  1698. latestRecord.effect_evaluation === 0 ||
  1699. !latestRecord.effect_evaluation
  1700. ) {
  1701. ElMessage.warning(
  1702. "请先对上一次识别结果进行点评,再进行新的识别"
  1703. );
  1704. isDragOver.value = false; // 重置拖拽状态
  1705. return;
  1706. }
  1707. }
  1708. } catch (error) {
  1709. console.error("检查最新识别记录失败:", error);
  1710. // 如果检查失败,继续执行识别流程
  1711. }
  1712. console.log("开始自动识别图片场景:", uploadedImageUrl.value);
  1713. // 开始识别状态
  1714. isIdentifying.value = true;
  1715. resetKeyElementState();
  1716. // 调用后端API进行隐患提示
  1717. // ElMessage.success("开始进行隐患提示,请稍候...");
  1718. // 模拟用户信息(暂时不从后端获取)
  1719. const account = "";
  1720. const username = "蜀道用户";
  1721. // 获取当日日期,格式:2025/10/23
  1722. const today = new Date();
  1723. const year = today.getFullYear();
  1724. const month = String(today.getMonth() + 1).padStart(2, "0");
  1725. const day = String(today.getDate()).padStart(2, "0");
  1726. const currentDate = `${year}/${month}/${day}`;
  1727. // 截取手机号后四位
  1728. const accountLastFour =
  1729. account.length >= 4 ? account.slice(-4) : account;
  1730. const requestData = {
  1731. // ===== 已删除:user_id - 后端从token解析 =====
  1732. image: uploadedImageUrl.value,
  1733. account: accountLastFour,
  1734. username: username,
  1735. date: currentDate,
  1736. };
  1737. console.log("发送自动场景识别请求:", requestData);
  1738. const { response, sceneKey } = await detectSceneAutomatically(
  1739. requestData
  1740. );
  1741. console.log("隐患提示响应:", response);
  1742. // 检查响应结构,兼容不同的字段名
  1743. const isSuccess = response.code === 200 || response.statusCode === 200;
  1744. if (isSuccess) {
  1745. ElMessage.success("隐患提示完成!");
  1746. // 保存识别结果
  1747. detectionResult.value = {
  1748. ...response.data,
  1749. scene_name: response.data.scene_name || sceneKey,
  1750. display_labels: response.data.display_labels || [],
  1751. element_hazards: response.data.element_hazards || {},
  1752. };
  1753. selectedScenario.value = detectionResult.value.scene_name || sceneKey;
  1754. // 处理标注后的图片
  1755. if (response.data.annotated_image) {
  1756. annotatedImageUrl.value = `${response.data.annotated_image}`;
  1757. }
  1758. // 开始过渡动画
  1759. isTransitioning.value = true;
  1760. // 等待右侧卡片滑出和左侧卡片拉伸动画完成(1秒)
  1761. setTimeout(() => {
  1762. // 跳转到详情页
  1763. currentView.value = "detail";
  1764. isTransitioning.value = false;
  1765. // 开始扫描效果
  1766. showScanningEffect.value = true;
  1767. showAnalysisPrompt.value = true; // 显示分析提示
  1768. }, 1000);
  1769. // 延迟4秒后显示识别结果(1秒过渡 + 3秒扫描)
  1770. setTimeout(() => {
  1771. showScanningEffect.value = false;
  1772. showAnalysisPrompt.value = false; // 隐藏分析提示
  1773. // 开始整个分析文本流式输出效果
  1774. startAnalysisStreaming();
  1775. }, 4000);
  1776. // 刷新历史记录
  1777. await getHistoryRecords();
  1778. // 自动选中最新创建的历史记录
  1779. if (historyData.value.length > 0) {
  1780. const latestRecord = historyData.value[0]; // 假设最新的记录在数组第一位
  1781. latestRecord.tagType =
  1782. detectionResult.value.scene_name || latestRecord.tagType;
  1783. latestRecord.detections =
  1784. detectionResult.value.detections || [];
  1785. selectedHistoryItem.value = latestRecord;
  1786. // 更新所有记录的active状态
  1787. historyData.value.forEach((item) => {
  1788. item.isActive = item.id === latestRecord.id;
  1789. });
  1790. console.log("自动选中最新记录:", latestRecord);
  1791. }
  1792. console.log("识别结果:", response.data);
  1793. console.log("标注图片URL:", annotatedImageUrl.value);
  1794. } else {
  1795. ElMessage.error(response.msg || "隐患提示失败");
  1796. }
  1797. } catch (error) {
  1798. console.error("开始识别失败:", error);
  1799. ElMessage.error("隐患提示失败: " + (error.msg || "未知错误"));
  1800. isDragOver.value = false; // 重置拖拽状态
  1801. } finally {
  1802. // 清除识别状态
  1803. isIdentifying.value = false;
  1804. }
  1805. };
  1806. // 打开示例弹窗
  1807. const openExampleModal = async (hazardInfo) => {
  1808. try {
  1809. console.log("openExampleModal 被调用,隐患信息:", hazardInfo);
  1810. // 如果正在加载中,直接返回避免重复请求
  1811. if (isLoadingExample.value) {
  1812. console.log("正在加载示例图,忽略重复点击");
  1813. return;
  1814. }
  1815. selectedHazard.value = hazardInfo;
  1816. isDragOver.value = false; // 重置拖拽状态
  1817. // 开始加载示例图
  1818. isLoadingExample.value = true;
  1819. // 调用API获取示例图
  1820. const response = await apis.getThirdSceneExampleImage({
  1821. third_scene_name: hazardInfo.description,
  1822. });
  1823. console.log("获取示例图响应:", response);
  1824. if (response.statusCode === 200) {
  1825. const exampleData = response.data;
  1826. // 检查是否有示例图数据
  1827. if (
  1828. exampleData &&
  1829. (exampleData.correct_example_image ||
  1830. exampleData.wrong_example_image)
  1831. ) {
  1832. exampleImages.value = {
  1833. correctImageUrl: exampleData.correct_example_image || "",
  1834. errorImageUrl: exampleData.wrong_example_image || "",
  1835. };
  1836. // 重置图片加载状态
  1837. imageLoadingStates.value = {
  1838. correct: false,
  1839. error: false,
  1840. };
  1841. showExampleModal.value = true;
  1842. console.log("示例弹窗已打开,示例图数据:", exampleImages.value);
  1843. console.log("API返回的原始数据:", exampleData);
  1844. console.log(
  1845. "正确示例图URL:",
  1846. exampleData.correct_example_image
  1847. );
  1848. console.log("错误示例图URL:", exampleData.wrong_example_image);
  1849. } else {
  1850. // 没有示例图数据,显示提示
  1851. ElMessage.warning("暂无示例图");
  1852. console.log("没有找到示例图数据");
  1853. }
  1854. } else {
  1855. ElMessage.error("获取示例图失败: " + (response.msg || "未知错误"));
  1856. console.error("获取示例图失败:", response.msg);
  1857. }
  1858. } catch (error) {
  1859. console.error("打开示例弹窗失败:", error);
  1860. ElMessage.error("获取示例图失败,请稍后重试");
  1861. isDragOver.value = false; // 重置拖拽状态
  1862. } finally {
  1863. isLoadingExample.value = false;
  1864. }
  1865. };
  1866. // 关闭示例弹窗
  1867. const closeExampleModal = () => {
  1868. try {
  1869. console.log("closeExampleModal 被调用");
  1870. showExampleModal.value = false;
  1871. // 清空示例图数据
  1872. exampleImages.value = {};
  1873. selectedHazard.value = null;
  1874. isLoadingExample.value = false;
  1875. imageLoadingStates.value = { correct: false, error: false };
  1876. console.log("示例弹窗已关闭");
  1877. } catch (error) {
  1878. console.error("关闭示例弹窗失败:", error);
  1879. }
  1880. };
  1881. // 处理图片加载错误
  1882. const handleImageError = (event, type) => {
  1883. console.log(`图片加载失败 (${type}):`, event.target.src);
  1884. // 清空对应的图片URL,这样会显示"暂无示例图"
  1885. if (type === "correct") {
  1886. exampleImages.value.correctImageUrl = "";
  1887. imageLoadingStates.value.correct = false;
  1888. } else if (type === "error") {
  1889. exampleImages.value.errorImageUrl = "";
  1890. imageLoadingStates.value.error = false;
  1891. }
  1892. };
  1893. // 处理图片加载完成,检测图片方向
  1894. const handleImageLoad = (event, type) => {
  1895. const img = event.target;
  1896. const aspectRatio = img.naturalWidth / img.naturalHeight;
  1897. // 根据宽高比判断图片方向
  1898. if (aspectRatio > 1) {
  1899. // 横图
  1900. img.setAttribute("data-orientation", "landscape");
  1901. console.log(
  1902. `图片加载完成 (${type}): 横图, 宽高比: ${aspectRatio.toFixed(2)}`
  1903. );
  1904. } else {
  1905. // 竖图
  1906. img.setAttribute("data-orientation", "portrait");
  1907. console.log(
  1908. `图片加载完成 (${type}): 竖图, 宽高比: ${aspectRatio.toFixed(2)}`
  1909. );
  1910. }
  1911. // 设置对应图片的加载状态为完成
  1912. if (type === "correct") {
  1913. imageLoadingStates.value.correct = false;
  1914. } else if (type === "error") {
  1915. imageLoadingStates.value.error = false;
  1916. }
  1917. console.log(`图片加载完成 (${type}):`, img.src);
  1918. };
  1919. // 打开图片预览(支持自定义URL)
  1920. const openImagePreview = (imageUrl = null) => {
  1921. try {
  1922. console.log("openImagePreview 被调用");
  1923. if (imageUrl) {
  1924. // 如果提供了URL,使用它作为预览图片
  1925. previewImageUrl.value = imageUrl;
  1926. } else {
  1927. // 否则使用默认的标注图片
  1928. previewImageUrl.value = annotatedImageUrl.value;
  1929. }
  1930. showImagePreview.value = true;
  1931. console.log("图片预览已打开,URL:", previewImageUrl.value);
  1932. } catch (error) {
  1933. console.error("打开图片预览失败:", error);
  1934. }
  1935. };
  1936. // 关闭图片预览
  1937. const closeImagePreview = () => {
  1938. try {
  1939. console.log("closeImagePreview 被调用");
  1940. showImagePreview.value = false;
  1941. previewImageUrl.value = "";
  1942. console.log("图片预览已关闭");
  1943. } catch (error) {
  1944. console.error("关闭图片预览失败:", error);
  1945. }
  1946. };
  1947. // 处理主图片加载错误
  1948. const handleMainImageError = (event) => {
  1949. try {
  1950. console.log("主图片加载失败");
  1951. // 可以在这里添加其他错误处理逻辑
  1952. } catch (error) {
  1953. console.error("处理主图片错误失败:", error);
  1954. }
  1955. };
  1956. // 开始整个分析文本流式输出效果
  1957. const startAnalysisStreaming = () => {
  1958. try {
  1959. console.log("开始整个分析文本流式输出效果");
  1960. // 重置状态
  1961. isStreamingAnalysis.value = false;
  1962. streamingAnalysis.value = "";
  1963. resetKeyElementState();
  1964. visibleHazardCards.value = {}; // 重置隐患卡片显示状态
  1965. // 立即开始流式输出,不需要延迟
  1966. // 构建完整的分析文本(带HTML标签)
  1967. const sceneName = detectionResult.value?.scene_name;
  1968. const sceneText = sceneName ? scenarios[sceneName]?.name : "未知场景";
  1969. const labelsArray = displayLabels.value;
  1970. // 构建带HTML的完整文本
  1971. const labelsTags = labelsArray
  1972. .map((label) => `<span class="label-tag">${label}</span>`)
  1973. .join("");
  1974. const fullAnalysisText = `我识别到这是一个<span class="scene-tag">${sceneText}</span>场景,检测到的关键要素为${labelsTags}`;
  1975. // 将HTML文本分解为可见字符数组(处理标签)
  1976. const segments = [];
  1977. let tempText = fullAnalysisText;
  1978. let inTag = false;
  1979. let currentSegment = "";
  1980. for (let i = 0; i < tempText.length; i++) {
  1981. const char = tempText[i];
  1982. if (char === "<") {
  1983. // 如果之前有文本,先保存
  1984. if (currentSegment && !inTag) {
  1985. segments.push(currentSegment);
  1986. currentSegment = "";
  1987. }
  1988. inTag = true;
  1989. currentSegment += char;
  1990. } else if (char === ">") {
  1991. currentSegment += char;
  1992. // 标签结束,保存整个标签
  1993. segments.push(currentSegment);
  1994. currentSegment = "";
  1995. inTag = false;
  1996. } else if (inTag) {
  1997. // 在标签内,继续累积
  1998. currentSegment += char;
  1999. } else {
  2000. // 在标签外,每个字符单独作为一个segment
  2001. segments.push(char);
  2002. }
  2003. }
  2004. if (currentSegment) {
  2005. segments.push(currentSegment);
  2006. }
  2007. // 开始流式输出
  2008. isStreamingAnalysis.value = true;
  2009. let currentIndex = 0;
  2010. const streamInterval = setInterval(() => {
  2011. if (currentIndex < segments.length) {
  2012. streamingAnalysis.value += segments[currentIndex];
  2013. currentIndex++;
  2014. } else {
  2015. // 输出完成
  2016. clearInterval(streamInterval);
  2017. isStreamingAnalysis.value = false;
  2018. console.log("分析文本流式输出完成");
  2019. // 文本流式输出完成后,延迟500ms开始显示隐患卡片
  2020. setTimeout(() => {
  2021. showHazardCardsSequentially();
  2022. }, 500);
  2023. }
  2024. }, 30); // 每30ms输出一个segment
  2025. } catch (error) {
  2026. console.error("开始分析文本流式输出失败:", error);
  2027. // 出错时直接显示完整内容
  2028. isStreamingAnalysis.value = false;
  2029. }
  2030. };
  2031. // 逐个显示隐患卡片
  2032. const showHazardCardsSequentially = () => {
  2033. try {
  2034. console.log("开始逐个显示隐患卡片");
  2035. const hazards = filteredHazards.value || [];
  2036. visibleHazardCards.value = {};
  2037. // 在第一个卡片显示之前,先滚动到隐患section
  2038. setTimeout(() => {
  2039. const hazardsSection = document.querySelector(".hazards-section");
  2040. if (hazardsSection) {
  2041. hazardsSection.scrollIntoView({
  2042. behavior: "smooth",
  2043. block: "start",
  2044. });
  2045. }
  2046. }, 100);
  2047. hazards.forEach((hazard, index) => {
  2048. setTimeout(() => {
  2049. visibleHazardCards.value[index] = true;
  2050. console.log(`显示第${index + 1}个隐患卡片`);
  2051. // 每显示一个卡片,平滑滚动确保可见
  2052. setTimeout(() => {
  2053. const cards = document.querySelectorAll(".hazard-card");
  2054. if (cards[index]) {
  2055. cards[index].scrollIntoView({
  2056. behavior: "smooth",
  2057. block: "nearest",
  2058. inline: "nearest",
  2059. });
  2060. }
  2061. }, 50);
  2062. }, index * 150); // 每个卡片延迟150ms显示
  2063. });
  2064. } catch (error) {
  2065. console.error("显示隐患卡片失败:", error);
  2066. }
  2067. };
  2068. // 新增:清除上传的图片
  2069. const clearUploadedImage = () => {
  2070. try {
  2071. console.log("clearUploadedImage 被调用");
  2072. uploadedImage.value = null;
  2073. uploadedImageUrl.value = "";
  2074. annotatedImageUrl.value = "";
  2075. detectionResult.value = null;
  2076. resetKeyElementState();
  2077. isDragOver.value = false; // 重置拖拽状态
  2078. if (fileInput.value) {
  2079. fileInput.value.value = ""; // 清空input的value
  2080. console.log("文件输入已清空");
  2081. } else {
  2082. console.log("fileInput 引用为空,无法清空");
  2083. }
  2084. console.log("上传图片已清除");
  2085. } catch (error) {
  2086. console.error("清除上传图片失败:", error);
  2087. }
  2088. };
  2089. // 新增:重新选择图片
  2090. const reselectImage = () => {
  2091. try {
  2092. console.log("reselectImage 被调用");
  2093. clearUploadedImage();
  2094. isDragOver.value = false; // 重置拖拽状态
  2095. triggerFileUpload();
  2096. console.log("重新选择图片完成");
  2097. } catch (error) {
  2098. console.error("重新选择图片失败:", error);
  2099. }
  2100. };
  2101. // 新增:处理拖拽上传
  2102. const handleDrop = (event) => {
  2103. try {
  2104. console.log("handleDrop 被调用");
  2105. event.preventDefault();
  2106. isDragOver.value = false;
  2107. const file = event.dataTransfer.files[0];
  2108. console.log("拖拽的文件:", file);
  2109. if (file) {
  2110. processFile(file); // 使用通用的文件处理函数
  2111. } else {
  2112. console.log("没有拖拽文件");
  2113. isDragOver.value = false; // 如果没有文件,确保重置拖拽状态
  2114. }
  2115. } catch (error) {
  2116. console.error("拖拽处理失败:", error);
  2117. isDragOver.value = false; // 重置拖拽状态
  2118. }
  2119. };
  2120. // 新增:处理拖拽进入
  2121. const handleDragOver = (event) => {
  2122. try {
  2123. console.log("handleDragOver 被调用");
  2124. event.preventDefault();
  2125. isDragOver.value = true;
  2126. console.log("拖拽进入状态已设置");
  2127. } catch (error) {
  2128. console.error("拖拽进入处理失败:", error);
  2129. isDragOver.value = false; // 重置拖拽状态
  2130. }
  2131. };
  2132. // 新增:处理拖拽离开
  2133. const handleDragLeave = (event) => {
  2134. try {
  2135. console.log("handleDragLeave 被调用");
  2136. event.preventDefault();
  2137. isDragOver.value = false;
  2138. console.log("拖拽离开状态已设置");
  2139. } catch (error) {
  2140. console.error("拖拽离开处理失败:", error);
  2141. isDragOver.value = false; // 重置拖拽状态
  2142. }
  2143. };
  2144. // 新增:处理图片方向,确保保持原始方向
  2145. const processImageOrientation = (file) => {
  2146. return new Promise((resolve) => {
  2147. const canvas = document.createElement("canvas");
  2148. const ctx = canvas.getContext("2d");
  2149. const img = new Image();
  2150. img.onload = () => {
  2151. // 设置画布尺寸为图片的原始尺寸
  2152. canvas.width = img.naturalWidth;
  2153. canvas.height = img.naturalHeight;
  2154. // 绘制图片,保持原始方向
  2155. ctx.drawImage(img, 0, 0);
  2156. // 将画布转换为Blob
  2157. canvas.toBlob((blob) => {
  2158. // 创建一个新的File对象,保持原始文件名和类型
  2159. const processedFile = new File([blob], file.name, {
  2160. type: file.type,
  2161. lastModified: file.lastModified,
  2162. });
  2163. resolve(processedFile);
  2164. }, file.type);
  2165. };
  2166. img.src = URL.createObjectURL(file);
  2167. });
  2168. };
  2169. // 新增:通用的文件处理函数
  2170. const processFile = async (file) => {
  2171. try {
  2172. console.log("processFile 被调用,文件:", file);
  2173. // 检查文件大小(5MB限制)
  2174. if (file.size > 5 * 1024 * 1024) {
  2175. console.log("文件大小超过限制:", file.size);
  2176. ElMessage.error("文件大小不能超过5MB");
  2177. isDragOver.value = false; // 重置拖拽状态
  2178. return;
  2179. }
  2180. // 检查文件格式
  2181. const allowedTypes = ["image/jpeg", "image/jpg", "image/png"];
  2182. console.log("文件类型:", file.type);
  2183. if (!allowedTypes.includes(file.type)) {
  2184. console.log("不支持的文件类型:", file.type);
  2185. ElMessage.error("只支持JPG、PNG格式的图片");
  2186. isDragOver.value = false; // 重置拖拽状态
  2187. return;
  2188. }
  2189. // 处理图片方向,确保保持原始方向
  2190. const processedFile = await processImageOrientation(file);
  2191. uploadedImage.value = processedFile;
  2192. console.log("选择文件:", processedFile.name);
  2193. // 自动上传文件到后端
  2194. console.log("开始上传文件到服务器");
  2195. await uploadFileToServer(processedFile);
  2196. } catch (error) {
  2197. console.error("处理文件失败:", error);
  2198. isDragOver.value = false; // 重置拖拽状态
  2199. }
  2200. };
  2201. // 处理文件上传
  2202. const handleFileUpload = async (event) => {
  2203. try {
  2204. console.log("handleFileUpload 被调用", event);
  2205. const file = event.target.files[0];
  2206. console.log("选择的文件:", file);
  2207. if (file) {
  2208. await processFile(file);
  2209. } else {
  2210. console.log("没有选择文件");
  2211. }
  2212. } catch (error) {
  2213. console.error("文件上传处理失败:", error);
  2214. isDragOver.value = false; // 重置拖拽状态
  2215. }
  2216. };
  2217. // 获取历史记录
  2218. const getHistoryRecords = async () => {
  2219. try {
  2220. console.log("📋 开始获取隐患识别历史记录...");
  2221. isLoadingHistory.value = true;
  2222. const startTime = performance.now();
  2223. const response = await apis.getHazardHistory({
  2224. // ===== 已删除:user_id - 后端从token解析 =====
  2225. });
  2226. const endTime = performance.now();
  2227. console.log(
  2228. `📋 隐患识别历史记录API调用耗时: ${(endTime - startTime).toFixed(
  2229. 2
  2230. )}ms`
  2231. );
  2232. console.log("📋 历史记录响应:", response);
  2233. if (response.statusCode === 200 || response.code === 200) {
  2234. // 设置历史记录总数
  2235. historyTotal.value = response.total || 0;
  2236. // 处理历史记录数据(简化版本)
  2237. const records = response.data || [];
  2238. historyData.value = records.map((record, index) => ({
  2239. id: record.id || index,
  2240. title: record.title || "隐患提示记录",
  2241. description: record.description || "暂无描述",
  2242. time: formatTime(record.created_at),
  2243. tagType: record.tag_type, // 默认标签,详情加载时会更新
  2244. isActive: false,
  2245. originalImageUrl: record.original_image_url,
  2246. recognitionImageUrl: record.recognition_image_url,
  2247. labels: record.labels || "",
  2248. third_scenes: [], // 历史记录中不包含详细数据
  2249. effect_evaluation: record.effect_evaluation || 0, // 添加点评状态参数
  2250. }));
  2251. console.log(
  2252. `✅ 隐患识别历史记录处理完成: ${historyData.value.length}条记录,总数: ${historyTotal.value}`
  2253. );
  2254. } else {
  2255. console.error("❌ 获取历史记录失败:", response.message);
  2256. }
  2257. } catch (error) {
  2258. console.error("❌ 获取历史记录失败:", error);
  2259. } finally {
  2260. isLoadingHistory.value = false;
  2261. }
  2262. };
  2263. // 格式化时间
  2264. const formatTime = (timestamp) => {
  2265. if (!timestamp) return "未知时间";
  2266. console.log(
  2267. "formatTime 被调用,原始时间戳:",
  2268. timestamp,
  2269. "类型:",
  2270. typeof timestamp
  2271. );
  2272. // 处理时间戳
  2273. let date;
  2274. if (typeof timestamp === "string") {
  2275. // 如果是ISO字符串格式,直接创建Date对象
  2276. date = new Date(timestamp);
  2277. } else {
  2278. // 如果是数字时间戳
  2279. let timestampMs = timestamp;
  2280. if (timestamp.toString().length === 10) {
  2281. timestampMs = timestamp * 1000;
  2282. } else if (timestamp.toString().length === 11) {
  2283. timestampMs = timestamp * 1000;
  2284. } else if (timestamp.toString().length === 13) {
  2285. // 13位时间戳,直接使用
  2286. } else {
  2287. timestampMs = timestamp * 1000;
  2288. }
  2289. date = new Date(timestampMs);
  2290. }
  2291. console.log("转换后的日期:", date);
  2292. const now = new Date();
  2293. // 获取今天的开始时间(0点0分0秒)
  2294. const todayStart = new Date(
  2295. now.getFullYear(),
  2296. now.getMonth(),
  2297. now.getDate()
  2298. );
  2299. // 获取昨天的开始时间
  2300. const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000);
  2301. console.log("今天开始时间:", todayStart);
  2302. console.log("昨天开始时间:", yesterdayStart);
  2303. console.log("记录时间:", date);
  2304. // 今天的对话(日期相同)
  2305. if (date >= todayStart) {
  2306. const result = date.toLocaleTimeString("zh-CN", {
  2307. hour: "2-digit",
  2308. minute: "2-digit",
  2309. });
  2310. console.log("今天对话,返回:", result);
  2311. return result;
  2312. }
  2313. // 昨天的对话(日期是昨天)
  2314. if (date >= yesterdayStart && date < todayStart) {
  2315. const result =
  2316. "昨天 " +
  2317. date.toLocaleTimeString("zh-CN", {
  2318. hour: "2-digit",
  2319. minute: "2-digit",
  2320. });
  2321. console.log("昨天对话,返回:", result);
  2322. return result;
  2323. }
  2324. // 更早的对话:显示为 "8月30日 15:30" 格式
  2325. const month = date.getMonth() + 1; // getMonth() 返回 0-11
  2326. const day = date.getDate();
  2327. const time = date.toLocaleTimeString("zh-CN", {
  2328. hour: "2-digit",
  2329. minute: "2-digit",
  2330. });
  2331. const result = `${month}月${day}日 ${time}`;
  2332. console.log("更早对话,返回:", result);
  2333. return result;
  2334. };
  2335. // 根据标签获取标签类型
  2336. const getTagTypeFromLabels = (labels) => {
  2337. if (!labels) return "gas_station";
  2338. let labelStr = "";
  2339. // 处理数组格式的labels
  2340. if (Array.isArray(labels)) {
  2341. labelStr = labels.join(" ").toLowerCase();
  2342. } else {
  2343. // 处理字符串格式的labels
  2344. labelStr = String(labels).toLowerCase();
  2345. }
  2346. if (labelStr.includes("隧道")) return "tunnel";
  2347. if (labelStr.includes("桥梁")) return "simple_supported_bridge";
  2348. if (labelStr.includes("加油站")) return "gas_station";
  2349. if (labelStr.includes("设备")) return "special_equipment";
  2350. if (labelStr.includes("高速")) return "operate_highway";
  2351. return "simple_supported_bridge"; // 默认返回桥梁工程
  2352. };
  2353. // 获取当前时间
  2354. const getCurrentTime = () => {
  2355. const now = new Date();
  2356. const month = now.getMonth() + 1;
  2357. const day = now.getDate();
  2358. const hours = now.getHours().toString().padStart(2, "0");
  2359. const minutes = now.getMinutes().toString().padStart(2, "0");
  2360. return `${month}月${day}日 ${hours}:${minutes}`;
  2361. };
  2362. // 等待图片加载完成
  2363. const waitForImageLoad = (imageUrl) => {
  2364. return new Promise((resolve, reject) => {
  2365. if (!imageUrl) {
  2366. resolve();
  2367. return;
  2368. }
  2369. const img = new Image();
  2370. const timeout = setTimeout(() => {
  2371. console.warn("图片加载超时:", imageUrl);
  2372. resolve(); // 超时也resolve,避免阻塞
  2373. }, 10000); // 10秒超时
  2374. img.onload = () => {
  2375. clearTimeout(timeout);
  2376. console.log("图片加载完成:", imageUrl);
  2377. resolve();
  2378. };
  2379. img.onerror = () => {
  2380. clearTimeout(timeout);
  2381. console.error("图片加载失败:", imageUrl);
  2382. resolve(); // 即使失败也resolve,避免阻塞
  2383. };
  2384. img.src = imageUrl;
  2385. });
  2386. };
  2387. // 打开点评弹窗
  2388. const openEvaluationModal = () => {
  2389. try {
  2390. console.log("打开点评弹窗");
  2391. showEvaluationModal.value = true;
  2392. // 如果是已点评状态,回显数据
  2393. if (selectedHistoryItem.value?.effect_evaluation > 0) {
  2394. // 从后端获取详细的点评数据
  2395. loadEvaluationData();
  2396. } else {
  2397. // 重置评价数据
  2398. evaluationData.value = {
  2399. sceneMatch: null,
  2400. promptAccurate: null,
  2401. rating: 0,
  2402. userRemark: "",
  2403. };
  2404. }
  2405. } catch (error) {
  2406. console.error("打开点评弹窗失败:", error);
  2407. }
  2408. };
  2409. // 加载点评数据
  2410. const loadEvaluationData = async () => {
  2411. try {
  2412. console.log("加载点评数据");
  2413. // 从详情接口获取完整的点评数据
  2414. const detailResponse = await apis.getRecognitionRecordDetail({
  2415. recognition_record_id: selectedHistoryItem.value.id,
  2416. });
  2417. if (detailResponse.statusCode === 200 || detailResponse.code === 200) {
  2418. const detailData = detailResponse.data;
  2419. // 回显数据,将整数转换为布尔值
  2420. evaluationData.value = {
  2421. sceneMatch: detailData.scene_match === 1,
  2422. promptAccurate: detailData.tip_accuracy === 1,
  2423. rating: detailData.effect_evaluation || 0,
  2424. userRemark: detailData.user_remark || "",
  2425. };
  2426. console.log("点评数据回显成功:", evaluationData.value);
  2427. } else {
  2428. console.error("获取点评数据失败:", detailResponse.message);
  2429. // 如果获取失败,使用默认值
  2430. evaluationData.value = {
  2431. sceneMatch: null,
  2432. promptAccurate: null,
  2433. rating: selectedHistoryItem.value.effect_evaluation || 0,
  2434. userRemark: "",
  2435. };
  2436. }
  2437. } catch (error) {
  2438. console.error("加载点评数据失败:", error);
  2439. // 如果获取失败,使用默认值
  2440. evaluationData.value = {
  2441. sceneMatch: null,
  2442. promptAccurate: null,
  2443. rating: selectedHistoryItem.value.effect_evaluation || 0,
  2444. userRemark: "",
  2445. };
  2446. }
  2447. };
  2448. // 关闭点评弹窗
  2449. const closeEvaluationModal = () => {
  2450. try {
  2451. console.log("关闭点评弹窗");
  2452. showEvaluationModal.value = false;
  2453. } catch (error) {
  2454. console.error("关闭点评弹窗失败:", error);
  2455. }
  2456. };
  2457. // 处理用户意见输入
  2458. const handleRemarkInput = (event) => {
  2459. const value = event.target.value;
  2460. // 限制输入长度
  2461. if (value.length > 200) {
  2462. evaluationData.value.userRemark = value.substring(0, 200);
  2463. } else {
  2464. evaluationData.value.userRemark = value;
  2465. }
  2466. };
  2467. // 提交评价
  2468. const submitEvaluation = async () => {
  2469. try {
  2470. console.log("提交评价:", evaluationData.value);
  2471. // 验证是否所有问题都已回答
  2472. if (
  2473. evaluationData.value.sceneMatch === null ||
  2474. evaluationData.value.promptAccurate === null ||
  2475. evaluationData.value.rating === 0
  2476. ) {
  2477. ElMessage.warning("请完成所有评价项目");
  2478. return;
  2479. }
  2480. // 调用后端API提交评价
  2481. const response = await apis.submitEvaluation({
  2482. id: selectedHistoryItem.value?.id,
  2483. scene_match: evaluationData.value.sceneMatch ? 1 : 0,
  2484. tip_accuracy: evaluationData.value.promptAccurate ? 1 : 0,
  2485. effect_evaluation: evaluationData.value.rating,
  2486. user_remark: evaluationData.value.userRemark,
  2487. });
  2488. if (response.statusCode === 200 || response.code === 200) {
  2489. ElMessage.success("评价提交成功");
  2490. // 更新当前历史记录的点评状态
  2491. if (selectedHistoryItem.value) {
  2492. selectedHistoryItem.value.effect_evaluation =
  2493. evaluationData.value.rating;
  2494. }
  2495. // 更新历史记录列表中的对应项
  2496. const historyItem = historyData.value.find(
  2497. (item) => item.id === selectedHistoryItem.value?.id
  2498. );
  2499. if (historyItem) {
  2500. historyItem.effect_evaluation = evaluationData.value.rating;
  2501. }
  2502. // 关闭弹窗
  2503. closeEvaluationModal();
  2504. } else {
  2505. ElMessage.error("评价提交失败: " + (response.msg || "未知错误"));
  2506. }
  2507. } catch (error) {
  2508. console.error("提交评价失败:", error);
  2509. ElMessage.error("评价提交失败,请稍后重试");
  2510. }
  2511. };
  2512. watch(
  2513. filteredHazards,
  2514. (hazards) => {
  2515. if (
  2516. showScanningEffect.value ||
  2517. showAnalysisPrompt.value ||
  2518. isStreamingAnalysis.value
  2519. ) {
  2520. return;
  2521. }
  2522. setVisibleHazardCards(hazards);
  2523. },
  2524. { deep: false }
  2525. );
  2526. watch(
  2527. [
  2528. selectedKeyElement,
  2529. currentView,
  2530. showScanningEffect,
  2531. () => detectionResult.value?.detections,
  2532. ],
  2533. async () => {
  2534. if (
  2535. !selectedKeyElement.value ||
  2536. currentView.value !== "detail" ||
  2537. showScanningEffect.value
  2538. ) {
  2539. elementOverlayStyle.value = null;
  2540. return;
  2541. }
  2542. await nextTick();
  2543. updateElementOverlayPosition();
  2544. }
  2545. );
  2546. const handleWindowResize = () => {
  2547. updateElementOverlayPosition();
  2548. };
  2549. onMounted(() => {
  2550. getHistoryRecords();
  2551. window.addEventListener("resize", handleWindowResize);
  2552. });
  2553. onBeforeUnmount(() => {
  2554. window.removeEventListener("resize", handleWindowResize);
  2555. });
  2556. </script>
  2557. <style lang="less" scoped>
  2558. // 删除图标样式
  2559. .delete-icon {
  2560. width: 16px;
  2561. height: 16px;
  2562. }
  2563. .chat-container {
  2564. display: flex;
  2565. height: 100vh;
  2566. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  2567. }
  2568. /* 中间历史记录栏 */
  2569. .history-sidebar {
  2570. width: 280px;
  2571. background: #e8f0ff;
  2572. display: flex;
  2573. flex-direction: column;
  2574. }
  2575. /* 中间历史记录栏样式 */
  2576. .history-sidebar {
  2577. padding: 24px 16px 0 16px;
  2578. .history-header {
  2579. background: transparent;
  2580. .section-title {
  2581. font-size: 16px;
  2582. font-weight: 600;
  2583. color: #2c3e50;
  2584. }
  2585. .new-chat-btn {
  2586. width: 248px;
  2587. height: 40px;
  2588. cursor: pointer;
  2589. transition: opacity 0.3s ease;
  2590. object-fit: contain;
  2591. display: block;
  2592. margin-top: 16px;
  2593. margin-bottom: 16px;
  2594. &:hover {
  2595. opacity: 0.8;
  2596. }
  2597. }
  2598. }
  2599. .history-list {
  2600. flex: 1;
  2601. overflow-y: auto;
  2602. width: 248px;
  2603. height: 120px;
  2604. /* 隐藏滚动条 */
  2605. &::-webkit-scrollbar {
  2606. display: none;
  2607. }
  2608. -ms-overflow-style: none;
  2609. scrollbar-width: none;
  2610. /* 空状态样式 */
  2611. .empty-history {
  2612. display: flex;
  2613. flex-direction: column;
  2614. align-items: center;
  2615. justify-content: center;
  2616. margin-top: 236px;
  2617. .empty-icon {
  2618. width: 147px;
  2619. height: 148px;
  2620. object-fit: contain;
  2621. margin-bottom: 16px;
  2622. }
  2623. .empty-text {
  2624. font-size: 16px;
  2625. color: #9c9fa7;
  2626. text-align: center;
  2627. }
  2628. }
  2629. .history-item {
  2630. background: white;
  2631. border-radius: 8px;
  2632. padding: 12px 18px 12px 15px;
  2633. margin-bottom: 8px;
  2634. cursor: pointer;
  2635. transition: all 0.3s ease;
  2636. border-left: 3px solid transparent;
  2637. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  2638. display: flex;
  2639. position: relative;
  2640. &:hover {
  2641. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  2642. .delete-btn {
  2643. opacity: 1;
  2644. visibility: visible;
  2645. }
  2646. }
  2647. &.active {
  2648. border-left-color: #3e7bfa;
  2649. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  2650. }
  2651. .history-content {
  2652. width: 100%;
  2653. .title-row {
  2654. display: flex;
  2655. justify-content: space-between;
  2656. align-items: center;
  2657. // margin-bottom: 4px;
  2658. .history-title {
  2659. font-size: 14px;
  2660. font-weight: 600;
  2661. line-height: 1.4;
  2662. color: #1f2937;
  2663. flex: 1;
  2664. overflow: hidden;
  2665. text-overflow: ellipsis;
  2666. white-space: nowrap;
  2667. margin-right: 8px;
  2668. }
  2669. .history-tag {
  2670. font-size: 11px;
  2671. padding: 2px 8px;
  2672. border-radius: 4px;
  2673. font-weight: 500;
  2674. flex-shrink: 0;
  2675. &.tag-tunnel {
  2676. background: rgba(62, 123, 250, 0.1);
  2677. color: #3366e6;
  2678. }
  2679. &.tag-bridge {
  2680. background: rgba(34, 184, 80, 0.1);
  2681. color: #22b850;
  2682. }
  2683. &.tag-equipment {
  2684. background: rgba(0, 128, 255, 0.1);
  2685. color: #0080ff;
  2686. }
  2687. &.tag-highway {
  2688. background: rgba(114, 46, 209, 0.1);
  2689. color: #722ed1;
  2690. }
  2691. &.tag-gas-station {
  2692. background: rgba(255, 77, 79, 0.1);
  2693. color: #ff4d4f;
  2694. }
  2695. }
  2696. }
  2697. .time-row {
  2698. margin-bottom: 4px;
  2699. .history-time {
  2700. font-size: 12px;
  2701. color: #6b7280;
  2702. }
  2703. }
  2704. .desc-row {
  2705. display: flex;
  2706. align-items: flex-start;
  2707. gap: 12px;
  2708. .history-icon {
  2709. width: 48px;
  2710. height: 48px;
  2711. flex-shrink: 0;
  2712. .history-icon-img {
  2713. width: 100%;
  2714. height: 100%;
  2715. object-fit: contain;
  2716. border-radius: 4px;
  2717. }
  2718. }
  2719. .history-desc {
  2720. flex: 1;
  2721. font-size: 12px;
  2722. line-height: 1.4;
  2723. color: #6b7280;
  2724. overflow: hidden;
  2725. display: -webkit-box;
  2726. -webkit-line-clamp: 3;
  2727. line-clamp: 3;
  2728. -webkit-box-orient: vertical;
  2729. text-overflow: ellipsis;
  2730. }
  2731. }
  2732. }
  2733. .delete-btn {
  2734. opacity: 0;
  2735. visibility: hidden;
  2736. transition: all 0.2s ease;
  2737. cursor: pointer;
  2738. padding: 4px 4px;
  2739. border-radius: 4px;
  2740. color: #7c8db5;
  2741. display: flex;
  2742. align-items: center;
  2743. justify-content: center;
  2744. margin-left: 8px;
  2745. flex-shrink: 0;
  2746. position: absolute;
  2747. top: 35px;
  2748. right: 2px;
  2749. &:hover {
  2750. color: #ff4757;
  2751. background-color: rgba(255, 71, 87, 0.1);
  2752. }
  2753. &.always-visible {
  2754. opacity: 1;
  2755. visibility: visible;
  2756. }
  2757. }
  2758. }
  2759. }
  2760. }
  2761. /* 主工作区域 */
  2762. .main-work {
  2763. flex: 1;
  2764. background: #ebf3ff;
  2765. display: flex;
  2766. flex-direction: column;
  2767. }
  2768. /* 工作头部 */
  2769. .work-header {
  2770. background: transparent;
  2771. padding: 30px 0px 0px 18px;
  2772. h2 {
  2773. margin: 0;
  2774. font-size: 20px;
  2775. font-weight: 600;
  2776. color: #2c3e50;
  2777. }
  2778. }
  2779. /* 工作内容区域 */
  2780. .work-content {
  2781. flex: 1;
  2782. padding: 22px;
  2783. overflow-y: auto;
  2784. display: flex;
  2785. flex-direction: column;
  2786. scroll-behavior: smooth;
  2787. }
  2788. /* 主布局:左右分栏 */
  2789. .main-layout {
  2790. display: flex;
  2791. gap: 20px;
  2792. height: 100%;
  2793. // min-height: 600px;
  2794. }
  2795. /* 左侧:隐患提示系统 */
  2796. .left-section {
  2797. width: 100%;
  2798. background: white;
  2799. border-radius: 16px;
  2800. padding: 30px 30px 15px 30px;
  2801. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  2802. display: flex;
  2803. flex-direction: column;
  2804. align-items: center;
  2805. min-height: min-content;
  2806. transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
  2807. &.transitioning {
  2808. width: 100%;
  2809. max-width: 100%;
  2810. padding: 40px;
  2811. transform: scale(1.02);
  2812. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
  2813. }
  2814. .hazard-system {
  2815. flex: 1;
  2816. display: flex;
  2817. flex-direction: column;
  2818. width: 687px;
  2819. .system-header {
  2820. text-align: center;
  2821. margin-bottom: 66px;
  2822. h3 {
  2823. font-size: 24px;
  2824. font-weight: 600;
  2825. color: #1f2937;
  2826. margin: 0 0 10px 0;
  2827. }
  2828. p {
  2829. font-size: 16px;
  2830. color: #6b7280;
  2831. margin: 0;
  2832. line-height: 1.4;
  2833. }
  2834. }
  2835. .step-section {
  2836. margin-bottom: 51px;
  2837. h4 {
  2838. font-size: 16px;
  2839. font-weight: 600;
  2840. color: #1f2937;
  2841. margin: 0 0 8px 0;
  2842. }
  2843. .step-description {
  2844. font-size: 14px;
  2845. color: #6b7280;
  2846. margin: 0 0 12px 0;
  2847. line-height: 1.4;
  2848. }
  2849. .scenario-tags {
  2850. display: flex;
  2851. flex-wrap: wrap;
  2852. gap: 12px;
  2853. .scenario-tag {
  2854. padding: 10px 20px;
  2855. border-radius: 8px;
  2856. font-size: 14px;
  2857. font-weight: 500;
  2858. cursor: pointer;
  2859. transition: all 0.3s ease;
  2860. border: 2px solid transparent;
  2861. background: #f8faff;
  2862. color: #1f2937;
  2863. &.active {
  2864. background: rgba(62, 123, 250, 0.1);
  2865. color: #3366e6;
  2866. border-color: #3366e6;
  2867. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  2868. }
  2869. &.disabled {
  2870. background: #f3f4f6;
  2871. color: #9ca3af;
  2872. border-color: #d1d5db;
  2873. cursor: not-allowed;
  2874. opacity: 0.6;
  2875. &:hover {
  2876. transform: none;
  2877. box-shadow: none;
  2878. }
  2879. }
  2880. &.identifying-disabled {
  2881. background: #f3f4f6;
  2882. color: #9ca3af;
  2883. border-color: #d1d5db;
  2884. cursor: not-allowed;
  2885. opacity: 0.6;
  2886. pointer-events: none;
  2887. &:hover {
  2888. transform: none;
  2889. box-shadow: none;
  2890. }
  2891. }
  2892. &:hover:not(.disabled) {
  2893. transform: translateY(-2px);
  2894. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  2895. }
  2896. }
  2897. }
  2898. .upload-area {
  2899. position: relative;
  2900. border: 2px dashed #3e7bfa;
  2901. border-radius: 12px;
  2902. margin-top: 12px;
  2903. height: 380px;
  2904. text-align: center;
  2905. cursor: pointer;
  2906. transition: all 0.3s ease;
  2907. &:hover {
  2908. border-color: #3e7bfa;
  2909. background: rgba(62, 123, 250, 0.02);
  2910. }
  2911. &.drag-over {
  2912. border-color: #3e7bfa;
  2913. background: rgba(62, 123, 250, 0.05);
  2914. box-shadow: 0 0 10px rgba(62, 123, 250, 0.2);
  2915. }
  2916. .uploaded-image-container {
  2917. position: relative;
  2918. width: 100%;
  2919. height: 100%;
  2920. display: flex;
  2921. align-items: center;
  2922. justify-content: center;
  2923. background-color: #f1f6ff;
  2924. border-radius: 8px;
  2925. overflow: hidden;
  2926. .uploaded-image {
  2927. width: 100%;
  2928. height: 100%;
  2929. object-fit: contain;
  2930. border-radius: 8px;
  2931. }
  2932. .image-overlay {
  2933. position: absolute;
  2934. top: 0;
  2935. left: 0;
  2936. width: 100%;
  2937. height: 100%;
  2938. background: rgba(0, 0, 0, 0.5);
  2939. display: flex;
  2940. align-items: center;
  2941. justify-content: center;
  2942. border-radius: 8px;
  2943. cursor: pointer;
  2944. .change-image-btn {
  2945. background: #3e7bfa;
  2946. color: white;
  2947. border: none;
  2948. border-radius: 8px;
  2949. padding: 8px 16px;
  2950. font-size: 14px;
  2951. font-weight: 600;
  2952. cursor: pointer;
  2953. transition: background-color 0.3s ease;
  2954. &:hover {
  2955. background: #3366e6;
  2956. }
  2957. }
  2958. }
  2959. }
  2960. .upload-content {
  2961. display: flex;
  2962. flex-direction: column;
  2963. align-items: center;
  2964. justify-content: center;
  2965. height: 100%;
  2966. padding: 20px;
  2967. .upload-icon {
  2968. width: 48px;
  2969. height: 48px;
  2970. margin-bottom: 12px;
  2971. }
  2972. .upload-text {
  2973. font-size: 16px;
  2974. font-weight: 600;
  2975. color: #2c3e50;
  2976. margin: 0 0 6px 0;
  2977. }
  2978. .upload-format {
  2979. font-size: 12px;
  2980. color: #6b7280;
  2981. margin: 0 0 50px 0;
  2982. }
  2983. .select-file-btn {
  2984. background: #e5eeff;
  2985. border: 1px solid #3e7bfa;
  2986. border-radius: 8px;
  2987. width: 120px;
  2988. height: 40px;
  2989. font-size: 14px;
  2990. color: #3e7bfa;
  2991. cursor: pointer;
  2992. transition: all 0.3s ease;
  2993. &:hover {
  2994. background: #e5e7eb;
  2995. border-color: #9ca3af;
  2996. }
  2997. }
  2998. }
  2999. }
  3000. .upload-status {
  3001. position: absolute;
  3002. top: 0;
  3003. left: 0;
  3004. width: 100%;
  3005. height: 100%;
  3006. background: rgba(255, 255, 255, 0.7);
  3007. display: flex;
  3008. flex-direction: column;
  3009. align-items: center;
  3010. justify-content: center;
  3011. border-radius: 12px;
  3012. z-index: 10;
  3013. backdrop-filter: blur(1px);
  3014. .loading-spinner {
  3015. border: 4px solid #f8f9fa;
  3016. border-top: 4px solid #3e7bfa;
  3017. border-radius: 50%;
  3018. width: 40px;
  3019. height: 40px;
  3020. animation: spin 1s linear infinite;
  3021. margin-bottom: 10px;
  3022. }
  3023. p {
  3024. font-size: 16px;
  3025. color: #4b5563;
  3026. font-weight: 500;
  3027. }
  3028. }
  3029. }
  3030. .action-section {
  3031. text-align: center;
  3032. // margin-top: -16px;
  3033. .start-identify-btn {
  3034. position: relative;
  3035. border: none;
  3036. background: none;
  3037. cursor: pointer;
  3038. padding: 0;
  3039. transition: opacity 0.3s ease;
  3040. &.btn-disabled {
  3041. opacity: 0.6;
  3042. cursor: not-allowed;
  3043. pointer-events: none;
  3044. }
  3045. .btn-bg {
  3046. width: 100%;
  3047. // max-width: 400px;
  3048. height: 48px;
  3049. object-fit: contain;
  3050. }
  3051. }
  3052. }
  3053. }
  3054. }
  3055. /* 右侧:使用流程 */
  3056. .right-section {
  3057. flex-shrink: 0;
  3058. display: flex;
  3059. flex-direction: column;
  3060. transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
  3061. &.slide-out {
  3062. transform: translateX(100%);
  3063. opacity: 0;
  3064. }
  3065. .process-card {
  3066. width: 250px;
  3067. background: white;
  3068. border-radius: 16px;
  3069. padding: 12px;
  3070. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  3071. height: 100%;
  3072. display: flex;
  3073. flex-direction: column;
  3074. min-height: max-content;
  3075. h3 {
  3076. font-size: 18px;
  3077. // font-weight: 600;
  3078. color: #1f2937;
  3079. // margin: 0 0 8px 0;
  3080. text-align: center;
  3081. }
  3082. .process-section {
  3083. flex: 1;
  3084. display: flex;
  3085. flex-direction: column;
  3086. .process-flow {
  3087. flex: 1;
  3088. display: flex;
  3089. flex-direction: column;
  3090. align-items: center;
  3091. justify-content: space-between;
  3092. padding: 20px 0;
  3093. .process-step {
  3094. display: flex;
  3095. flex-direction: column;
  3096. align-items: center;
  3097. width: 100%;
  3098. // margin-bottom: 20px;
  3099. .step-icon-wrapper {
  3100. position: relative;
  3101. width: 80px;
  3102. height: 80px;
  3103. margin-bottom: 12px;
  3104. display: flex;
  3105. align-items: center;
  3106. justify-content: center;
  3107. background: linear-gradient(
  3108. 135deg,
  3109. #ebf3ff 0%,
  3110. #f5f9ff 100%
  3111. );
  3112. border-radius: 50%;
  3113. box-shadow: 0 4px 12px rgba(62, 123, 250, 0.15);
  3114. .step-icon {
  3115. transition: transform 0.3s ease;
  3116. }
  3117. &:hover .step-icon {
  3118. transform: scale(1.1);
  3119. }
  3120. .step-number {
  3121. position: absolute;
  3122. top: 0;
  3123. right: 0;
  3124. width: 24px;
  3125. height: 24px;
  3126. background: linear-gradient(
  3127. 135deg,
  3128. rgba(51, 102, 230, 1),
  3129. rgba(51, 102, 230, 1)
  3130. );
  3131. border-radius: 50%;
  3132. display: flex;
  3133. align-items: center;
  3134. justify-content: center;
  3135. font-size: 14px;
  3136. font-weight: 600;
  3137. color: #ffffff;
  3138. }
  3139. }
  3140. .step-content {
  3141. text-align: center;
  3142. .step-title {
  3143. font-size: 16px;
  3144. font-weight: 600;
  3145. color: #1f2937;
  3146. margin-bottom: 4px;
  3147. }
  3148. .step-desc {
  3149. font-size: 14px;
  3150. color: #6b7280;
  3151. line-height: 1.4;
  3152. }
  3153. }
  3154. }
  3155. .step-connector {
  3156. width: 3px;
  3157. height: 60px;
  3158. background-color: #e4e4e4;
  3159. margin: 5px 0;
  3160. }
  3161. }
  3162. }
  3163. }
  3164. }
  3165. /* 固定布局,支持横向滚动 */
  3166. .chat-container {
  3167. min-width: 1000px; /* 设置最小宽度,确保内容完整显示 */
  3168. }
  3169. .work-content {
  3170. min-width: 1000px; /* 工作内容区域最小宽度 */
  3171. /* 隐藏垂直滚动条 */
  3172. &::-webkit-scrollbar {
  3173. display: none;
  3174. }
  3175. -ms-overflow-style: none;
  3176. scrollbar-width: none;
  3177. }
  3178. .detail-view {
  3179. min-width: 1000px; /* 详情页最小宽度 */
  3180. }
  3181. /* 详情页样式 */
  3182. .detail-view {
  3183. width: 100%;
  3184. max-width: 1528px;
  3185. height: calc(100vh - 88px); /* 固定高度,减去头部高度 */
  3186. background: white;
  3187. border-radius: 16px;
  3188. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  3189. display: flex;
  3190. flex-direction: column;
  3191. overflow: hidden;
  3192. animation: fadeInUp 0.6s ease-out;
  3193. opacity: 0;
  3194. animation-fill-mode: forwards;
  3195. .detail-header {
  3196. display: flex;
  3197. justify-content: space-between;
  3198. align-items: center;
  3199. padding: 16px 24px;
  3200. .header-left {
  3201. display: flex;
  3202. align-items: center;
  3203. gap: 8px;
  3204. .header-icon {
  3205. width: 48px;
  3206. height: 48px;
  3207. }
  3208. .header-icon-svg {
  3209. width: 20px;
  3210. height: 20px;
  3211. color: #1296db;
  3212. flex-shrink: 0;
  3213. }
  3214. .header-text {
  3215. display: flex;
  3216. align-items: center;
  3217. gap: 8px;
  3218. .main-title {
  3219. font-size: 15px;
  3220. font-weight: 500;
  3221. color: #ffffff;
  3222. background-color: #3b82f6;
  3223. padding: 4px 12px;
  3224. border-radius: 6px;
  3225. display: inline-flex;
  3226. align-items: center;
  3227. }
  3228. .sub-title-tag {
  3229. font-size: 11px;
  3230. padding: 2px 8px;
  3231. border-radius: 4px;
  3232. font-weight: 500;
  3233. flex-shrink: 0;
  3234. &.tag-tunnel {
  3235. background: rgba(62, 123, 250, 0.1);
  3236. color: #3366e6;
  3237. }
  3238. &.tag-bridge {
  3239. background: rgba(34, 184, 80, 0.1);
  3240. color: #22b850;
  3241. }
  3242. &.tag-equipment {
  3243. background: rgba(0, 128, 255, 0.1);
  3244. color: #0080ff;
  3245. }
  3246. &.tag-highway {
  3247. background: rgba(114, 46, 209, 0.1);
  3248. color: #722ed1;
  3249. }
  3250. &.tag-gas-station {
  3251. background: rgba(255, 77, 79, 0.1);
  3252. color: #ff4d4f;
  3253. }
  3254. }
  3255. }
  3256. }
  3257. .header-right {
  3258. display: flex;
  3259. align-items: center;
  3260. gap: 16px;
  3261. .back-btn {
  3262. display: flex;
  3263. align-items: center;
  3264. gap: 8px;
  3265. background: #e5eeff;
  3266. border: 1px solid #3e7bfa;
  3267. border-radius: 8px;
  3268. padding: 8px 16px;
  3269. font-size: 14px;
  3270. color: #3e7bfa;
  3271. cursor: pointer;
  3272. transition: all 0.3s ease;
  3273. &:hover {
  3274. background: #e5e7eb;
  3275. border-color: #9ca3af;
  3276. }
  3277. .back-icon {
  3278. width: 16px;
  3279. height: 16px;
  3280. }
  3281. }
  3282. .current-time {
  3283. font-size: 14px;
  3284. color: #6b7280;
  3285. }
  3286. }
  3287. }
  3288. .detail-content {
  3289. padding: 1rem;
  3290. max-width: 56rem;
  3291. width: 56rem;
  3292. margin: 0 auto;
  3293. position: relative;
  3294. display: flex;
  3295. flex-direction: column;
  3296. min-height: 0;
  3297. flex: 1;
  3298. overflow-y: auto;
  3299. scroll-behavior: smooth;
  3300. /* 隐藏滚动条 */
  3301. &::-webkit-scrollbar {
  3302. display: none;
  3303. }
  3304. -ms-overflow-style: none;
  3305. scrollbar-width: none;
  3306. .loading-overlay {
  3307. position: absolute;
  3308. top: 0;
  3309. left: 0;
  3310. width: 100%;
  3311. height: 100%;
  3312. background: rgba(255, 255, 255, 0.9);
  3313. display: flex;
  3314. flex-direction: column;
  3315. align-items: center;
  3316. justify-content: center;
  3317. z-index: 100;
  3318. .loading-spinner {
  3319. border: 4px solid #f3f3f3;
  3320. border-top: 4px solid #3e7bfa;
  3321. border-radius: 50%;
  3322. width: 40px;
  3323. height: 40px;
  3324. animation: spin 1s linear infinite;
  3325. margin-bottom: 16px;
  3326. }
  3327. p {
  3328. font-size: 16px;
  3329. color: #666;
  3330. margin: 0;
  3331. }
  3332. }
  3333. .image-section {
  3334. background-color: #dbeafe;
  3335. border-radius: 0.5rem;
  3336. padding: 1rem;
  3337. text-align: center;
  3338. margin-bottom: 1rem;
  3339. position: relative;
  3340. .evaluation-status {
  3341. position: absolute;
  3342. top: 12px;
  3343. right: 12px;
  3344. z-index: 10;
  3345. cursor: pointer;
  3346. .status-badge {
  3347. display: inline-block;
  3348. padding: 4px 12px;
  3349. border-radius: 12px;
  3350. font-size: 12px;
  3351. font-weight: 500;
  3352. // line-height: 1.2;
  3353. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  3354. &.evaluated {
  3355. background: #d1fae5;
  3356. color: #065f46;
  3357. border: 1px solid #a7f3d0;
  3358. cursor: default;
  3359. }
  3360. &.not-evaluated {
  3361. background: #fef3c7;
  3362. color: #92400e;
  3363. border: 1px solid #fde68a;
  3364. cursor: pointer;
  3365. }
  3366. }
  3367. }
  3368. .image-container {
  3369. position: relative;
  3370. width: 100%;
  3371. height: 350px;
  3372. border-radius: 0.5rem;
  3373. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  3374. border: 2px dashed #86efac;
  3375. overflow: hidden;
  3376. display: flex;
  3377. align-items: center;
  3378. justify-content: center;
  3379. background-color: #dbeafe;
  3380. }
  3381. .original-image,
  3382. .main-image {
  3383. width: 100%;
  3384. height: 100%;
  3385. display: block;
  3386. object-fit: contain;
  3387. transform: none !important;
  3388. image-orientation: from-image;
  3389. -webkit-image-orientation: from-image;
  3390. }
  3391. .main-image {
  3392. cursor: pointer;
  3393. }
  3394. .element-overlay-card {
  3395. position: absolute;
  3396. width: 260px;
  3397. max-height: 180px;
  3398. overflow-y: auto;
  3399. text-align: left;
  3400. padding: 14px 16px;
  3401. background: rgba(255, 255, 255, 0.96);
  3402. border: 1px solid rgba(239, 68, 68, 0.25);
  3403. border-radius: 14px;
  3404. box-shadow: 0 16px 32px rgba(15, 23, 42, 0.18);
  3405. backdrop-filter: blur(10px);
  3406. z-index: 6;
  3407. .element-card-title {
  3408. font-size: 14px;
  3409. font-weight: 600;
  3410. color: #b91c1c;
  3411. margin-bottom: 10px;
  3412. }
  3413. .element-card-list {
  3414. margin: 0;
  3415. padding-left: 18px;
  3416. color: #374151;
  3417. font-size: 13px;
  3418. line-height: 1.6;
  3419. }
  3420. .element-card-empty {
  3421. font-size: 13px;
  3422. color: #6b7280;
  3423. }
  3424. }
  3425. .scanning-overlay {
  3426. position: absolute;
  3427. top: 0;
  3428. left: 0;
  3429. width: 100%;
  3430. height: 100%;
  3431. pointer-events: none;
  3432. overflow: hidden;
  3433. border-radius: 0.5rem;
  3434. background: rgba(255, 255, 255, 0.3);
  3435. backdrop-filter: blur(2px);
  3436. .scanning-line {
  3437. position: absolute;
  3438. top: 0;
  3439. left: 0;
  3440. width: 100%;
  3441. height: 4px;
  3442. background: linear-gradient(
  3443. 90deg,
  3444. transparent 0%,
  3445. #3b82f6 20%,
  3446. #60a5fa 50%,
  3447. #3b82f6 80%,
  3448. transparent 100%
  3449. );
  3450. box-shadow: 0 0 15px rgba(59, 130, 246, 0.8),
  3451. 0 0 30px rgba(59, 130, 246, 0.5);
  3452. animation: scanning 1s ease-in-out 3; /* 扫描3次,每次1秒,共3秒 */
  3453. }
  3454. }
  3455. }
  3456. .analysis-section {
  3457. background-color: #f9fafb;
  3458. border-left: 0;
  3459. padding: 1rem;
  3460. border-radius: 0.5rem;
  3461. margin-bottom: 1rem;
  3462. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
  3463. .analysis-header {
  3464. display: flex;
  3465. align-items: center;
  3466. margin-bottom: 0;
  3467. .robot-avatar {
  3468. flex-shrink: 0;
  3469. margin-right: 0.75rem;
  3470. .robot-img {
  3471. width: 4rem;
  3472. height: 4rem;
  3473. object-fit: contain;
  3474. animation: robot-float 3s ease-in-out infinite;
  3475. }
  3476. }
  3477. }
  3478. .header-title {
  3479. flex: 1;
  3480. .analysis-title {
  3481. font-size: 1rem;
  3482. font-weight: 600;
  3483. color: #1f2937;
  3484. margin: 0;
  3485. line-height: 1.5;
  3486. }
  3487. .analysis-prompt {
  3488. display: flex;
  3489. align-items: center;
  3490. gap: 0.75rem;
  3491. color: #6b7280;
  3492. font-weight: 400;
  3493. font-size: 0.9375rem;
  3494. .typing-indicator {
  3495. display: inline-flex;
  3496. align-items: center;
  3497. gap: 4px;
  3498. .dot {
  3499. width: 6px;
  3500. height: 6px;
  3501. border-radius: 50%;
  3502. background-color: #3b82f6;
  3503. animation: typing-dot 1.4s infinite ease-in-out;
  3504. &:nth-child(1) {
  3505. animation-delay: 0s;
  3506. }
  3507. &:nth-child(2) {
  3508. animation-delay: 0.2s;
  3509. }
  3510. &:nth-child(3) {
  3511. animation-delay: 0.4s;
  3512. }
  3513. }
  3514. }
  3515. .prompt-text {
  3516. font-style: italic;
  3517. }
  3518. }
  3519. }
  3520. }
  3521. .analysis-body {
  3522. margin-left: 4.75rem;
  3523. .analysis-text {
  3524. font-size: 0.9375rem;
  3525. line-height: 1.8;
  3526. color: #374151;
  3527. margin-bottom: 1rem;
  3528. .scene-tag {
  3529. color: #3b82f6;
  3530. font-weight: 600;
  3531. }
  3532. .label-tag {
  3533. display: inline-block;
  3534. padding: 0.125rem 0.625rem;
  3535. margin: 0.125rem 0.25rem;
  3536. background-color: #eff6ff;
  3537. color: #3b82f6;
  3538. border: 1px solid #bfdbfe;
  3539. border-radius: 1rem;
  3540. font-weight: 500;
  3541. font-size: 0.8125rem;
  3542. }
  3543. .streaming-text {
  3544. border-right: 2px solid #3b82f6;
  3545. animation: blink 1s infinite;
  3546. }
  3547. }
  3548. .result-blockquote {
  3549. background-color: #ffffff;
  3550. border-left: 4px solid #3b82f6;
  3551. padding: 1rem;
  3552. margin: 0;
  3553. margin-bottom: 0.75rem;
  3554. border-radius: 0.5rem;
  3555. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
  3556. position: relative;
  3557. overflow: hidden;
  3558. .blockquote-label {
  3559. font-size: 0.875rem;
  3560. font-weight: 600;
  3561. color: #6b7280;
  3562. margin: 0 0 0.25rem 0;
  3563. }
  3564. .blockquote-content {
  3565. margin: 0;
  3566. color: #4b5563;
  3567. font-size: 0.9375rem;
  3568. line-height: 1.6;
  3569. .scene-highlight {
  3570. color: #3b82f6;
  3571. font-weight: 600;
  3572. }
  3573. .streaming-text {
  3574. border-right: 2px solid #3b82f6;
  3575. animation: blink 1s infinite;
  3576. }
  3577. }
  3578. }
  3579. .hazards-intro {
  3580. margin: 0;
  3581. color: #1f2937;
  3582. font-size: 0.9375rem;
  3583. font-weight: 400;
  3584. line-height: 1.6;
  3585. margin-bottom: 0.75rem;
  3586. }
  3587. .key-elements-section {
  3588. margin-bottom: 1rem;
  3589. padding: 0.875rem 1rem;
  3590. border-radius: 16px;
  3591. background: linear-gradient(135deg, #eef4ff 0%, #f8fbff 100%);
  3592. border: 1px solid #dbeafe;
  3593. .key-elements-title {
  3594. font-size: 0.875rem;
  3595. font-weight: 600;
  3596. color: #1e3a8a;
  3597. margin-bottom: 0.75rem;
  3598. }
  3599. .key-elements-buttons {
  3600. display: flex;
  3601. flex-wrap: wrap;
  3602. gap: 0.625rem;
  3603. }
  3604. .key-element-btn {
  3605. border: 1px solid #bfdbfe;
  3606. background: #ffffff;
  3607. color: #1d4ed8;
  3608. border-radius: 9999px;
  3609. padding: 0.45rem 0.9rem;
  3610. font-size: 0.875rem;
  3611. line-height: 1;
  3612. cursor: pointer;
  3613. transition: all 0.2s ease;
  3614. &:hover {
  3615. border-color: #60a5fa;
  3616. background: #eff6ff;
  3617. }
  3618. &.active {
  3619. background: #2563eb;
  3620. border-color: #2563eb;
  3621. color: #ffffff;
  3622. box-shadow: 0 10px 24px rgba(37, 99, 235, 0.22);
  3623. }
  3624. }
  3625. .key-elements-hint {
  3626. margin-top: 0.75rem;
  3627. font-size: 0.8125rem;
  3628. color: #64748b;
  3629. }
  3630. }
  3631. /* 场景隐患列表样式 - 移到analysis-section内部 */
  3632. .hazards-section {
  3633. margin-top: 0;
  3634. .hazards-content {
  3635. position: relative;
  3636. &.scanning-mode {
  3637. min-height: 200px;
  3638. }
  3639. .hazard-cards-container {
  3640. display: flex;
  3641. flex-direction: column;
  3642. gap: 0.75rem;
  3643. margin-top: 0.75rem;
  3644. }
  3645. .hazard-empty {
  3646. padding: 1rem 1.25rem;
  3647. margin-top: 0.75rem;
  3648. background: #ffffff;
  3649. border: 1px dashed #cbd5e1;
  3650. border-radius: 16px;
  3651. font-size: 0.875rem;
  3652. color: #64748b;
  3653. text-align: center;
  3654. }
  3655. .hazard-card {
  3656. background-color: #ffffff;
  3657. border-radius: 9999px;
  3658. border: 1px solid #f3f4f6;
  3659. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  3660. padding: 0.75rem;
  3661. display: flex;
  3662. align-items: flex-start;
  3663. cursor: pointer;
  3664. opacity: 0;
  3665. transform: translateX(-20px);
  3666. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  3667. position: relative;
  3668. overflow: hidden;
  3669. &.show {
  3670. opacity: 1;
  3671. transform: translateX(0);
  3672. }
  3673. &:hover {
  3674. transform: translateX(8px) scale(1.02) !important;
  3675. box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.3),
  3676. 0 8px 10px -6px rgba(59, 130, 246, 0.2);
  3677. border-color: rgba(59, 130, 246, 0.5);
  3678. }
  3679. .hazard-number {
  3680. width: 24px;
  3681. height: 24px;
  3682. background-color: #3b82f6;
  3683. border-radius: 50%;
  3684. display: flex;
  3685. align-items: center;
  3686. justify-content: center;
  3687. font-size: 14px;
  3688. font-weight: 500;
  3689. color: #ffffff;
  3690. flex-shrink: 0;
  3691. margin-right: 0.75rem;
  3692. margin-top: 0.125rem;
  3693. }
  3694. .hazard-text-container {
  3695. display: flex;
  3696. justify-content: space-between;
  3697. align-items: center;
  3698. flex-grow: 1;
  3699. .hazard-desc {
  3700. flex: 1;
  3701. font-size: 0.9375rem;
  3702. color: #1f2937;
  3703. margin: 0;
  3704. line-height: 1.5;
  3705. }
  3706. .example-link {
  3707. color: #3b82f6;
  3708. font-size: 0.875rem;
  3709. font-weight: 400;
  3710. padding: 0.25rem 0.75rem;
  3711. background-color: #eff6ff;
  3712. border-radius: 9999px;
  3713. text-decoration: none;
  3714. transition: all 0.2s ease;
  3715. flex-shrink: 0;
  3716. margin-left: 0.75rem;
  3717. &:hover {
  3718. background-color: #dbeafe;
  3719. }
  3720. }
  3721. }
  3722. }
  3723. /* 隐患区域加载遮罩层样式 */
  3724. .hazards-loading-overlay {
  3725. position: absolute;
  3726. top: 0;
  3727. left: 0;
  3728. width: 100%;
  3729. height: 100%;
  3730. background: rgba(249, 250, 251, 0.9);
  3731. display: flex;
  3732. flex-direction: column;
  3733. align-items: center;
  3734. justify-content: center;
  3735. z-index: 10;
  3736. border-radius: 8px;
  3737. backdrop-filter: blur(2px);
  3738. .loading-spinner {
  3739. border: 4px solid #f3f3f3;
  3740. border-top: 4px solid #3e7bfa;
  3741. border-radius: 50%;
  3742. width: 40px;
  3743. height: 40px;
  3744. animation: spin 1s linear infinite;
  3745. margin-bottom: 16px;
  3746. }
  3747. p {
  3748. font-size: 16px;
  3749. color: #6b7280;
  3750. margin: 0;
  3751. font-weight: 500;
  3752. }
  3753. }
  3754. }
  3755. }
  3756. }
  3757. }
  3758. /* 新的示例对比弹窗样式 */
  3759. .modal-backdrop {
  3760. position: fixed;
  3761. top: 0;
  3762. left: 0;
  3763. right: 0;
  3764. bottom: 0;
  3765. background: rgba(0, 0, 0, 0.6);
  3766. display: flex;
  3767. align-items: center;
  3768. justify-content: center;
  3769. z-index: 9999;
  3770. backdrop-filter: blur(4px);
  3771. }
  3772. .example-comparison-modal {
  3773. width: 90vw;
  3774. max-width: 1200px;
  3775. max-height: 80vh;
  3776. background: white;
  3777. border-radius: 16px;
  3778. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  3779. overflow: hidden;
  3780. animation: modalSlideIn 0.3s ease-out;
  3781. display: flex;
  3782. flex-direction: column;
  3783. }
  3784. .modal-header-section {
  3785. padding: 24px 32px;
  3786. border-bottom: 1px solid #e5e7eb;
  3787. display: flex;
  3788. align-items: center;
  3789. justify-content: space-between;
  3790. background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
  3791. }
  3792. .modal-title-area h3 {
  3793. margin: 0 0 8px 0;
  3794. font-size: 20px;
  3795. font-weight: 600;
  3796. color: #1f2937;
  3797. }
  3798. .hazard-info {
  3799. display: flex;
  3800. align-items: center;
  3801. gap: 12px;
  3802. }
  3803. .hazard-number {
  3804. width: 24px;
  3805. height: 24px;
  3806. background: #3b82f6;
  3807. color: white;
  3808. border-radius: 50%;
  3809. display: flex;
  3810. align-items: center;
  3811. justify-content: center;
  3812. font-size: 12px;
  3813. font-weight: 600;
  3814. }
  3815. .hazard-text {
  3816. color: #6b7280;
  3817. font-size: 16px;
  3818. font-weight: 500;
  3819. }
  3820. .close-button {
  3821. background: none;
  3822. border: none;
  3823. padding: 8px;
  3824. border-radius: 8px;
  3825. cursor: pointer;
  3826. color: #6b7280;
  3827. transition: all 0.2s ease;
  3828. }
  3829. .close-button:hover {
  3830. background: #f3f4f6;
  3831. color: #374151;
  3832. }
  3833. .modal-content-section {
  3834. flex: 1;
  3835. padding: 32px;
  3836. overflow-y: auto;
  3837. }
  3838. .loading-state {
  3839. display: flex;
  3840. flex-direction: column;
  3841. align-items: center;
  3842. justify-content: center;
  3843. min-height: 300px;
  3844. gap: 16px;
  3845. }
  3846. .spinner {
  3847. width: 40px;
  3848. height: 40px;
  3849. border: 4px solid #f3f4f6;
  3850. border-top: 4px solid #3b82f6;
  3851. border-radius: 50%;
  3852. animation: spin 1s linear infinite;
  3853. }
  3854. .comparison-container {
  3855. display: grid;
  3856. grid-template-columns: 1fr 1fr;
  3857. gap: 32px;
  3858. min-height: 400px;
  3859. }
  3860. .example-card {
  3861. background: #f9fafb;
  3862. border-radius: 12px;
  3863. overflow: hidden;
  3864. border: 2px solid transparent;
  3865. transition: all 0.3s ease;
  3866. }
  3867. .example-card:hover {
  3868. transform: translateY(-4px);
  3869. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
  3870. }
  3871. .correct-example {
  3872. border-color: #10b981;
  3873. }
  3874. .error-example {
  3875. border-color: #ef4444;
  3876. }
  3877. .card-header {
  3878. padding: 16px 20px;
  3879. background: white;
  3880. border-bottom: 1px solid #e5e7eb;
  3881. }
  3882. .status-badge {
  3883. display: inline-flex;
  3884. align-items: center;
  3885. gap: 8px;
  3886. padding: 6px 12px;
  3887. border-radius: 20px;
  3888. font-size: 14px;
  3889. font-weight: 500;
  3890. }
  3891. .status-badge.correct {
  3892. background: #d1fae5;
  3893. color: #065f46;
  3894. }
  3895. .status-badge.error {
  3896. background: #fee2e2;
  3897. color: #991b1b;
  3898. }
  3899. .card-content {
  3900. padding: 20px;
  3901. display: flex;
  3902. align-items: center;
  3903. justify-content: center;
  3904. min-height: 300px;
  3905. }
  3906. .example-image {
  3907. max-width: 100%;
  3908. max-height: 300px;
  3909. object-fit: contain;
  3910. border-radius: 8px;
  3911. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  3912. }
  3913. .clickable-image {
  3914. cursor: pointer;
  3915. transition: all 0.3s ease;
  3916. }
  3917. .clickable-image:hover {
  3918. transform: scale(1.02);
  3919. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  3920. }
  3921. .image-loading {
  3922. display: flex;
  3923. flex-direction: column;
  3924. align-items: center;
  3925. gap: 12px;
  3926. color: #6b7280;
  3927. }
  3928. .no-image {
  3929. display: flex;
  3930. flex-direction: column;
  3931. align-items: center;
  3932. gap: 16px;
  3933. color: #9ca3af;
  3934. text-align: center;
  3935. }
  3936. .no-image svg {
  3937. opacity: 0.5;
  3938. }
  3939. .no-image p {
  3940. margin: 0;
  3941. font-size: 14px;
  3942. }
  3943. /* 弹窗动画 */
  3944. @keyframes modalSlideIn {
  3945. from {
  3946. opacity: 0;
  3947. transform: scale(0.9) translateY(-20px);
  3948. }
  3949. to {
  3950. opacity: 1;
  3951. transform: scale(1) translateY(0);
  3952. }
  3953. }
  3954. @keyframes imageZoomIn {
  3955. from {
  3956. opacity: 0;
  3957. transform: scale(0.8);
  3958. }
  3959. to {
  3960. opacity: 1;
  3961. transform: scale(1);
  3962. }
  3963. }
  3964. /* 响应式设计 */
  3965. @media (max-width: 768px) {
  3966. .comparison-container {
  3967. grid-template-columns: 1fr;
  3968. gap: 20px;
  3969. }
  3970. .modal-content-section {
  3971. padding: 20px;
  3972. }
  3973. .modal-header-section {
  3974. padding: 16px 20px;
  3975. }
  3976. .example-comparison-modal {
  3977. width: 95vw;
  3978. max-height: 90vh;
  3979. }
  3980. }
  3981. .modal-header {
  3982. display: flex;
  3983. align-items: center;
  3984. justify-content: space-between;
  3985. padding: 24px 52px 0px 76px;
  3986. .modal-title {
  3987. font-size: 22px;
  3988. font-weight: 600;
  3989. color: #1f2937;
  3990. }
  3991. .close-icon {
  3992. width: 24px;
  3993. height: 24px;
  3994. cursor: pointer;
  3995. transition: opacity 0.3s ease;
  3996. &:hover {
  3997. opacity: 0.7;
  3998. }
  3999. }
  4000. }
  4001. .modal-hazard-info {
  4002. display: flex;
  4003. align-items: center;
  4004. justify-content: center;
  4005. gap: 12px;
  4006. .hazard-number {
  4007. width: 24px;
  4008. height: 24px;
  4009. background: #3e7bfa;
  4010. border-radius: 50%;
  4011. display: flex;
  4012. align-items: center;
  4013. justify-content: center;
  4014. font-size: 14px;
  4015. font-weight: 600;
  4016. color: #ffffff;
  4017. flex-shrink: 0;
  4018. }
  4019. .hazard-description {
  4020. font-size: 16px;
  4021. color: #374151;
  4022. line-height: 1.5;
  4023. }
  4024. }
  4025. .modal-body {
  4026. display: flex;
  4027. gap: 24px;
  4028. padding: 32px 81px;
  4029. height: calc(100% - 120px);
  4030. position: relative;
  4031. .loading-overlay {
  4032. position: absolute;
  4033. top: 0;
  4034. left: 0;
  4035. width: 100%;
  4036. height: 100%;
  4037. background: rgba(255, 255, 255, 0.9);
  4038. display: flex;
  4039. flex-direction: column;
  4040. align-items: center;
  4041. justify-content: center;
  4042. z-index: 100;
  4043. .loading-spinner {
  4044. border: 4px solid #f3f3f3;
  4045. border-top: 4px solid #3e7bfa;
  4046. border-radius: 50%;
  4047. width: 40px;
  4048. height: 40px;
  4049. animation: spin 1s linear infinite;
  4050. margin-bottom: 16px;
  4051. }
  4052. p {
  4053. font-size: 16px;
  4054. color: #666;
  4055. margin: 0;
  4056. }
  4057. }
  4058. .example-panel {
  4059. flex: 1;
  4060. // background: #f9fafb;
  4061. border-radius: 12px;
  4062. // padding: 24px;
  4063. display: flex;
  4064. flex-direction: column;
  4065. .panel-image {
  4066. flex: 1;
  4067. display: flex;
  4068. align-items: center;
  4069. justify-content: center;
  4070. position: relative;
  4071. max-height: 480px;
  4072. overflow: hidden;
  4073. img {
  4074. max-width: 100%;
  4075. max-height: 100%;
  4076. width: 100%;
  4077. height: 100%;
  4078. object-fit: contain;
  4079. border-radius: 8px;
  4080. display: block;
  4081. margin: 0 auto;
  4082. // 横图充满容器
  4083. &[data-orientation="landscape"] {
  4084. object-fit: cover;
  4085. }
  4086. // 竖图保持完整显示
  4087. &[data-orientation="portrait"] {
  4088. object-fit: contain;
  4089. }
  4090. }
  4091. .no-image-placeholder {
  4092. width: 100%;
  4093. height: 100%;
  4094. display: flex;
  4095. align-items: center;
  4096. justify-content: center;
  4097. background-color: #f5f5f5;
  4098. border: 2px dashed #d1d5db;
  4099. border-radius: 8px;
  4100. .placeholder-text {
  4101. color: #6b7280;
  4102. font-size: 16px;
  4103. font-weight: 500;
  4104. text-align: center;
  4105. }
  4106. }
  4107. .image-loading {
  4108. width: 100%;
  4109. height: 100%;
  4110. display: flex;
  4111. flex-direction: column;
  4112. align-items: center;
  4113. justify-content: center;
  4114. background-color: #f9fafb;
  4115. border: 2px dashed #d1d5db;
  4116. border-radius: 8px;
  4117. .loading-spinner {
  4118. border: 3px solid #f3f3f3;
  4119. border-top: 3px solid #3e7bfa;
  4120. border-radius: 50%;
  4121. width: 32px;
  4122. height: 32px;
  4123. animation: spin 1s linear infinite;
  4124. margin-bottom: 12px;
  4125. }
  4126. p {
  4127. color: #6b7280;
  4128. font-size: 14px;
  4129. font-weight: 500;
  4130. margin: 0;
  4131. text-align: center;
  4132. }
  4133. }
  4134. .image-label {
  4135. position: absolute;
  4136. top: 0px;
  4137. left: 0px;
  4138. z-index: 10;
  4139. .label-icon {
  4140. width: 95px;
  4141. height: 30px;
  4142. }
  4143. }
  4144. }
  4145. &.correct-panel {
  4146. .panel-image > img:first-child {
  4147. border: 1px solid #34d399;
  4148. border-radius: 8px;
  4149. }
  4150. }
  4151. &.error-panel {
  4152. .panel-image > img:first-child {
  4153. border: 1px solid #ef4444;
  4154. border-radius: 8px;
  4155. }
  4156. }
  4157. }
  4158. }
  4159. .empty-history {
  4160. display: flex;
  4161. flex-direction: column;
  4162. align-items: center;
  4163. justify-content: center;
  4164. margin-top: 236px;
  4165. .empty-icon {
  4166. width: 147px;
  4167. height: 148px;
  4168. object-fit: contain;
  4169. margin-bottom: 16px;
  4170. }
  4171. .empty-text {
  4172. font-size: 16px;
  4173. color: #9c9fa7;
  4174. text-align: center;
  4175. }
  4176. }
  4177. }
  4178. @keyframes spin {
  4179. 0% {
  4180. transform: rotate(0deg);
  4181. }
  4182. 100% {
  4183. transform: rotate(360deg);
  4184. }
  4185. }
  4186. /* Toast提示组件样式 */
  4187. .toast {
  4188. position: fixed;
  4189. top: 20px;
  4190. right: 20px;
  4191. z-index: 9999;
  4192. animation: slideIn 0.3s ease-out;
  4193. }
  4194. .toast-content {
  4195. display: flex;
  4196. align-items: center;
  4197. gap: 12px;
  4198. padding: 16px 20px;
  4199. border-radius: 8px;
  4200. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  4201. min-width: 300px;
  4202. }
  4203. .toast-success {
  4204. background: #f0f9ff;
  4205. border: 1px solid #0ea5e9;
  4206. .toast-icon img {
  4207. width: 20px;
  4208. height: 20px;
  4209. }
  4210. .toast-message {
  4211. color: #0c4a6e;
  4212. font-weight: 500;
  4213. }
  4214. }
  4215. .toast-error {
  4216. background: #fef2f2;
  4217. border: 1px solid #ef4444;
  4218. .toast-icon img {
  4219. width: 20px;
  4220. height: 20px;
  4221. }
  4222. .toast-message {
  4223. color: #991b1b;
  4224. font-weight: 500;
  4225. }
  4226. }
  4227. @keyframes slideIn {
  4228. from {
  4229. transform: translateX(100%);
  4230. opacity: 0;
  4231. }
  4232. to {
  4233. transform: translateX(0);
  4234. opacity: 1;
  4235. }
  4236. }
  4237. /* 图片预览弹窗样式 */
  4238. .image-preview-overlay {
  4239. position: fixed !important;
  4240. top: 0 !important;
  4241. left: 0 !important;
  4242. right: 0 !important;
  4243. bottom: 0 !important;
  4244. width: 100vw !important;
  4245. height: 100vh !important;
  4246. background: rgba(0, 0, 0, 0.8) !important;
  4247. display: flex !important;
  4248. align-items: center !important;
  4249. justify-content: center !important;
  4250. z-index: 9999 !important;
  4251. cursor: pointer !important;
  4252. margin: 0 !important;
  4253. padding: 0 !important;
  4254. box-sizing: border-box !important;
  4255. }
  4256. .preview-image {
  4257. max-width: 90%;
  4258. max-height: 90%;
  4259. object-fit: contain;
  4260. border-radius: 8px;
  4261. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  4262. transform: none !important;
  4263. image-orientation: from-image;
  4264. -webkit-image-orientation: from-image;
  4265. }
  4266. /* 历史记录加载状态样式 */
  4267. .history-loading {
  4268. display: flex;
  4269. flex-direction: column;
  4270. align-items: center;
  4271. justify-content: center;
  4272. padding: 40px 20px;
  4273. min-height: 200px;
  4274. }
  4275. .history-loading .loading-spinner {
  4276. width: 32px;
  4277. height: 32px;
  4278. border: 3px solid #f3f3f3;
  4279. border-top: 3px solid #409eff;
  4280. border-radius: 50%;
  4281. animation: spin 1s linear infinite;
  4282. margin: 0 auto 16px auto;
  4283. }
  4284. .history-loading .loading-text {
  4285. color: #6b7280;
  4286. font-size: 14px;
  4287. margin: 0;
  4288. font-weight: 400;
  4289. }
  4290. @keyframes spin {
  4291. 0% {
  4292. transform: rotate(0deg);
  4293. }
  4294. 100% {
  4295. transform: rotate(360deg);
  4296. }
  4297. }
  4298. @keyframes scanning {
  4299. 0% {
  4300. top: 0;
  4301. opacity: 0;
  4302. }
  4303. 10% {
  4304. opacity: 1;
  4305. }
  4306. 90% {
  4307. opacity: 1;
  4308. }
  4309. 100% {
  4310. top: 100%;
  4311. opacity: 0;
  4312. }
  4313. }
  4314. /* 机器人浮动动画 */
  4315. @keyframes robot-float {
  4316. 0%,
  4317. 100% {
  4318. transform: translateY(0px) rotate(0deg);
  4319. }
  4320. 50% {
  4321. transform: translateY(-5px) rotate(2deg);
  4322. }
  4323. }
  4324. /* 眼睛眨眼动画 */
  4325. @keyframes blink {
  4326. 0%,
  4327. 90%,
  4328. 100% {
  4329. opacity: 1;
  4330. }
  4331. 95% {
  4332. opacity: 0.2;
  4333. }
  4334. }
  4335. /* 天线波动动画 */
  4336. @keyframes antenna-wave {
  4337. 0%,
  4338. 100% {
  4339. transform: rotate(0deg);
  4340. }
  4341. 25% {
  4342. transform: rotate(-8deg);
  4343. }
  4344. 75% {
  4345. transform: rotate(8deg);
  4346. }
  4347. }
  4348. /* 信号脉冲动画 */
  4349. @keyframes signal-pulse {
  4350. 0%,
  4351. 100% {
  4352. opacity: 1;
  4353. transform: scale(1);
  4354. }
  4355. 50% {
  4356. opacity: 0.4;
  4357. transform: scale(1.3);
  4358. }
  4359. }
  4360. /* 微笑动画 */
  4361. @keyframes smile {
  4362. 0%,
  4363. 90%,
  4364. 100% {
  4365. d: path("M 35 60 Q 50 70 65 60");
  4366. }
  4367. 95% {
  4368. d: path("M 35 58 Q 50 68 65 58");
  4369. }
  4370. }
  4371. /* 心跳动画 */
  4372. @keyframes heartbeat {
  4373. 0%,
  4374. 100% {
  4375. transform: scale(1);
  4376. }
  4377. 10%,
  4378. 30% {
  4379. transform: scale(1.15);
  4380. }
  4381. 20%,
  4382. 40% {
  4383. transform: scale(1);
  4384. }
  4385. }
  4386. /* 打字指示器动画 */
  4387. @keyframes typing-dot {
  4388. 0%,
  4389. 60%,
  4390. 100% {
  4391. transform: translateY(0);
  4392. opacity: 0.7;
  4393. }
  4394. 30% {
  4395. transform: translateY(-10px);
  4396. opacity: 1;
  4397. }
  4398. }
  4399. /* 淡入上升动画 */
  4400. @keyframes fadeInUp {
  4401. from {
  4402. opacity: 0;
  4403. transform: translateY(30px);
  4404. }
  4405. to {
  4406. opacity: 1;
  4407. transform: translateY(0);
  4408. }
  4409. }
  4410. @keyframes loadingDots {
  4411. 0%,
  4412. 80%,
  4413. 100% {
  4414. transform: scale(0);
  4415. opacity: 0.5;
  4416. }
  4417. 40% {
  4418. transform: scale(1);
  4419. opacity: 1;
  4420. }
  4421. }
  4422. /* 点评弹窗样式 */
  4423. .evaluation-modal-overlay {
  4424. position: fixed !important;
  4425. top: 0 !important;
  4426. left: 0 !important;
  4427. right: 0 !important;
  4428. bottom: 0 !important;
  4429. width: 100vw !important;
  4430. height: 100vh !important;
  4431. background: rgba(0, 0, 0, 0.5) !important;
  4432. display: flex !important;
  4433. align-items: center !important;
  4434. justify-content: center !important;
  4435. z-index: 9999 !important;
  4436. margin: 0 !important;
  4437. padding: 0 !important;
  4438. box-sizing: border-box !important;
  4439. }
  4440. .evaluation-modal {
  4441. width: 442px;
  4442. background: linear-gradient(135deg, rgb(239, 246, 255) 0%, #ffffff 100%);
  4443. border-radius: 16px;
  4444. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
  4445. overflow: hidden;
  4446. }
  4447. .modal-header {
  4448. display: flex;
  4449. align-items: center;
  4450. justify-content: space-between;
  4451. padding: 24px 24px 0 24px;
  4452. .modal-title {
  4453. font-size: 20px;
  4454. font-weight: 600;
  4455. color: #1f2937;
  4456. }
  4457. .close-icon {
  4458. width: 24px;
  4459. height: 24px;
  4460. cursor: pointer;
  4461. transition: opacity 0.3s ease;
  4462. &:hover {
  4463. opacity: 0.7;
  4464. }
  4465. }
  4466. }
  4467. .modal-body {
  4468. padding: 24px;
  4469. .question-section {
  4470. margin-bottom: 32px;
  4471. &:last-child {
  4472. margin-bottom: 0;
  4473. }
  4474. .question-title {
  4475. font-size: 16px;
  4476. font-weight: 500;
  4477. color: #374151;
  4478. margin-bottom: 16px;
  4479. }
  4480. .answer-buttons {
  4481. display: flex;
  4482. gap: 12px;
  4483. .answer-btn {
  4484. flex: 1;
  4485. padding: 12px 24px;
  4486. border: 2px solid #d1d5db;
  4487. border-radius: 8px;
  4488. background: white;
  4489. color: #6b7280;
  4490. font-size: 14px;
  4491. font-weight: 500;
  4492. cursor: pointer;
  4493. transition: all 0.3s ease;
  4494. &:hover {
  4495. border-color: #3e7bfa;
  4496. color: #3e7bfa;
  4497. }
  4498. &.active {
  4499. border-color: #3e7bfa;
  4500. background: #3e7bfa;
  4501. color: white;
  4502. }
  4503. &.disabled {
  4504. cursor: not-allowed;
  4505. }
  4506. }
  4507. }
  4508. .star-rating {
  4509. display: flex;
  4510. gap: 8px;
  4511. .star {
  4512. font-size: 32px;
  4513. color: #d1d5db;
  4514. cursor: pointer;
  4515. transition: color 0.2s ease;
  4516. &:hover {
  4517. color: #fbbf24;
  4518. }
  4519. &.active {
  4520. color: #fbbf24;
  4521. }
  4522. &.disabled {
  4523. cursor: not-allowed;
  4524. }
  4525. }
  4526. }
  4527. .remark-input-container {
  4528. position: relative;
  4529. .remark-textarea {
  4530. width: 100%;
  4531. min-height: 100px;
  4532. padding: 12px;
  4533. border: 2px solid #d1d5db;
  4534. border-radius: 8px;
  4535. font-size: 14px;
  4536. font-family: inherit;
  4537. resize: vertical;
  4538. transition: border-color 0.3s ease;
  4539. &:focus {
  4540. outline: none;
  4541. border-color: #3e7bfa;
  4542. }
  4543. &:disabled {
  4544. background-color: #f9fafb;
  4545. color: #6b7280;
  4546. cursor: not-allowed;
  4547. }
  4548. &::placeholder {
  4549. color: #9ca3af;
  4550. }
  4551. }
  4552. .character-count {
  4553. position: absolute;
  4554. bottom: 8px;
  4555. right: 12px;
  4556. font-size: 12px;
  4557. color: #6b7280;
  4558. span {
  4559. &.over-limit {
  4560. color: #ef4444;
  4561. font-weight: 500;
  4562. }
  4563. }
  4564. }
  4565. }
  4566. }
  4567. }
  4568. .modal-footer {
  4569. padding: 0 24px 24px 24px;
  4570. text-align: center;
  4571. .submit-btn {
  4572. background: none;
  4573. border: none;
  4574. cursor: pointer;
  4575. padding: 0;
  4576. .submit-icon {
  4577. width: 120px;
  4578. height: 48px;
  4579. object-fit: contain;
  4580. }
  4581. }
  4582. }
  4583. /* 全局弹窗样式 - 确保 teleport 弹窗正确显示 */
  4584. body > .example-modal-overlay,
  4585. body > .image-preview-overlay,
  4586. body > .evaluation-modal-overlay {
  4587. position: fixed !important;
  4588. top: 0 !important;
  4589. left: 0 !important;
  4590. right: 0 !important;
  4591. bottom: 0 !important;
  4592. width: 100vw !important;
  4593. height: 100vh !important;
  4594. z-index: 9999 !important;
  4595. display: flex !important;
  4596. align-items: center !important;
  4597. justify-content: center !important;
  4598. margin: 0 !important;
  4599. padding: 0 !important;
  4600. box-sizing: border-box !important;
  4601. }
  4602. </style>