| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955 |
- <template>
- <div class="mobile-hazard-detection">
- <!-- 移动端隐患提示页面 -->
- <MobileHeader title="隐患提示" @back="goBack" @menu="showHistoryDrawer" />
-
- <div class="mobile-content">
-
- <!-- 通用历史记录抽屉 -->
- <MobileHistoryDrawer
- :visible="!isIdentifying && showHistory"
- title="历史记录"
- :historyData="historyData"
- :loading="isLoadingHistory"
- @close="showHistory = false"
- @createNewTask="createNewTask"
- @handleHistoryItem="handleHistoryItem"
- @deleteHistoryItem="deleteHistoryItem"
- />
- <!-- 主界面:隐患提示系统 -->
- <div v-if="currentView === 'main'" class="main-layout">
- <!-- 使用流程选项卡 -->
- <div class="process-tabs">
- <div class="tab-item" :class="{ active: activeMode === 'detect' }" @click="activeMode = 'detect'">
- <span>智能识别</span>
- </div>
- <div class="tab-item" :class="{ active: activeMode === 'process' }" @click="activeMode = 'process'">
- <span>使用流程</span>
- </div>
- </div>
- <!-- 智能识别内容 -->
- <div v-if="activeMode === 'detect'" class="hazard-system">
- <div class="system-header">
- <h3>智能隐患提示系统</h3>
- <p>基于AI技术的工程安全智能隐患提示系统,实时检测分析,提供专业评估和预防建议</p>
- </div>
- <!-- 步骤一:选择场景 -->
- <div class="step-section">
- <h4>步骤一:选择场景</h4>
- <p class="step-description">请先选择您要识别的工程场景</p>
- <div class="scenario-tags">
- <div
- v-for="(scenario, key) in scenarios"
- :key="key"
- :class="['scenario-tag', {
- active: selectedScenario === key,
- disabled: key !== 'gas_station' && key !== 'simple_supported_bridge' && key !== 'tunnel' && key !== 'special_equipment' && key !== 'operate_highway',
- 'identifying-disabled': isIdentifying,
- 'compact': key === 'operate_highway'
- }]"
- @click="!isIdentifying && (key === 'gas_station' || key === 'simple_supported_bridge' || key === 'tunnel' || key === 'special_equipment' || key === 'operate_highway') ? selectScenario(key) : null"
- >
- {{ scenario.name }}
- </div>
- </div>
- </div>
- <!-- 步骤二:上传图片 -->
- <div class="step-section">
- <h4>步骤二:上传需要识别的场景图片</h4>
- <div
- class="upload-area"
- @click="triggerFileUpload"
- :class="{ 'drag-over': isDragOver }"
- >
- <!-- 显示上传的图片 -->
- <div v-if="uploadedImageUrl" class="uploaded-image-container">
- <img :src="uploadedImageUrl" alt="已上传的图片" class="uploaded-image" />
- <div class="image-overlay">
- <button class="change-image-btn" @click.stop="reselectImage">更换图片</button>
- </div>
- </div>
-
- <!-- 显示上传区域 -->
- <div v-else class="upload-content">
- <img src="@/assets/Hazard/5.png" alt="上传图标" class="upload-icon" />
- <p class="upload-text">点击上传图片</p>
- <p class="upload-format">支持JPG、PNG格式</p>
- <button class="select-file-btn" @click.stop="triggerFileUpload">选择图片文件</button>
- </div>
-
- <!-- 上传状态指示器 -->
- <div v-if="isUploading" class="upload-status">
- <div class="loading-spinner"></div>
- <p>正在上传...</p>
- </div>
-
- <!-- 识别状态指示器 -->
- <div v-if="isIdentifying" class="upload-status">
- <div class="loading-spinner"></div>
- <p>正在识别隐患...</p>
- </div>
-
- <input
- ref="fileInput"
- type="file"
- accept="image/*"
- @change="handleFileUpload"
- style="display: none"
- />
- </div>
- </div>
- <!-- 开始识别按钮 -->
- <div class="action-section">
- <button class="start-identify-btn" @click="startIdentification" :disabled="isIdentifying" :class="{ 'btn-disabled': isIdentifying }">
- <img :src="uploadedImageUrl ? startIdentifyActiveImg : startIdentifyImg" alt="开始识别" class="btn-bg" />
- </button>
- </div>
- </div>
- <!-- 使用流程内容 -->
- <div v-if="activeMode === 'process'" class="process-content">
- <div class="process-header">
- <h3>使用流程</h3>
- <p>了解如何使用智能隐患识别系统</p>
- </div>
-
- <!-- 流程概述 -->
- <div class="process-steps">
- <div class="step-item">
- <div class="step-number">1</div>
- <div class="step-text">
- <h4>选择场景</h4>
- <p>从支持的五种工程场景中选择您要检测的场景类型</p>
- </div>
- </div>
-
- <div class="step-item">
- <div class="step-number">2</div>
- <div class="step-text">
- <h4>上传图片</h4>
- <p>上传您要识别的场景图片,支持JPG、PNG格式</p>
- </div>
- </div>
-
- <div class="step-item">
- <div class="step-number">3</div>
- <div class="step-text">
- <h4>开始识别</h4>
- <p>点击开始识别按钮,系统将自动检测场景中的隐患</p>
- </div>
- </div>
-
- <div class="step-item">
- <div class="step-number">4</div>
- <div class="step-text">
- <h4>查看结果</h4>
- <p>识别完成后可查看详细的分析结果和隐患列表</p>
- </div>
- </div>
- </div>
-
- <!-- 流程示意图 -->
- <!-- <div class="process-image-section">
- <img src="@/assets/Hazard/4.png" alt="使用流程" class="process-image" />
- </div> -->
- </div>
- </div>
- <!-- 详情页:隐患提示结果 -->
- <div v-if="currentView === 'detail'" class="detail-layout">
- <!-- 顶部标题栏 -->
- <div class="detail-header">
- <div class="header-left">
- <img src="@/assets/Hazard/6.png" alt="顶部图标" class="header-icon" />
- <div class="header-text">
- <span class="main-title">{{ detectionResult?.scene_name ? scenarios[detectionResult.scene_name]?.name : '隐患提示结果' }}</span>
- <span :class="['sub-title-tag', getTagClass(detectionResult?.scene_name)]">{{ detectionResult?.scene_name ? scenarios[detectionResult.scene_name]?.name : '未知场景' }}</span>
- </div>
- </div>
- <div class="header-right">
- <div class="current-time">{{ selectedHistoryItem?.time || getCurrentTime() }}</div>
- </div>
- </div>
- <!-- 主要内容区域 -->
- <div class="detail-content">
- <!-- 加载状态 -->
- <div v-if="isLoadingDetail || isImageLoading" class="loading-overlay">
- <div class="loading-spinner"></div>
- <p>{{ isLoadingDetail ? '正在加载详情...' : '正在加载图片...' }}</p>
- </div>
-
- <!-- 图片显示区域 -->
- <div class="image-section">
- <!-- 点评状态 -->
- <div class="evaluation-status" @click="openEvaluationModal">
- <span :class="['status-badge', selectedHistoryItem?.effect_evaluation > 0 ? 'evaluated' : 'not-evaluated']">
- {{ selectedHistoryItem?.effect_evaluation > 0 ? '已点评' : '未点评' }}
- </span>
- </div>
-
- <div class="image-container">
- <!-- 用户上传的原图 -->
- <img
- v-if="showScanningEffect"
- :src="uploadedImageUrl"
- alt="用户上传图片"
- class="original-image"
- />
-
- <!-- 扫描效果 -->
- <div v-if="showScanningEffect" class="scanning-overlay">
- <div class="scanning-line"></div>
- </div>
-
- <!-- 识别结果图片 -->
- <img
- v-if="!showScanningEffect"
- :src="annotatedImageUrl"
- alt="隐患提示图片"
- class="main-image"
- @click="openImagePreview"
- style="cursor: pointer; transform: none !important;"
- @error="handleMainImageError"
- />
- </div>
- </div>
- <!-- 识别结果分析 -->
- <div class="analysis-section">
- <div class="analysis-header">
- <img src="@/assets/Hazard/7.png" alt="警告标志" class="warning-icon" />
- <h3>识别结果分析</h3>
- </div>
- <div class="analysis-content">
- <p class="scene-info">
- <!-- 扫描期间显示分析提示 -->
- <span v-if="showAnalysisPrompt" class="analysis-prompt">
- 蜀道安全管理AI智能助手正在为您分析图片,请稍候……
- </span>
-
- <!-- 扫描完成后显示分析结果 -->
- <template v-else>
- <span v-if="isStreamingAnalysis" class="streaming-analysis-text">{{ streamingAnalysis }}</span>
- <span v-else-if="!isStreamingAnalysis && detectionResult">
- 经过智能分析,发现场景:<span :class="['scene-tags', getTagClass(detectionResult?.scene_name)]">[{{ detectionResult?.scene_name ? scenarios[detectionResult.scene_name]?.name : '未知场景' }}]</span>
- <span class="detection-count">[{{ detectionResult.labels }}]</span>
- </span>
- </template>
- </p>
- </div>
- </div>
- <!-- 场景隐患列表 -->
- <div class="hazards-section">
- <div class="hazards-header">
- <h3>该场景常见隐患有:</h3>
- </div>
- <div class="hazards-content" :class="{ 'scanning-mode': showScanningEffect }">
- <!-- 扫描期间的遮罩层 -->
- <div v-if="showScanningEffect" class="hazards-loading-overlay">
- <div class="loading-spinner"></div>
- <p>正在分析场景隐患...</p>
- </div>
-
- <!-- 隐患内容 -->
- <div v-else class="hazard-item">
- <div
- v-for="(hazard, index) in detectionResult?.third_scenes || []"
- :key="index"
- class="hazard-line"
- >
- <span class="hazard-number">{{ index + 1 }}</span>
- <span class="hazard-desc">{{ hazard }}</span>
- <button class="example-btn" @click="openExampleModal({number: index + 1, description: hazard})">示例</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 图片预览弹窗 -->
- <div v-if="showImagePreview" class="image-preview-overlay" @click="closeImagePreview">
- <img :src="annotatedImageUrl" alt="预览图片" class="preview-image" />
- </div>
- <!-- 示例弹窗 -->
- <div v-if="showExampleModal" class="example-modal-overlay" @click="closeExampleModal">
- <div class="example-modal" @click.stop>
- <div class="modal-header">
- <span class="modal-title">示例详情</span>
- <img src="@/assets/Hazard/11.png" alt="关闭" class="close-icon" @click="closeExampleModal" />
- </div>
- <div class="modal-hazard-info">
- <div class="hazard-number">{{ selectedHazard?.number }}</div>
- <span class="hazard-description">{{ selectedHazard?.description }}</span>
- </div>
- <div class="modal-body">
- <!-- 加载状态 -->
- <div v-if="isLoadingExample" class="loading-overlay">
- <div class="loading-spinner"></div>
- <p>正在加载示例图...</p>
- </div>
-
- <!-- 示例图内容 -->
- <template v-else>
- <div class="example-images">
- <div class="example-panel correct-panel">
- <div class="panel-image">
- <!-- 正确示例图加载状态 -->
- <div v-if="imageLoadingStates.correct" class="image-loading">
- <div class="loading-spinner"></div>
- <p>正在加载图片...</p>
- </div>
- <!-- 有正确示例图时显示图片 -->
- <img
- v-if="exampleImages.correctImageUrl"
- :src="exampleImages.correctImageUrl"
- alt="正确示例图片"
- :style="{ display: imageLoadingStates.correct ? 'none' : 'block' }"
- @error="handleImageError($event, 'correct')"
- @load="handleImageLoad($event, 'correct')"
- />
- <!-- 没有正确示例图时显示提示文字 -->
- <div v-if="!exampleImages.correctImageUrl && !imageLoadingStates.correct" class="no-image-placeholder">
- <div class="placeholder-text">暂无示例图</div>
- </div>
- <div class="image-label correct-label">
- <img src="@/assets/Hazard/9.png" alt="正确" class="label-icon" />
- </div>
- </div>
- </div>
-
- <div class="example-panel error-panel">
- <div class="panel-image">
- <!-- 错误示例图加载状态 -->
- <div v-if="imageLoadingStates.error" class="image-loading">
- <div class="loading-spinner"></div>
- <p>正在加载图片...</p>
- </div>
- <!-- 有错误示例图时显示图片 -->
- <img
- v-if="exampleImages.errorImageUrl"
- :src="exampleImages.errorImageUrl"
- alt="错误示例图片"
- :style="{ display: imageLoadingStates.error ? 'none' : 'block' }"
- @error="handleImageError($event, 'error')"
- @load="handleImageLoad($event, 'error')"
- />
- <!-- 没有错误示例图时显示提示文字 -->
- <div v-if="!exampleImages.errorImageUrl && !imageLoadingStates.error" class="no-image-placeholder">
- <div class="placeholder-text">暂无示例图</div>
- </div>
- <div class="image-label error-label">
- <img src="@/assets/Hazard/10.png" alt="错误" class="label-icon" />
- </div>
- </div>
- </div>
- </div>
- </template>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 点评弹窗 -->
- <div v-if="showEvaluationModal" class="evaluation-modal-overlay" @click="closeEvaluationModal">
- <div class="evaluation-modal" @click.stop>
- <div class="modal-header">
- <span class="modal-title">点评确认</span>
- <img src="@/assets/Hazard/11.png" alt="关闭" class="close-icon" @click="closeEvaluationModal" />
- </div>
-
- <div class="modal-body">
- <!-- 问题1:场景是否匹配 -->
- <div class="question-section">
- <div class="question-title">1.场景是否匹配?</div>
- <div class="answer-buttons">
- <button
- :class="['answer-btn', { active: evaluationData.sceneMatch === true, disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- @click="selectedHistoryItem?.effect_evaluation > 0 ? null : (evaluationData.sceneMatch = true)"
- >
- 是
- </button>
- <button
- :class="['answer-btn', { active: evaluationData.sceneMatch === false, disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- @click="selectedHistoryItem?.effect_evaluation > 0 ? null : (evaluationData.sceneMatch = false)"
- >
- 否
- </button>
- </div>
- </div>
-
- <!-- 问题2:提示是否准确 -->
- <div class="question-section">
- <div class="question-title">2.提示是否准确?</div>
- <div class="answer-buttons">
- <button
- :class="['answer-btn', { active: evaluationData.promptAccurate === true, disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- @click="selectedHistoryItem?.effect_evaluation > 0 ? null : (evaluationData.promptAccurate = true)"
- >
- 是
- </button>
- <button
- :class="['answer-btn', { active: evaluationData.promptAccurate === false, disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- @click="selectedHistoryItem?.effect_evaluation > 0 ? null : (evaluationData.promptAccurate = false)"
- >
- 否
- </button>
- </div>
- </div>
-
- <!-- 问题3:效果评价 -->
- <div class="question-section">
- <div class="question-title">3.效果评价</div>
- <div class="star-rating">
- <span
- v-for="star in 5"
- :key="star"
- :class="['star', { active: star <= evaluationData.rating, disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- @click="selectedHistoryItem?.effect_evaluation > 0 ? null : (evaluationData.rating = star)"
- >
- ★
- </span>
- </div>
- </div>
-
- <!-- 问题4:用户意见 -->
- <div v-if="shouldShowRemarkSection" class="question-section">
- <div class="question-title">4.您的意见</div>
- <div class="remark-input-container">
- <textarea
- v-model="evaluationData.userRemark"
- :disabled="selectedHistoryItem?.effect_evaluation > 0"
- :class="['remark-textarea', { disabled: selectedHistoryItem?.effect_evaluation > 0 }]"
- placeholder="请输入您的意见或建议..."
- maxlength="200"
- rows="4"
- ></textarea>
- <div class="char-count">
- <span :class="{ 'over-limit': evaluationData.userRemark.length > 200 }">
- {{ evaluationData.userRemark.length }}/200
- </span>
- </div>
- </div>
- </div>
- </div>
-
- <div class="modal-footer" v-if="!selectedHistoryItem?.effect_evaluation || selectedHistoryItem.effect_evaluation === 0">
- <button class="submit-btn" @click="submitEvaluation">
- <img src="@/assets/Hazard/13.png" alt="提交反馈" class="submit-icon" />
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 移动端Toast提示组件 -->
- <MobileToast :visible="toastVisible" :message="toastMessage" @close="closeToast" />
- </div>
- </template>
- <script setup>
- import { useRouter } from 'vue-router'
- import MobileHeader from '@/components/MobileHeader.vue'
- import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
- import DeleteConfirmModal from '@/components/DeleteConfirmModal.vue'
- import { ref, onMounted, watch, computed } from 'vue'
- import { apis } from '@/request/apis.js'
- // ===== 已删除:getUserId - 不再需要,改用token =====
- // import { getUserId } from '@/utils/userManager.js'
- import MobileToast from '@/components/MobileToast.vue'
- import startIdentifyImg from "@/assets/Hazard/2.png"
- import startIdentifyActiveImg from "@/assets/Hazard/3.png"
- const router = useRouter()
- const goBack = () => {
- router.go(-1)
- }
- // 显示历史记录抽屉的方法
- const showHistoryDrawer = () => {
- if (!isIdentifying.value) {
- showHistory.value = true
- }
- // AI处理中时不执行任何操作,不记录点击意图
- }
- // Toast状态管理
- const toastVisible = ref(false)
- const toastMessage = ref('')
- // Toast便捷方法
- const showToast = (message, duration = 2000) => {
- toastMessage.value = message
- toastVisible.value = true
-
- if (duration > 0) {
- setTimeout(() => {
- toastVisible.value = false
- }, duration)
- }
- }
- const closeToast = () => {
- toastVisible.value = false
- }
- // 替换ElMessage的工具方法
- const showSuccess = (message) => showToast(message, 2000)
- const showError = (message) => showToast(message, 3000)
- const showWarning = (message) => showToast(message, 2500)
- const showHistory = ref(false)
- // 选项卡状态
- const activeMode = ref("detect") // 'detect' 或 'process'
- // 隐患识别相关状态
- const selectedScenario = ref("tunnel"); // 默认选择隧道工程
- const uploadedImage = ref(null);
- const uploadedImageUrl = ref(""); // 存储上传后的图片URL
- const fileInput = ref(null);
- const currentView = ref("main"); // 当前视图:main-主界面,detail-详情页
- const selectedHistoryItem = ref(null); // 选中的历史记录项
- const isUploading = ref(false); // 修改:上传状态标识
- const isDragOver = ref(false); // 修改:拖拽上传区域是否悬停
- const isIdentifying = ref(false); // 修改:识别状态标识
- const detectionResult = ref(null); // 存储识别结果
- const annotatedImageUrl = ref(""); // 存储标注后的图片URL
- // 图片预览相关数据
- const showImagePreview = ref(false); // 控制图片预览弹窗显示
- const showScanningEffect = ref(false); // 控制扫描效果显示
- const isLoadingLabels = ref(false); // 控制标签加载状态
- const isStreamingLabels = ref(false); // 控制标签流式输出状态
- const streamingLabels = ref(''); // 流式输出的标签内容
- const isStreamingAnalysis = ref(false); // 控制整个分析文本流式输出状态
- const streamingAnalysis = ref(''); // 流式输出的完整分析文本
- const showAnalysisPrompt = ref(false); // 控制分析提示显示
- // 历史记录相关状态
- const historyData = ref([])
- const historyTotal = ref(0)
- const isLoadingHistory = ref(false)
- const isLoadingDetail = ref(false); // 控制详情加载状态
- const isImageLoading = ref(false); // 控制图片加载状态
- // 示例弹窗相关数据
- const showExampleModal = ref(false); // 控制示例弹窗显示
- const selectedHazard = ref(null); // 选中的隐患项目
- const exampleImages = ref({}); // 存储示例图数据
- const isLoadingExample = ref(false); // 控制示例图加载状态
- const imageLoadingStates = ref({
- correct: false, // 正确示例图加载状态
- error: false // 错误示例图加载状态
- }); // 控制每张图片的加载状态
- // 点评弹窗相关数据
- const showEvaluationModal = ref(false); // 控制点评弹窗显示
- const evaluationData = ref({
- sceneMatch: null, // 场景是否匹配
- promptAccurate: null, // 提示是否准确
- rating: 0, // 效果评价评分 1-5
- userRemark: '' // 用户意见
- });
- // 删除相关状态
- const showDeleteModal = ref(false); // 控制是否显示删除确认弹窗
- const deleteTargetItem = ref(null); // 要删除的目标项
- // 时间解析与格式化(容错更强)
- const parseToDate = (input) => {
- if (!input) return null
- if (typeof input === 'number') {
- const ms = input < 1e12 ? input * 1000 : input
- return new Date(ms)
- }
- if (typeof input === 'string') {
- let d = new Date(input)
- if (!isNaN(d)) return d
- const normalized = input.replace(/-/g, '/').replace('T', ' ')
- d = new Date(normalized)
- if (!isNaN(d)) return d
- }
- return new Date(input)
- }
- const formatTime = (timestamp) => {
- const date = parseToDate(timestamp)
- if (!date || isNaN(date)) return ''
- const now = new Date()
- const isToday = date.toDateString() === now.toDateString()
- const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
- const isYesterday = date.toDateString() === yesterday.toDateString()
- if (isToday) {
- return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- if (isYesterday) {
- return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- const month = date.getMonth() + 1
- const day = date.getDate()
- const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- return `${month}月${day}日 ${time}`
- }
- // 生成对话标题
- const generateConversationTitle = (content) => {
- if (!content) return '新对话'
- // 取前30个字符作为标题
- const title = content.replace(/<[^>]*>/g, '').trim()
- return title.length > 30 ? title.substring(0, 30) + '...' : title
- }
- // 获取历史记录列表
- const getHistoryRecordList = async () => {
- try {
- console.log('📋 开始获取移动端隐患提示历史记录列表...')
- isLoadingHistory.value = true
- const startTime = performance.now()
-
- const response = await apis.getHazardHistory({
- // ===== 已删除:user_id - 后端从token解析 =====
- })
-
- const endTime = performance.now()
- console.log(`📋 移动端隐患提示历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
- console.log('📋 移动端历史记录列表响应:', response)
-
- if (response.statusCode === 200 || response.code === 200) {
- // 设置历史记录总数
- historyTotal.value = response.total || 0
-
- // 转换后端数据为前端格式
- historyData.value = response.data.map(record => ({
- id: record.id,
- title: record.title || '隐患提示记录',
- time: formatTime(record.created_at),
- businessType: 'hazard',
- isActive: false,
- effect_evaluation: record.effect_evaluation || 0,
- // 保存原始数据用于后续查询
- rawData: record
- }))
- // 高亮当前记录
- if (selectedHistoryItem.value?.id) {
- historyData.value.forEach(item => { item.isActive = item.id === selectedHistoryItem.value.id })
- }
- console.log(`✅ 移动端隐患提示历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
- } else {
- console.error('❌ 获取移动端历史记录列表失败:', response.statusCode)
- }
- } catch (error) {
- console.error('❌ 获取移动端历史记录列表失败:', error)
- } finally {
- isLoadingHistory.value = false
- }
- }
- // 新建任务
- // 配置常量
- const scenarios = {
- tunnel: { name: "隧道工程", color: "#3366E6" },
- simple_supported_bridge: { name: "桥梁工程", color: "#22B850" },
- "gas_station": { name: "加油站", color: "#FF4D4F" },
- special_equipment: { name: "特种设备", color: "#0080FF" },
- operate_highway: { name: "运营高速公路", color: "#722ED1" },
- };
- // 标签类型配置,便于后端动态管理
- const tagTypeConfig = {
- tunnel: {
- class: "tag-tunnel",
- background: "rgba(62, 123, 250, 0.1)",
- color: "#3366E6",
- text: "隧道",
- },
- simple_supported_bridge: {
- class: "tag-bridge",
- background: "rgba(34, 184, 80, 0.1)",
- color: "#22B850",
- text: "桥梁",
- },
- special_equipment: {
- class: "tag-equipment",
- background: "rgba(0, 128, 255, 0.1)",
- color: "#0080FF",
- text: "特种设备",
- },
- operate_highway: {
- class: "tag-highway",
- background: "rgba(114, 46, 209, 0.1)",
- color: "#722ED1",
- text: "运营高速公路",
- },
- "gas_station": {
- class: "tag-gas-station",
- background: "rgba(255, 77, 79, 0.1)",
- color: "#FF4D4F",
- text: "加油站",
- },
- };
- // 根据标签类型获取样式类名
- const getTagClass = (tagType) => {
- return tagTypeConfig[tagType]?.class || "tag-tunnel";
- };
- // 根据标签类型获取显示文字
- const getTagText = (tagType) => {
- return tagTypeConfig[tagType]?.text || "隧道";
- };
- // 删除确认消息
- const deleteConfirmMessage = computed(() => {
- const title = deleteTargetItem.value?.item?.title || ''
- return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`
- })
- // 控制是否显示备注区域
- const shouldShowRemarkSection = computed(() => {
- // 如果是已点评状态,只有当有备注内容时才显示
- if (selectedHistoryItem.value?.effect_evaluation > 0) {
- return evaluationData.value.userRemark && evaluationData.value.userRemark.trim() !== ''
- }
- // 如果是未点评状态,始终显示(让用户可以输入)
- return true
- })
- const createNewTask = () => {
- console.log("createNewChat 被调用");
-
- // 重置所有状态
- currentView.value = "main";
- selectedScenario.value = "tunnel"; // 默认选择隧道工程
- uploadedImage.value = null;
- uploadedImageUrl.value = "";
- selectedHistoryItem.value = null;
- showExampleModal.value = false;
- selectedHazard.value = null;
- isUploading.value = false;
- isDragOver.value = false; // 重置拖拽状态
- isIdentifying.value = false; // 重置识别状态
- annotatedImageUrl.value = "";
- exampleImages.value = {}; // 清空示例图数据
- isLoadingExample.value = false; // 重置示例图加载状态
- imageLoadingStates.value = { correct: false, error: false }; // 重置图片加载状态
- isImageLoading.value = false; // 重置图片加载状态
- showScanningEffect.value = false; // 重置扫描效果状态
- isLoadingLabels.value = false; // 重置标签加载状态
- isStreamingLabels.value = false; // 重置标签流式输出状态
- streamingLabels.value = ''; // 清空流式输出内容
- isStreamingAnalysis.value = false; // 重置分析文本流式输出状态
- streamingAnalysis.value = ''; // 清空分析文本流式输出内容
- showAnalysisPrompt.value = false; // 重置分析提示状态
- // 清空文件输入
- if (fileInput.value) {
- fileInput.value.value = "";
- console.log("文件输入已清空");
- }
-
- // 重置所有历史记录的active状态
- if (historyData.value.length > 0) {
- historyData.value.forEach((item) => {
- item.isActive = false;
- });
- }
-
- showHistory.value = false
- console.log("新任务创建完成");
- };
- // 处理历史记录点击
- const handleHistoryItem = async (historyItem) => {
- if (historyItem.isActive) return
-
- console.log("点击移动端隐患提示历史记录:", historyItem)
-
- // 关闭历史记录抽屉
- showHistory.value = false
-
- // 加载历史记录详情
- await handleHistoryDetail(historyItem)
- }
- // 选择场景
- const selectScenario = (scenarioKey) => {
- try {
- console.log("selectScenario 被调用,场景:", scenarioKey);
- selectedScenario.value = scenarioKey;
- console.log("选择场景:", scenarios[scenarioKey].name);
- } catch (error) {
- console.error("选择场景失败:", error);
- }
- };
- // 触发文件上传
- const triggerFileUpload = () => {
- try {
- console.log("triggerFileUpload 被调用");
- if (fileInput.value) {
- fileInput.value.click();
- console.log("已触发文件选择器");
- } else {
- console.error("fileInput 引用为空");
- }
- } catch (error) {
- console.error("触发文件上传失败:", error);
- }
- };
- // 上传文件到服务器
- const uploadFileToServer = async (file) => {
- try {
- console.log("uploadFileToServer 被调用,文件:", file);
- isUploading.value = true;
-
- // 创建FormData对象
- const formData = new FormData();
- formData.append('image', file);
- console.log("FormData 已创建:", formData);
-
- // 调用后端上传接口
- console.log("开始调用后端API...");
- const response = await apis.uploadImage(formData);
- console.log("后端API响应:", response);
-
- if (response.statusCode === 200) {
- uploadedImageUrl.value = response.fileUrl || response.fileURL;
- console.log("上传成功:", uploadedImageUrl.value);
- showSuccess("图片上传成功!");
- } else {
- throw new Error(response.message || "上传失败");
- }
- } catch (error) {
- console.error("上传失败:", error);
- showError("图片上传失败: " + (error.message || "未知错误"));
- uploadedImage.value = null;
- uploadedImageUrl.value = "";
- } finally {
- isUploading.value = false;
- }
- };
- // 重新选择图片
- const reselectImage = () => {
- try {
- console.log("reselectImage 被调用");
- clearUploadedImage();
- triggerFileUpload();
- console.log("重新选择图片完成");
- } catch (error) {
- console.error("重新选择图片失败:", error);
- }
- };
- // 清除上传的图片
- const clearUploadedImage = () => {
- try {
- console.log("clearUploadedImage 被调用");
- uploadedImage.value = null;
- uploadedImageUrl.value = "";
- if (fileInput.value) {
- fileInput.value.value = "";
- console.log("文件输入已清空");
- }
- console.log("上传图片已清除");
- } catch (error) {
- console.error("清除上传图片失败:", error);
- }
- };
- // 处理图片方向,确保保持原始方向
- const processImageOrientation = (file) => {
- return new Promise((resolve) => {
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- const img = new Image();
-
- img.onload = () => {
- // 设置画布尺寸为图片的原始尺寸
- canvas.width = img.naturalWidth;
- canvas.height = img.naturalHeight;
-
- // 绘制图片,保持原始方向
- ctx.drawImage(img, 0, 0);
-
- // 将画布转换为Blob
- canvas.toBlob((blob) => {
- // 创建一个新的File对象,保持原始文件名和类型
- const processedFile = new File([blob], file.name, {
- type: file.type,
- lastModified: file.lastModified
- });
- resolve(processedFile);
- }, file.type);
- };
-
- img.src = URL.createObjectURL(file);
- });
- };
- // 图片压缩函数
- const compressImage = (file, maxWidth = 1920, quality = 0.8) => {
- return new Promise((resolve, reject) => {
- const maxSize = 5 * 1024 * 1024; // 5MB limit
-
- // 如果文件本身小于5MB,直接返回
- if (file.size <= maxSize) {
- console.log('文件大小符合要求,无需压缩:', file.size);
- resolve(file);
- return;
- }
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- const img = new Image();
-
- img.onload = () => {
- try {
- let currentWidth = img.width;
- let currentHeight = img.height;
- let currentQuality = quality;
-
- // 初始尺寸调整
- if (currentWidth > maxWidth) {
- currentHeight = (currentHeight * maxWidth) / currentWidth;
- currentWidth = maxWidth;
- }
-
- canvas.width = currentWidth;
- canvas.height = currentHeight;
- ctx.drawImage(img, 0, 0, currentWidth, currentHeight);
-
- // 递归压缩函数
- const attemptCompression = (q, w, h) => {
- canvas.width = w;
- canvas.height = h;
- ctx.drawImage(img, 0, 0, w, h);
-
- canvas.toBlob((blob) => {
- if (!blob) {
- reject(new Error('图片压缩失败: Blob生成失败'));
- return;
- }
-
- console.log(`尝试压缩: 质量=${q.toFixed(2)}, 尺寸=${w}x${h}, 大小=${(blob.size / 1024 / 1024).toFixed(2)}MB`);
-
- if (blob.size <= maxSize) {
- // 压缩成功
- const compressedFile = new File([blob], file.name, {
- type: 'image/jpeg',
- lastModified: Date.now()
- });
- console.log(`最终压缩结果: ${file.size} -> ${compressedFile.size} bytes`);
- resolve(compressedFile);
- } else {
- // 仍然过大,继续压缩
- if (q > 0.2) {
- // 优先降低质量
- attemptCompression(q - 0.1, w, h);
- } else if (w > 800) {
- // 质量已很低,开始缩小尺寸
- attemptCompression(0.8, w * 0.8, h * 0.8);
- } else {
- // 无法继续压缩,强制返回当前结果(虽然可能仍略大于5MB,但已尽力)
- console.warn('无法进一步压缩,返回当前结果');
- const compressedFile = new File([blob], file.name, {
- type: 'image/jpeg',
- lastModified: Date.now()
- });
- resolve(compressedFile);
- }
- }
- }, 'image/jpeg', q);
- };
-
- // 开始第一次压缩尝试
- attemptCompression(currentQuality, currentWidth, currentHeight);
-
- } catch (error) {
- reject(error);
- }
- };
-
- img.onerror = () => reject(new Error('图片加载失败'));
- img.src = URL.createObjectURL(file);
- });
- };
- // 通用的文件处理函数
- const processFile = async (file) => {
- try {
- console.log("processFile 被调用,文件:", file);
-
- // 检查文件格式
- const allowedTypes = ["image/jpeg", "image/jpg", "image/png"];
- console.log("文件类型:", file.type);
- if (!allowedTypes.includes(file.type)) {
- console.log("不支持的文件类型:", file.type);
- showError("只支持JPG、PNG格式的图片");
- return;
- }
-
- let processedFile = file;
-
- // 如果文件大于5MB,进行压缩
- if (file.size > 5 * 1024 * 1024) {
- console.log("文件过大,开始压缩:", file.size);
- showToast("图片较大,正在压缩...", 2000);
-
- try {
- processedFile = await compressImage(file, 1920, 0.8);
- console.log("压缩完成:", processedFile.size);
- } catch (error) {
- console.error("压缩失败:", error);
- showError("图片压缩失败,请选择较小的图片");
- return;
- }
- }
-
- // 处理图片方向,确保保持原始方向
- const orientedFile = await processImageOrientation(processedFile);
- uploadedImage.value = orientedFile;
- console.log("选择文件:", orientedFile.name);
-
- // 自动上传文件到后端
- console.log("开始上传文件到服务器");
- await uploadFileToServer(orientedFile);
- } catch (error) {
- console.error("处理文件失败:", error);
- }
- };
- // 处理文件上传
- const handleFileUpload = async (event) => {
- try {
- console.log("handleFileUpload 被调用", event);
- const file = event.target.files[0];
- console.log("选择的文件:", file);
- if (file) {
- await processFile(file);
- } else {
- console.log("没有选择文件");
- }
- } catch (error) {
- console.error("文件上传处理失败:", error);
- }
- };
- // 开始识别
- const startIdentification = async () => {
- try {
- console.log("startIdentification 被调用");
-
- // 防抖检查:如果正在识别中,直接返回
- if (isIdentifying.value) {
- console.log("识别正在进行中,忽略重复点击");
- showWarning("识别正在进行中,请勿重复点击");
- return;
- }
-
- if (!selectedScenario.value) {
- console.log("未选择场景");
- showWarning("请先选择场景");
- return;
- }
-
- if (!uploadedImageUrl.value) {
- console.log("未上传图片");
- showWarning("请先上传图片");
- return;
- }
-
- // 检查用户最新识别记录是否已点评
- try {
- console.log("检查最新识别记录是否已点评");
- const latestRecordResponse = await apis.getLatestRecognitionRecord({
- // ===== 已删除:user_id - 后端从token解析 =====
- });
-
- if (latestRecordResponse.statusCode === 200 && latestRecordResponse.data) {
- const latestRecord = latestRecordResponse.data;
- console.log("最新识别记录:", latestRecord);
-
- // 如果最新记录存在且未点评,提示用户
- if (latestRecord.effect_evaluation === 0 || !latestRecord.effect_evaluation) {
- showWarning("请先对上一次识别结果进行点评,再进行新的识别");
- return;
- }
- }
- } catch (error) {
- console.error("检查最新识别记录失败:", error);
- // 如果检查失败,继续执行识别流程
- }
-
- console.log("开始识别:", {
- scenario: scenarios[selectedScenario.value].name,
- image: uploadedImageUrl.value,
- });
-
- // 开始识别状态
- isIdentifying.value = true;
-
- // 调用后端API进行隐患提示
- showSuccess("开始进行隐患提示,请稍候...");
-
- // 模拟用户信息(暂时不从后端获取)
- const account = '';
- const username = '蜀道用户';
-
- // 获取当日日期,格式:2025/10/23
- const today = new Date();
- const year = today.getFullYear();
- const month = String(today.getMonth() + 1).padStart(2, '0');
- const day = String(today.getDate()).padStart(2, '0');
- const currentDate = `${year}/${month}/${day}`;
-
- // 截取手机号后四位
- const accountLastFour = account.length >= 4 ? account.slice(-4) : account;
-
- const requestData = {
- // ===== 已删除:user_id - 后端从token解析 =====
- scene_name: selectedScenario.value,
- image: uploadedImageUrl.value,
- account: accountLastFour,
- username: username,
- date: currentDate
- };
-
- console.log("发送隐患提示请求:", requestData);
- const response = await apis.hazardDetection(requestData);
- console.log("隐患提示响应:", response);
-
- // 检查响应结构,兼容不同的字段名
- const isSuccess = response.code === 200 || response.statusCode === 200;
-
- if (isSuccess) {
- // showSuccess("隐患提示完成!");
-
- // 保存识别结果
- detectionResult.value = response.data;
-
- // 处理标注后的图片
- if (response.data.annotated_image) {
- annotatedImageUrl.value = `${response.data.annotated_image}`;
- }
-
- // 跳转到详情页
- currentView.value = "detail";
-
- // 开始扫描效果
- showScanningEffect.value = true;
- showAnalysisPrompt.value = true; // 显示分析提示
-
- // 延迟6秒后显示识别结果(扫描3圈,每圈2秒)
- setTimeout(() => {
- showScanningEffect.value = false;
- showAnalysisPrompt.value = false; // 隐藏分析提示
- // 开始整个分析文本流式输出效果
- startAnalysisStreaming();
- }, 6000);
-
- // 刷新历史记录
- await getHistoryRecordList();
-
- // 自动选中最新创建的历史记录
- if (historyData.value.length > 0) {
- const latestRecord = historyData.value[0]; // 假设最新的记录在数组第一位
- selectedHistoryItem.value = latestRecord;
-
- // 更新所有记录的active状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === latestRecord.id;
- });
-
- console.log("自动选中最新记录:", latestRecord);
- }
-
- console.log("识别结果:", response.data);
- console.log("标注图片URL:", annotatedImageUrl.value);
- } else {
- showError(response.msg || "隐患提示失败");
- }
- } catch (error) {
- console.error("开始识别失败:", error);
- showError("隐患提示失败: " + (error.msg || "未知错误"));
- } finally {
- // 清除识别状态
- isIdentifying.value = false;
- }
- };
- // 开始整个分析文本流式输出效果
- const startAnalysisStreaming = () => {
- try {
- console.log("开始整个分析文本流式输出效果");
-
- // 重置状态
- isStreamingAnalysis.value = false;
- streamingAnalysis.value = '';
-
- // 立即开始流式输出,不需要延迟
- // 构建完整的分析文本
- const sceneName = detectionResult.value?.scene_name;
- const sceneText = sceneName ? scenarios[sceneName]?.name : '未知场景';
- const labels = detectionResult.value?.labels || '';
- const labelsText = Array.isArray(labels) ? labels.join('') : labels;
-
- const fullAnalysisText = `经过智能分析,发现场景:[${sceneText}][${labelsText}]`;
-
- // 开始流式输出
- isStreamingAnalysis.value = true;
- let currentIndex = 0;
- const streamInterval = setInterval(() => {
- if (currentIndex < fullAnalysisText.length) {
- streamingAnalysis.value = fullAnalysisText.substring(0, currentIndex + 1);
- currentIndex++;
- } else {
- // 输出完成
- clearInterval(streamInterval);
- isStreamingAnalysis.value = false;
- console.log("分析文本流式输出完成");
- }
- }, 100); // 每100ms输出一个字符
-
- } catch (error) {
- console.error("开始分析文本流式输出失败:", error);
- // 出错时直接显示完整内容
- isStreamingAnalysis.value = false;
- }
- };
- // 处理历史记录点击详情
- const handleHistoryDetail = async (historyItem) => {
- try {
- console.log("handleHistoryDetail 被调用,历史记录:", historyItem);
-
- selectedHistoryItem.value = historyItem;
- currentView.value = "detail";
- isLoadingDetail.value = true;
- isImageLoading.value = true;
- // 调用详情接口获取完整数据
- console.log("开始获取记录详情,ID:", historyItem.id);
- const detailResponse = await apis.getRecognitionRecordDetail({
- recognition_record_id: historyItem.id
- });
-
- if (detailResponse.statusCode === 200 || detailResponse.code === 200) {
- const detailData = detailResponse.data;
- console.log("获取详情成功:", detailData);
-
- // 设置识别结果数据
- detectionResult.value = {
- scene_name: detailData.tag_type || getTagTypeFromLabels(detailData.labels),
- labels: detailData.labels,
- total_detections: detailData.labels ? (Array.isArray(detailData.labels) ? detailData.labels.length : 0) : 0,
- third_scenes: detailData.third_scenes || []
- };
- // 设置图片URL
- const newImageUrl = detailData.recognition_image_url || detailData.original_image_url;
- annotatedImageUrl.value = newImageUrl;
-
- // 更新历史记录中的标签类型
- const tagType = detailData.tag_type || getTagTypeFromLabels(detailData.labels);
- historyItem.tagType = tagType;
-
- } else {
- console.error("获取详情失败:", detailResponse.message);
- showError("获取记录详情失败");
-
- // 使用历史记录中的基础数据作为后备
- detectionResult.value = {
- scene_name: historyItem.tagType || 'simple_supported_bridge',
- labels: historyItem.labels,
- total_detections: 0,
- third_scenes: []
- };
- const fallbackImageUrl = historyItem.recognitionImageUrl || historyItem.originalImageUrl;
- annotatedImageUrl.value = fallbackImageUrl;
- }
- // 更新数据层的active状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === historyItem.id;
- });
- console.log("历史记录状态已更新");
- } catch (error) {
- console.error("处理历史记录失败:", error);
- showError("获取记录详情失败");
- } finally {
- isLoadingDetail.value = false;
- isImageLoading.value = false;
- }
- };
- // 根据标签获取标签类型
- const getTagTypeFromLabels = (labels) => {
- if (!labels) return 'gas_station';
-
- let labelStr = '';
-
- if (Array.isArray(labels)) {
- labelStr = labels.join(' ').toLowerCase();
- } else {
- labelStr = String(labels).toLowerCase();
- }
-
- if (labelStr.includes('隧道')) return 'tunnel';
- if (labelStr.includes('桥梁')) return 'simple_supported_bridge';
- if (labelStr.includes('加油站')) return 'gas_station';
- if (labelStr.includes('设备')) return 'special_equipment';
- if (labelStr.includes('高速')) return 'operate_highway';
-
- return 'simple_supported_bridge';
- };
- // 获取当前时间
- const getCurrentTime = () => {
- const now = new Date();
- const month = now.getMonth() + 1;
- const day = now.getDate();
- const hours = now.getHours().toString().padStart(2, '0');
- const minutes = now.getMinutes().toString().padStart(2, '0');
- return `${month}月${day}日 ${hours}:${minutes}`;
- };
- // 处理主图片加载错误
- const handleMainImageError = (event) => {
- try {
- console.log("主图片加载失败");
- // 可以在这里添加其他错误处理逻辑
- } catch (error) {
- console.error("处理主图片错误失败:", error);
- }
- };
- // 打开图片预览
- const openImagePreview = () => {
- try {
- console.log("openImagePreview 被调用");
- showImagePreview.value = true;
- console.log("图片预览已打开");
- } catch (error) {
- console.error("打开图片预览失败:", error);
- }
- };
- // 关闭图片预览
- const closeImagePreview = () => {
- try {
- console.log("closeImagePreview 被调用");
- showImagePreview.value = false;
- console.log("图片预览已关闭");
- } catch (error) {
- console.error("关闭图片预览失败:", error);
- }
- };
- // 打开示例弹窗
- const openExampleModal = async (hazardInfo) => {
- try {
- console.log("openExampleModal 被调用,隐患信息:", hazardInfo);
- selectedHazard.value = hazardInfo;
-
- // 开始加载示例图
- isLoadingExample.value = true;
-
- // 调用API获取示例图
- const response = await apis.getThirdSceneExampleImage({
- third_scene_name: hazardInfo.description
- });
-
- console.log("获取示例图响应:", response);
-
- if (response.statusCode === 200) {
- const exampleData = response.data;
-
- // 检查是否有示例图数据
- if (exampleData && (exampleData.correct_example_image || exampleData.error_example_image)) {
- exampleImages.value = {
- correctImageUrl: exampleData.correct_example_image || '',
- errorImageUrl: exampleData.error_example_image || ''
- };
-
- // 设置图片加载状态
- imageLoadingStates.value = {
- correct: !!exampleData.correct_example_image,
- error: !!exampleData.error_example_image
- };
-
- showExampleModal.value = true;
- console.log("示例弹窗已打开,示例图数据:", exampleImages.value);
- } else {
- showWarning("暂无示例图");
- console.log("没有找到示例图数据");
- }
- } else {
- showError("获取示例图失败: " + (response.msg || "未知错误"));
- console.error("获取示例图失败:", response.msg);
- }
- } catch (error) {
- console.error("打开示例弹窗失败:", error);
- showError("获取示例图失败,请稍后重试");
- } finally {
- isLoadingExample.value = false;
- }
- };
- // 关闭示例弹窗
- const closeExampleModal = () => {
- try {
- console.log("closeExampleModal 被调用");
- showExampleModal.value = false;
- exampleImages.value = {};
- selectedHazard.value = null;
- isLoadingExample.value = false;
- imageLoadingStates.value = { correct: false, error: false };
- console.log("示例弹窗已关闭");
- } catch (error) {
- console.error("关闭示例弹窗失败:", error);
- }
- };
- // 处理图片加载错误
- const handleImageError = (event, type) => {
- console.log(`图片加载失败 (${type}):`, event.target.src);
- if (type === 'correct') {
- exampleImages.value.correctImageUrl = '';
- imageLoadingStates.value.correct = false;
- } else if (type === 'error') {
- exampleImages.value.errorImageUrl = '';
- imageLoadingStates.value.error = false;
- }
- };
- // 处理图片加载完成
- const handleImageLoad = (event, type) => {
- const img = event.target;
- const aspectRatio = img.naturalWidth / img.naturalHeight;
-
- if (aspectRatio > 1) {
- img.setAttribute('data-orientation', 'landscape');
- console.log(`图片加载完成 (${type}): 横图, 宽高比: ${aspectRatio.toFixed(2)}`);
- } else {
- img.setAttribute('data-orientation', 'portrait');
- console.log(`图片加载完成 (${type}): 竖图, 宽高比: ${aspectRatio.toFixed(2)}`);
- }
-
- if (type === 'correct') {
- imageLoadingStates.value.correct = false;
- } else if (type === 'error') {
- imageLoadingStates.value.error = false;
- }
- };
- // 打开点评弹窗
- const openEvaluationModal = async () => {
- try {
- console.log("打开点评弹窗");
- showEvaluationModal.value = true;
-
- // 如果当前记录已点评,则回显后端数据并禁用交互
- if (selectedHistoryItem.value?.effect_evaluation > 0) {
- await loadEvaluationData();
- } else {
- // 未点评则重置数据
- evaluationData.value = {
- sceneMatch: null,
- promptAccurate: null,
- rating: 0,
- userRemark: ''
- };
- }
- } catch (error) {
- console.error("打开点评弹窗失败:", error);
- }
- };
- // 加载点评数据(回显PC端逻辑)
- const loadEvaluationData = async () => {
- try {
- if (!selectedHistoryItem.value?.id) return
-
- console.log('📝 开始加载点评数据,记录ID:', selectedHistoryItem.value.id)
-
- try {
- const detailResponse = await apis.getRecognitionRecordDetail({
- recognition_record_id: selectedHistoryItem.value.id
- })
-
- console.log('📝 详情接口响应:', detailResponse)
-
- if (detailResponse.statusCode === 200 || detailResponse.code === 200) {
- const detailData = detailResponse.data
- console.log('📝 移动端加载点评数据:', detailData)
- console.log('📝 用户备注字段:', detailData.user_remark)
-
- evaluationData.value = {
- sceneMatch: detailData.scene_match === 1,
- promptAccurate: detailData.tip_accuracy === 1,
- rating: detailData.effect_evaluation || 0,
- userRemark: detailData.user_remark || ''
- }
-
- console.log('📝 设置后的evaluationData:', evaluationData.value)
- } else {
- console.warn('📝 详情接口返回错误:', detailResponse)
- // 使用历史记录中的基础数据作为后备
- evaluationData.value = {
- sceneMatch: null,
- promptAccurate: null,
- rating: selectedHistoryItem.value.effect_evaluation || 0,
- userRemark: ''
- }
- }
- } catch (apiError) {
- console.error('📝 详情接口调用失败:', apiError)
- // 如果详情接口调用失败,使用历史记录中的基础数据
- evaluationData.value = {
- sceneMatch: null,
- promptAccurate: null,
- rating: selectedHistoryItem.value.effect_evaluation || 0,
- userRemark: ''
- }
- }
-
- // 同步本地selectedHistoryItem的状态,保证页内与外层抽屉一致
- if (selectedHistoryItem.value) {
- selectedHistoryItem.value.effect_evaluation = evaluationData.value.rating
- }
- // 同步historyData对应条目
- const historyItem = historyData.value.find(item => item.id === selectedHistoryItem.value?.id)
- if (historyItem) {
- historyItem.effect_evaluation = evaluationData.value.rating
- }
-
- } catch (error) {
- console.error('📝 加载点评数据失败:', error)
- // 出错时使用默认值
- evaluationData.value = {
- sceneMatch: null,
- promptAccurate: null,
- rating: selectedHistoryItem.value?.effect_evaluation || 0,
- userRemark: ''
- }
- }
- }
- // 关闭点评弹窗
- const closeEvaluationModal = () => {
- try {
- console.log("关闭点评弹窗");
- showEvaluationModal.value = false;
- } catch (error) {
- console.error("关闭点评弹窗失败:", error);
- }
- };
- // 提交评价
- const submitEvaluation = async () => {
- try {
- console.log("提交评价:", evaluationData.value);
-
- // 验证是否所有问题都已回答
- if (evaluationData.value.sceneMatch === null ||
- evaluationData.value.promptAccurate === null ||
- evaluationData.value.rating === 0) {
- showWarning("请完成所有评价项目");
- return;
- }
-
- // 调用后端API提交评价
- const response = await apis.submitEvaluation({
- id: selectedHistoryItem.value?.id,
- scene_match: evaluationData.value.sceneMatch ? 1 : 0,
- tip_accuracy: evaluationData.value.promptAccurate ? 1 : 0,
- effect_evaluation: evaluationData.value.rating,
- user_remark: evaluationData.value.userRemark
- });
-
- if (response.statusCode === 200 || response.code === 200) {
- showSuccess("评价提交成功");
-
- // 更新当前历史记录的点评状态
- if (selectedHistoryItem.value) {
- selectedHistoryItem.value.effect_evaluation = evaluationData.value.rating;
- }
-
- // 更新历史记录列表中的对应项
- const historyItem = historyData.value.find(item => item.id === selectedHistoryItem.value?.id);
- if (historyItem) {
- historyItem.effect_evaluation = evaluationData.value.rating;
- }
-
- // 关闭弹窗
- closeEvaluationModal();
- } else {
- showError("评价提交失败: " + (response.msg || "未知错误"));
- }
- } catch (error) {
- console.error("提交评价失败:", error);
- showError("评价提交失败,请稍后重试");
- }
- };
- // 删除历史记录
- const deleteHistoryItem = async (historyItem, index) => {
- try {
- console.log('开始删除移动端历史记录:', historyItem)
-
- const response = await apis.deleteRecognitionRecord({
- recognition_record_id: historyItem.id
- })
-
- if (response.statusCode === 200) {
- // 从本地数据中移除
- historyData.value.splice(index, 1)
- historyTotal.value = Math.max(0, historyTotal.value - 1)
-
- // 如果删除的是当前激活的历史记录,执行新建任务
- if (historyItem.isActive) {
- console.log('删除激活的历史记录,执行新建任务')
- createNewTask()
- }
-
- console.log('✅ 移动端历史记录删除成功')
- showSuccess('删除成功')
- } else {
- console.error('❌ 删除移动端历史记录删除失败:', response)
- }
- } catch (error) {
- console.error('❌ 删除移动端历史记录失败:', error)
- }
- }
- // 页面加载时不再自动加载历史记录,改为点击菜单时加载
- onMounted(async () => {
- try {
- console.log('🚀 移动端隐患提示页面初始化完成')
- } catch (error) {
- console.error('❌ 移动端隐患提示页面初始化失败:', error)
- }
- })
- // 监听历史记录抽屉显示状态,显示时加载数据
- watch(showHistory, async (newVal) => {
- if (newVal && historyData.value.length === 0) {
- console.log('📋 历史记录抽屉打开,开始加载数据...')
- await getHistoryRecordList()
- }
- })
- </script>
- <style lang="less" scoped>
- .mobile-hazard-detection {
- height: 100vh;
- background: #EBF3FF;
- font-family: "Alibaba PuHuiTi 3.0", sans-serif;
- overflow-y: auto;
- overflow-x: hidden;
- }
- .mobile-content {
- padding: 16px;
- position: relative;
- min-height: calc(100vh - 60px);
- }
- // 选项卡样式
- .process-tabs {
- display: flex;
- background: white;
- border-radius: 12px;
- padding: 4px;
- margin-bottom: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- .tab-item {
- flex: 1;
- padding: 12px 16px;
- text-align: center;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: 14px;
- font-weight: 500;
- color: #6b7280;
- &.active {
- background: #3e7bfa;
- color: white;
- box-shadow: 0 2px 4px rgba(62, 123, 250, 0.3);
- }
- &:hover:not(.active) {
- color: #3e7bfa;
- background: rgba(62, 123, 250, 0.05);
- }
- }
- }
- // 主界面布局
- .main-layout {
- .hazard-system {
- background: white;
- border-radius: 16px;
- padding: 20px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- .system-header {
- text-align: center;
- margin-bottom: 32px;
-
- h3 {
- font-size: 20px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 10px 0;
- }
-
- p {
- font-size: 14px;
- color: #6b7280;
- margin: 0;
- line-height: 1.4;
- }
- }
-
- .step-section {
- margin-bottom: 24px;
-
- h4 {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 8px 0;
- }
-
- .step-description {
- font-size: 14px;
- color: #6b7280;
- margin: 0 0 12px 0;
- line-height: 1.4;
- }
-
- .scenario-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
-
- .scenario-tag {
- padding: 12px 16px;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- background: #f8faff;
- color: #1f2937;
- flex: 1;
- min-width: calc(50% - 6px);
- text-align: center;
-
- &.compact {
- min-width: calc(50% - 6px);
- flex: 0 1 auto;
- }
-
- &.active {
- background: rgba(62, 123, 250, 0.1);
- color: #3366e6;
- border-color: #3366e6;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
- }
-
- &.disabled {
- background: #f3f4f6;
- color: #9ca3af;
- border-color: #d1d5db;
- cursor: not-allowed;
- opacity: 0.6;
-
- &:hover {
- transform: none;
- box-shadow: none;
- }
- }
-
- &.identifying-disabled {
- background: #f3f4f6;
- color: #9ca3af;
- border-color: #d1d5db;
- cursor: not-allowed;
- opacity: 0.6;
- pointer-events: none;
-
- &:hover {
- transform: none;
- box-shadow: none;
- }
- }
-
- &:hover:not(.disabled):not(.identifying-disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- }
- }
-
- .upload-area {
- position: relative;
- border: 2px dashed #3e7bfa;
- border-radius: 12px;
- margin-top: 12px;
- height: 300px;
- text-align: center;
- cursor: pointer;
- transition: all 0.3s ease;
-
- &:hover {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.02);
- }
- &.drag-over {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.05);
- box-shadow: 0 0 10px rgba(62, 123, 250, 0.2);
- }
-
- .uploaded-image-container {
- position: relative;
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #f1f6ff;
- border-radius: 8px;
- overflow: hidden;
- .uploaded-image {
- width: 100%;
- height: 100%;
- object-fit: contain;
- border-radius: 8px;
- }
- .image-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 8px;
- cursor: pointer;
- opacity: 0;
- transition: opacity 0.3s ease;
- .change-image-btn {
- background: #3e7bfa;
- color: white;
- border: none;
- border-radius: 8px;
- padding: 6px 12px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: background-color 0.3s ease;
- &:hover {
- background: #3366e6;
- }
- }
- }
-
- &:hover .image-overlay {
- opacity: 1;
- }
- }
- .upload-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- padding: 20px;
- .upload-icon {
- width: 40px;
- height: 40px;
- margin-bottom: 10px;
- }
-
-
- .upload-text {
- font-size: 14px;
- font-weight: 600;
- color: #2c3e50;
- margin: 0 0 6px 0;
- }
-
- .upload-format {
- font-size: 12px;
- color: #6b7280;
- margin: 0 0 20px 0;
- }
-
- .select-file-btn {
- background: #e5eeff;
- border: 1px solid #3e7bfa;
- border-radius: 8px;
- width: 120px;
- height: 36px;
- font-size: 14px;
- color: #3e7bfa;
- cursor: pointer;
- transition: all 0.3s ease;
-
- &:hover {
- background: #e5e7eb;
- border-color: #9ca3af;
- }
- }
- }
- }
- .upload-status {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, 0.7);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border-radius: 12px;
- z-index: 10;
- backdrop-filter: blur(1px);
- .loading-spinner {
- border: 4px solid #f8f9fa;
- border-top: 4px solid #3e7bfa;
- border-radius: 50%;
- width: 32px;
- height: 32px;
- animation: spin 1s linear infinite;
- margin-bottom: 8px;
- }
- p {
- font-size: 14px;
- color: #4b5563;
- font-weight: 500;
- }
- }
- }
-
- .action-section {
- text-align: center;
- margin-top: 24px;
-
- .start-identify-btn {
- position: relative;
- border: none;
- background: none;
- cursor: pointer;
- padding: 0;
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- transition: opacity 0.3s ease;
-
- &.btn-disabled {
- opacity: 0.6;
- cursor: not-allowed;
- pointer-events: none;
- }
-
-
- .btn-bg {
- width: 100%;
- border-radius: 12px;
- // max-width: 340px;
- height: 60px;
- object-fit: cover;
- margin: 0 auto;
- display: block;
- }
- }
- }
- }
-
- // 使用流程页面样式
- .process-content {
- background: white;
- border-radius: 16px;
- padding: 20px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- .process-header {
- text-align: center;
- margin-bottom: 24px;
-
- h3 {
- font-size: 20px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 8px 0;
- }
-
- p {
- font-size: 14px;
- color: #6b7280;
- margin: 0;
- }
- }
- .process-steps {
- margin-bottom: 24px;
-
- .step-item {
- display: flex;
- align-items: center;
- margin-bottom: 16px;
-
- .step-number {
- width: 32px;
- height: 32px;
- background: #3e7bfa;
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 14px;
- margin-right: 12px;
- flex-shrink: 0;
- }
-
- .step-text {
- flex: 1;
-
- h4 {
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 4px 0;
- }
-
- p {
- font-size: 16px;
- color: #6b7280;
- margin: 0;
- line-height: 1.4;
- }
- }
- }
- }
- .process-image-section {
- text-align: center;
-
- .process-image {
- width: 100%;
- max-width: 300px;
- height: auto;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- }
- }
- }
- // 详情页布局
- .detail-layout {
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 0;
- margin-bottom: 16px;
- .header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- .header-icon {
- width: 32px;
- height: 32px;
- }
- .header-text {
- display: flex;
- flex-direction: column;
- gap: 4px;
- .main-title {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- line-height: 1.2;
- }
- .sub-title-tag {
- font-size: 10px;
- line-height: 10px;
- text-align: center;
- padding:4px 0px;
- border-radius: 4px;
- font-weight: 500;
- flex-shrink: 0;
-
-
- &.tag-tunnel {
- background: rgba(62, 123, 250, 0.1);
- color: #3366e6;
- }
-
- &.tag-bridge {
- background: rgba(34, 184, 80, 0.1);
- color: #22b850;
- }
-
- &.tag-equipment {
- background: rgba(0, 128, 255, 0.1);
- color: #0080ff;
- }
-
- &.tag-highway {
- background: rgba(114, 46, 209, 0.1);
- color: #722ed1;
- }
-
- &.tag-gas-station {
- background: rgba(255, 77, 79, 0.1);
- color: #ff4d4f;
- }
- }
- }
- }
- .header-right {
- .current-time {
- font-size: 12px;
- color: #6b7280;
- }
- }
- }
- .detail-content {
- background: white;
- border-radius: 16px;
- padding: 16px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- position: relative;
-
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, 0.9);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 100;
-
- .loading-spinner {
- border: 4px solid #f3f3f3;
- border-top: 4px solid #3e7bfa;
- border-radius: 50%;
- width: 32px;
- height: 32px;
- animation: spin 1s linear infinite;
- margin-bottom: 12px;
- }
-
- p {
- font-size: 14px;
- color: #666;
- margin: 0;
- }
- }
- .image-section {
- background-color: #f1f6ff;
- border-radius: 8px;
- padding: 12px 0;
- text-align: center;
- margin-bottom: 16px;
- position: relative;
- .evaluation-status {
- position: absolute;
- top: 8px;
- right: 8px;
- z-index: 10;
- cursor: pointer;
- .status-badge {
- display: inline-block;
- padding: 3px 8px;
- border-radius: 8px;
- font-size: 10px;
- font-weight: 500;
- line-height: 1.2;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- &.evaluated {
- background: #d1fae5;
- color: #065f46;
- border: 1px solid #a7f3d0;
- }
- &.not-evaluated {
- background: #fef3c7;
- color: #92400e;
- border: 1px solid #fde68a;
- }
- }
- }
- .image-container {
- position: relative;
- display: inline-block;
- max-width: 280px;
- height: 200px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- border: 2px dashed #2c8d6980;
- padding: 4px;
- overflow: hidden;
- }
- .original-image, .main-image {
- width: 100%;
- height: 100%;
- border-radius: 8px;
- object-fit: contain;
- transform: none !important;
- }
- .main-image {
- cursor: pointer;
- }
- .scanning-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- overflow: hidden;
- border-radius: 8px;
- .scanning-line {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 2px;
- background: linear-gradient(90deg,
- transparent 0%,
- #3e7bfa 20%,
- #3e7bfa 80%,
- transparent 100%
- );
- box-shadow: 0 0 8px rgba(62, 123, 250, 0.8);
- animation: scanning 2s ease-in-out 3;
- }
- }
- }
- .analysis-section {
- background-color: #F9FAFB;
- border-left: 3px solid #3B82F6;
- padding: 16px 10px;
- margin-bottom: 16px;
-
- .analysis-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- .warning-icon {
- width: 24px;
- height: 24px;
- }
- h3 {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0;
- }
- }
- .analysis-content {
- .scene-info {
- font-size: 14px;
- color: #374151;
- margin-left: 32px;
- word-wrap: break-word;
-
- .scene-tags {
- color: #3E7BFA;
- font-weight: 500;
- }
- .detection-count {
- color: #3E7BFA;
- font-weight: 600;
- margin-left: 8px;
- }
- .streaming-text {
- color: #3E7BFA;
- font-weight: 600;
- border-right: 2px solid #3E7BFA;
- animation: blink 1s infinite;
- }
- .streaming-analysis-text {
- color: #374151;
- font-weight: 400;
- border-right: 2px solid #374151;
- animation: blink 1s infinite;
- }
- .analysis-prompt {
- color: #3E7BFA;
- font-weight: 500;
- font-style: italic;
- }
- .loading-labels {
- display: inline-flex;
- align-items: center;
- color: #3E7BFA;
- font-weight: 600;
- margin-left: 8px;
- }
- .loading-dots {
- display: inline-block;
- width: 3px;
- height: 3px;
- border-radius: 50%;
- background-color: #3E7BFA;
- animation: loadingDots 1.4s infinite ease-in-out both;
- margin-left: 1px;
- }
- .loading-dots::before,
- .loading-dots::after {
- content: '';
- position: absolute;
- width: 3px;
- height: 3px;
- border-radius: 50%;
- background-color: #3E7BFA;
- animation: loadingDots 1.4s infinite ease-in-out both;
- }
- .loading-dots::before {
- left: -6px;
- animation-delay: -0.32s;
- }
- .loading-dots::after {
- left: 6px;
- animation-delay: 0.32s;
- }
- }
- }
- }
- .hazards-section {
- background-color: #F9FAFB;
- padding: 12px;
- border-radius: 8px;
-
- .hazards-header {
- margin-bottom: 12px;
-
- h3 {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0;
- }
- }
- .hazards-content {
- position: relative; /* 为遮罩层提供定位基准 */
-
- &.scanning-mode {
- min-height: 120px; /* 扫描期间保持最小高度 */
- }
-
- .hazard-item {
- .hazard-line {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
-
- .hazard-number {
- width: 20px;
- height: 20px;
- background: #3E7BFA;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- color: #FFFFFF;
- flex-shrink: 0;
- }
- .hazard-desc {
- flex: 1;
- font-size: 14px;
- color: #000000;
- line-height: 1.4;
- word-wrap: break-word;
- }
- .example-btn {
- background: #3468E710;
- color: #3468E7;
- border: none;
- border-radius: 6px;
- width: 40px;
- height: 20px;
- font-size: 12px;
- cursor: pointer;
- transition: background-color 0.3s ease;
- flex-shrink: 0;
- &:hover {
- background: #3468E720;
- }
- }
- }
- }
-
- /* 隐患区域加载遮罩层样式 */
- .hazards-loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(249, 250, 251, 0.9);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 10;
- border-radius: 8px;
- backdrop-filter: blur(2px);
-
- .loading-spinner {
- border: 3px solid #f3f3f3;
- border-top: 3px solid #3e7bfa;
- border-radius: 50%;
- width: 32px;
- height: 32px;
- animation: spin 1s linear infinite;
- margin-bottom: 12px;
- }
-
- p {
- font-size: 14px;
- color: #6b7280;
- margin: 0;
- font-weight: 500;
- }
- }
- }
- }
- }
- }
- // 弹窗样式
- .image-preview-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- cursor: pointer;
- }
- .preview-image {
- max-width: 90%;
- max-height: 90%;
- object-fit: contain;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- transform: none !important;
- }
- .example-modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .example-modal {
- width: 90%;
- max-width: 500px;
- max-height: 80%;
- background: white;
- border-radius: 16px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
- overflow: hidden;
- }
- .modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 20px;
- .modal-title {
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- text-align: center
- }
-
- .close-icon {
- width: 24px;
- height: 24px;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover {
- opacity: 0.7;
- }
- }
- }
- .modal-hazard-info {
- display: flex;
- align-items: center;
- justify-content: center;
- // gap: 8px;
- margin-top: 16px;
- margin-bottom: 8px;
- .hazard-number {
- width: 20px;
- height: 20px;
- background: #3E7BFA;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: 600;
- color: #FFFFFF;
- flex-shrink: 0;
- }
- .hazard-description {
- font-size: 14px;
- color: #374151;
- line-height: 1.4;
- text-align: center;
- padding: 0 8px;
- }
- }
- .modal-body {
- padding: 0 20px 20px;
- height: calc(100% - 120px);
-
- .loading-overlay {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 200px;
-
- .loading-spinner {
- border: 4px solid #f3f3f3;
- border-top: 4px solid #3e7bfa;
- border-radius: 50%;
- width: 32px;
- height: 32px;
- animation: spin 1s linear infinite;
- margin-bottom: 12px;
- }
-
- p {
- font-size: 14px;
- color: #666;
- margin: 0;
- }
- }
- .example-images {
- display: flex;
- gap: 12px;
-
- .example-panel {
- flex: 1;
-
- .panel-image {
- position: relative;
- height: 150px;
- border-radius: 8px;
- overflow: hidden;
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: 8px;
- }
-
- .no-image-placeholder {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #f5f5f5;
- border: 2px dashed #d1d5db;
- border-radius: 8px;
-
- .placeholder-text {
- color: #6b7280;
- font-size: 12px;
- font-weight: 500;
- text-align: center;
- }
- }
- .image-loading {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background-color: #f9fafb;
- border: 2px dashed #d1d5db;
- border-radius: 8px;
-
- .loading-spinner {
- border: 3px solid #f3f3f3;
- border-top: 3px solid #3e7bfa;
- border-radius: 50%;
- width: 24px;
- height: 24px;
- animation: spin 1s linear infinite;
- margin-bottom: 8px;
- }
- p {
- color: #6b7280;
- font-size: 12px;
- font-weight: 500;
- margin: 0;
- }
- }
- .image-label {
- position: absolute;
- top: 4px;
- left: 4px;
- z-index: 10;
- .label-icon {
- width: 60px;
- height: 20px;
- }
- }
- }
- }
- }
- }
- .evaluation-modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .evaluation-modal {
- width: 90%;
- max-width: 400px;
- background: white;
- border-radius: 16px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
- overflow: hidden;
- }
- .modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20px 20px 0;
- .modal-title {
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- }
-
- .close-icon {
- width: 24px;
- height: 24px;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover {
- opacity: 0.7;
- }
- }
- }
- .modal-body {
- padding: 20px;
-
- .question-section {
- margin-bottom: 24px;
-
- .question-title {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- margin-bottom: 12px;
- }
-
- .answer-buttons {
- display: flex;
- gap: 8px;
-
- .answer-btn {
- flex: 1;
- padding: 8px 16px;
- border: 2px solid #d1d5db;
- border-radius: 8px;
- background: white;
- color: #6b7280;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
-
- &:hover {
- border-color: #3e7bfa;
- color: #3e7bfa;
- }
-
- &.active {
- border-color: #3e7bfa;
- background: #3e7bfa;
- color: white;
- }
- &.disabled {
- cursor: not-allowed;
- pointer-events: none;
- opacity: 0.6;
- }
- }
- }
-
- .star-rating {
- display: flex;
- gap: 6px;
-
- .star {
- font-size: 24px;
- color: #d1d5db;
- cursor: pointer;
- transition: color 0.2s ease;
-
- &:hover {
- color: #fbbf24;
- }
-
- &.active {
- color: #fbbf24;
- }
- &.disabled {
- cursor: not-allowed;
- pointer-events: none;
- // 不改变颜色,保留已选中星星的金色显示
- }
-
- // 禁用且已选中时,保持金色
- &.disabled.active {
- color: #fbbf24;
- }
- }
- }
-
- .remark-input-container {
- position: relative;
-
- .remark-textarea {
- width: 100%;
- padding: 12px;
- border: 2px solid #d1d5db;
- border-radius: 8px;
- font-size: 14px;
- font-family: inherit;
- resize: vertical;
- min-height: 80px;
- transition: border-color 0.3s ease;
- background: white;
-
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
-
- &::placeholder {
- color: #9ca3af;
- }
-
- &.disabled {
- background: #f9fafb;
- color: #6b7280;
- cursor: not-allowed;
- border-color: #e5e7eb;
- }
- }
-
- .char-count {
- position: absolute;
- bottom: 8px;
- right: 12px;
- font-size: 12px;
- color: #9ca3af;
- background: white;
- padding: 2px 4px;
- border-radius: 4px;
-
- .over-limit {
- color: #ef4444;
- font-weight: 500;
- }
- }
- }
- }
- }
- .modal-footer {
- padding: 0 20px 20px;
- text-align: center;
-
- .submit-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
-
- .submit-icon {
- width: 100px;
- height: 36px;
- object-fit: contain;
- }
- }
- }
- // 动画
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- @keyframes scanning {
- 0% {
- top: 0;
- opacity: 0;
- }
- 10% {
- opacity: 1;
- }
- 90% {
- opacity: 1;
- }
- 100% {
- top: 100%;
- opacity: 0;
- }
- }
- @keyframes blink {
- 0%, 50% {
- border-color: #3E7BFA;
- }
- 51%, 100% {
- border-color: transparent;
- }
- }
- @keyframes loadingDots {
- 0%, 80%, 100% {
- transform: scale(0);
- opacity: 0.5;
- }
- 40% {
- transform: scale(1);
- opacity: 1;
- }
- }
- </style>
|