| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293 |
- const ICONS = {
- activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
- "arrow-left": '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
- "arrow-down": '<path d="m7 7 5 5 5-5"/><path d="M12 12v10"/>',
- "arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
- "arrow-up": '<path d="m7 17 5-5 5 5"/><path d="M12 12v10"/>',
- "arrow-up-right": '<path d="M7 7h10v10"/><path d="M7 17 17 7"/>',
- award: '<circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/>',
- bell: '<path d="M10.3 21a2 2 0 0 0 3.4 0"/><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/>',
- bot: '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>',
- camera: '<path d="M14.5 4h-5L8 6H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-4z"/><circle cx="12" cy="13" r="4"/>',
- "book-open": '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
- "chart-no-axes-combined": '<path d="M12 16v5"/><path d="M16 14v7"/><path d="M20 10v11"/><path d="m22 3-8.7 8.7a1 1 0 0 1-1.4 0L8.4 8.2a1 1 0 0 0-1.4 0L2 13.2"/><path d="M4 16v5"/>',
- "check-circle-2": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
- "chevron-down": '<path d="m6 9 6 6 6-6"/>',
- "chevron-right": '<path d="m9 18 6-6-6-6"/>',
- "circle-alert": '<circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
- "circle-dollar-sign": '<circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 6v12"/>',
- "clipboard-check": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="m9 14 2 2 4-4"/>',
- "cloud-dust": '<path d="M10 8a3 3 0 1 1 6 0"/><path d="M5 13a4 4 0 0 1 3-6.9"/><path d="M13 6a4 4 0 0 1 6 3.3"/><path d="M4 15h12a3 3 0 0 1 0 6H7a3 3 0 0 1-3-3"/><path d="M7 21v-2"/><path d="M11 21v-2"/><path d="M15 21v-2"/>',
- cpu: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M9 2v2"/><path d="M15 2v2"/><path d="M9 20v2"/><path d="M15 20v2"/><path d="M2 9h2"/><path d="M2 15h2"/><path d="M20 9h2"/><path d="M20 15h2"/>',
- database: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/>',
- "door-open": '<path d="M13 4h3a2 2 0 0 1 2 2v14"/><path d="M2 20h3"/><path d="M13 20h9"/><path d="M5 20V6a2 2 0 0 1 2-2h6v16"/><path d="M10 12v.01"/>',
- download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
- "file-text": '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M8 13h8"/><path d="M8 17h8"/><path d="M8 9h2"/>',
- filter: '<path d="M3 4h18"/><path d="M6 12h12"/><path d="M10 20h4"/>',
- flame: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
- folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
- gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
- history: '<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
- "layout-dashboard": '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
- "maximize-2": '<path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/><path d="M9 21H3v-6"/>',
- menu: '<path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/>',
- mountain: '<path d="m8 3 4 8 5-5 5 15H2L8 3z"/><path d="m4.14 15.08 2.39-2.39"/><path d="m14.5 8.5 1.5 1.5"/>',
- network: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
- pickaxe: '<path d="M14.5 3.5a2.1 2.1 0 0 1 3 3L8 16l-4 1 1-4z"/><path d="m5 14 5 5"/><path d="m3 3 4 4"/><path d="m14 3 4 4"/>',
- play: '<polygon points="6 3 20 12 6 21 6 3"/>',
- "plug-zap": '<path d="M13 2 3 14h9l-1 8 10-12h-9z"/>',
- plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
- "radio-tower": '<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.2 19.1 19.1"/><path d="M12 12h.01"/><path d="M12 16v6"/><path d="m9 22 3-5 3 5"/>',
- "refresh-cw": '<path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><path d="M3 21v-5h5"/>',
- route: '<circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/>',
- "scan-search": '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="11" cy="11" r="3"/><path d="m16 16-2.6-2.6"/>',
- search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
- settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
- "settings-2": '<path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>',
- "shield-alert": '<path d="M20 13c0 5-3.5 7.5-8 9-4.5-1.5-8-4-8-9V5l8-3 8 3z"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
- "shield-check": '<path d="M20 13c0 5-3.5 7.5-8 9-4.5-1.5-8-4-8-9V5l8-3 8 3z"/><path d="m9 12 2 2 4-4"/>',
- "sliders-horizontal": '<path d="M21 4h-7"/><path d="M10 4H3"/><path d="M21 12h-9"/><path d="M8 12H3"/><path d="M21 20h-5"/><path d="M12 20H3"/><circle cx="12" cy="4" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="14" cy="20" r="2"/>',
- sparkles: '<path d="m12 3-1.9 5.1a2 2 0 0 1-1.2 1.2L3.8 11.2a1 1 0 0 0 0 1.9l5.1 1.9a2 2 0 0 1 1.2 1.2L12 21l1.9-5.1a2 2 0 0 1 1.2-1.2l5.1-1.9a1 1 0 0 0 0-1.9l-5.1-1.9a2 2 0 0 1-1.2-1.2z"/><path d="M5 3v4"/><path d="M3 5h4"/><path d="M19 17v4"/><path d="M17 19h4"/>',
- thermometer: '<path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"/><path d="M12 14v-4"/>',
- "trending-up": '<path d="m3 17 6-6 4 4 8-8"/><path d="M14 7h7v7"/>',
- clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
- "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
- "trash-2": '<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5"/><path d="M14 11v5"/>',
- pencil: '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>',
- upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
- "user-round": '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
- users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
- "volume-2": '<path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>',
- video: '<path d="m16 13 5.2 3.5a.5.5 0 0 0 .8-.4V7.9a.5.5 0 0 0-.8-.4L16 11"/><rect width="14" height="12" x="2" y="6" rx="2"/>',
- wind: '<path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/>',
- workflow: '<rect width="6" height="6" x="3" y="3" rx="1"/><rect width="6" height="6" x="15" y="15" rx="1"/><path d="M9 6h4a2 2 0 0 1 2 2v4"/><path d="M15 18h-4a2 2 0 0 1-2-2v-4"/>',
- x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
- };
- const VIEW_META = {
- dashboard: ["综合驾驶舱", "汇聚通防业务数据,洞察矿井通风系统运行态势"],
- data: ["数据感知", "统一接入业务资料、监测数据、视频监控与通防设施工况"],
- "facility-status": ["通防设施工况", "按设备类别查看通风设施运行、报警和断开状态"],
- "safety-monitoring": ["KJ 安全监控系统", "通防监测数据接入、实时曲线与智能趋势预测"],
- "video-platform": ["工业视频平台", "通道管理、实时视频、分屏预览与智能识别事件"],
- "business-data": ["业务资料", "统一管理规程、措施、报表、台账与验收资料,实现资料智能抽取与全文检索"],
- knowledge: ["通防知识中心", "融合规范标准、事故案例与专家经验,实现知识检索与智能问答"],
- diagnosis: ["隐患识别中心", "面向五类重大安全风险,编排专业智能体完成识别、研判与风险汇聚"],
- evaluation: ["综合评价", "建立多维度量化指标体系,结合大模型完成评价、解释与优化建议"],
- "closed-loop": ["闭环处置", "将诊断与评价结果转为任务,实现整改、复核、销号和案例沉淀"],
- settings: ["系统配置", "管理数据源、模型服务、指标规则、用户权限与平台运行参数"]
- };
- const HAZARD_SCENES = [
- {
- key: "local",
- name: "局部通风",
- icon: "wind",
- description: "局部通风机、风筒与循环风风险"
- },
- {
- key: "facility",
- name: "通风设施",
- icon: "door-open",
- description: "风门、风窗、密闭与漏风风险"
- },
- {
- key: "system",
- name: "通风系统管理",
- icon: "network",
- description: "主通风机、阻力与系统可靠性风险"
- },
- {
- key: "volume",
- name: "风量分配",
- icon: "gauge",
- description: "采掘工作面、用风点与风速风险"
- },
- {
- key: "disaster",
- name: "灾害防治",
- icon: "shield-alert",
- description: "瓦斯、粉尘、火灾与有毒有害气体风险"
- }
- ];
- const HAZARD_RISK_LIBRARY = {
- "risk-gas": {
- id: "risk-gas",
- defect_name: "N1202 回风巷瓦斯浓度超限",
- defect_code: "RISK-GAS-001",
- scene_key: "disaster",
- scene: "灾害防治",
- severity: "high",
- confidence: 92.6,
- status: "processing",
- conclusion: "瓦斯浓度超过报警阈值,存在重大安全隐患。",
- recommendation: "立即停止受影响区域作业,核查局部通风机和回风系统。",
- occurred_at: "10:38:24",
- evidence: ["radio-tower", "wind", "video"]
- },
- "risk-local-fan": {
- id: "risk-local-fan",
- defect_name: "T-17 局部通风机停风",
- defect_code: "RISK-LOCAL-001",
- scene_key: "local",
- scene: "局部通风",
- severity: "high",
- confidence: 91.2,
- status: "pending",
- conclusion: "局部通风机连续失电,存在无计划停风风险。",
- recommendation: "核查供电回路、风筒连接和受影响区域人员撤离情况。",
- occurred_at: "10:32:08",
- evidence: ["plug-zap", "wind"]
- },
- "risk-door": {
- id: "risk-door",
- defect_name: "2# 调节风门状态不一致",
- defect_code: "RISK-FAC-001",
- scene_key: "facility",
- scene: "通风设施",
- severity: "medium",
- confidence: 88.7,
- status: "pending",
- conclusion: "视频画面与风门传感器反馈状态不一致。",
- recommendation: "安排现场复核风门开闭状态、闭锁装置和密封情况。",
- occurred_at: "10:28:42",
- evidence: ["video", "settings-2"]
- },
- "risk-volume": {
- id: "risk-volume",
- defect_name: "S1301 采煤工作面风量偏小",
- defect_code: "RISK-AIR-001",
- scene_key: "volume",
- scene: "风量分配",
- severity: "medium",
- confidence: 89.3,
- status: "processing",
- conclusion: "实际风量低于计划风量,未满足工作面配风要求。",
- recommendation: "调整风窗开度,排查漏风点并完成风量复测。",
- occurred_at: "09:56:10",
- evidence: ["gauge", "file-text"]
- },
- "risk-resistance": {
- id: "risk-resistance",
- defect_name: "北翼通风阻力较上月上升 6.2%",
- defect_code: "RISK-SYS-001",
- scene_key: "system",
- scene: "通风系统管理",
- severity: "low",
- confidence: 86.5,
- status: "closed",
- conclusion: "北翼主要风路阻力呈持续上升趋势。",
- recommendation: "排查巷道变形、堆积物及局部堵塞,持续跟踪阻力变化。",
- occurred_at: "08:30:00",
- evidence: ["activity", "file-text"]
- },
- "risk-duct-leak": {
- id: "risk-duct-leak",
- defect_name: "T-17 风筒接口漏风",
- defect_code: "RISK-LOCAL-002",
- scene_key: "local",
- scene: "局部通风",
- severity: "medium",
- confidence: 87.8,
- status: "pending",
- conclusion: "风筒接口漏风导致末端风量不足。",
- recommendation: "检查风筒吊挂和接口连接,修复后进行末端风量复测。",
- occurred_at: "11:02:16",
- evidence: ["wind", "video", "gauge"]
- },
- "risk-circulation": {
- id: "risk-circulation",
- defect_name: "N1201 局部通风机循环风风险",
- defect_code: "RISK-LOCAL-003",
- scene_key: "local",
- scene: "局部通风",
- severity: "high",
- confidence: 90.4,
- status: "pending",
- conclusion: "吸风口与巷道风流瓦斯浓度差及风筒位置满足循环风风险特征。",
- recommendation: "立即调整风筒位置并复测吸风口、巷道和末端风流参数。",
- occurred_at: "11:06:40",
- evidence: ["radio-tower", "wind", "route"]
- },
- "risk-window": {
- id: "risk-window",
- defect_name: "S1301 调节风窗开度偏差",
- defect_code: "RISK-FAC-002",
- scene_key: "facility",
- scene: "通风设施",
- severity: "medium",
- confidence: 87.1,
- status: "pending",
- conclusion: "风窗实际开度与配风计划存在偏差。",
- recommendation: "复核风窗执行机构并按照配风计划重新标定开度。",
- occurred_at: "11:10:08",
- evidence: ["settings-2", "gauge"]
- },
- "risk-seal-leak": {
- id: "risk-seal-leak",
- defect_name: "北翼 2# 密闭漏风",
- defect_code: "RISK-FAC-003",
- scene_key: "facility",
- scene: "通风设施",
- severity: "medium",
- confidence: 89.6,
- status: "pending",
- conclusion: "密闭压差与相邻风量变化显示存在漏风特征。",
- recommendation: "检查密闭墙体、观测孔和周边裂隙,完成封堵后复测。",
- occurred_at: "11:14:22",
- evidence: ["activity", "radio-tower"]
- },
- "risk-main-fan": {
- id: "risk-main-fan",
- defect_name: "主通风机电流波动异常",
- defect_code: "RISK-SYS-002",
- scene_key: "system",
- scene: "通风系统管理",
- severity: "medium",
- confidence: 88.2,
- status: "pending",
- conclusion: "主通风机电流波动幅度超过历史稳定区间。",
- recommendation: "检查风机机械状态、供电质量和风阻变化,必要时切换备用风机。",
- occurred_at: "11:18:35",
- evidence: ["activity", "plug-zap"]
- },
- "risk-reliability": {
- id: "risk-reliability",
- defect_name: "主要风路缺少可靠备用通风路径",
- defect_code: "RISK-SYS-003",
- scene_key: "system",
- scene: "通风系统管理",
- severity: "low",
- confidence: 85.9,
- status: "pending",
- conclusion: "部分主要风路在故障切换场景下缺少可靠备用路径。",
- recommendation: "复核通风网络可靠性,完善备用风路和应急调度方案。",
- occurred_at: "11:22:04",
- evidence: ["network", "file-text"]
- },
- "risk-allocation": {
- id: "risk-allocation",
- defect_name: "N1202 用风点配风偏差",
- defect_code: "RISK-AIR-002",
- scene_key: "volume",
- scene: "风量分配",
- severity: "medium",
- confidence: 88.9,
- status: "pending",
- conclusion: "实测风量与计划配风偏差超过允许范围。",
- recommendation: "核查邻近风路和风窗状态,按计划重新分配风量。",
- occurred_at: "11:25:11",
- evidence: ["gauge", "network"]
- },
- "risk-speed": {
- id: "risk-speed",
- defect_name: "回风巷风速低于规程下限",
- defect_code: "RISK-AIR-003",
- scene_key: "volume",
- scene: "风量分配",
- severity: "medium",
- confidence: 90.1,
- status: "pending",
- conclusion: "回风巷实测风速低于规程规定的最低风速。",
- recommendation: "核查巷道断面、堆积物和漏风情况,调整后复测风速。",
- occurred_at: "11:28:47",
- evidence: ["gauge", "wind"]
- },
- "risk-dust": {
- id: "risk-dust",
- defect_name: "S1302 粉尘浓度接近上限",
- defect_code: "RISK-DIS-001",
- scene_key: "disaster",
- scene: "灾害防治",
- severity: "medium",
- confidence: 89.4,
- status: "pending",
- conclusion: "采煤工作面粉尘浓度接近规定上限。",
- recommendation: "检查喷雾降尘、净化水幕和除尘风机联动运行状态。",
- occurred_at: "11:31:19",
- evidence: ["cloud-dust", "video"]
- },
- "risk-fire": {
- id: "risk-fire",
- defect_name: "采空区 CO 浓度趋势上升",
- defect_code: "RISK-DIS-002",
- scene_key: "disaster",
- scene: "灾害防治",
- severity: "high",
- confidence: 91.7,
- status: "pending",
- conclusion: "采空区 CO 浓度连续上升,存在自然发火风险。",
- recommendation: "核查灌浆、注氮和堵漏措施,持续监测 CO 与温度变化。",
- occurred_at: "11:35:02",
- evidence: ["flame", "radio-tower", "activity"]
- }
- };
- const HAZARD_AGENTS = [
- {
- id: "local-fan-state",
- sceneKey: "local",
- name: "局部通风机运行状态识别",
- description: "识别无计划停风、供电中断、风机切换异常和运行状态偏离。",
- icon: "plug-zap",
- model: "风机工况时序模型",
- cycle: "5 分钟",
- sources: ["风机工况", "供电状态", "视频画面"],
- findingIds: ["risk-local-fan"]
- },
- {
- id: "local-duct-leak",
- sceneKey: "local",
- name: "风筒漏风与末端风量识别",
- description: "识别风筒破损、接口漏风、吊挂异常以及末端风量不足。",
- icon: "wind",
- model: "风量关联分析模型",
- cycle: "10 分钟",
- sources: ["风筒状态", "末端风量", "视频智能识别"],
- findingIds: ["risk-duct-leak"]
- },
- {
- id: "local-circulation",
- sceneKey: "local",
- name: "循环风风险识别",
- description: "结合吸风口瓦斯、巷道风流、风筒位置和风量关系研判循环风。",
- icon: "refresh-cw",
- model: "循环风专家规则集",
- cycle: "5 分钟",
- sources: ["瓦斯监测", "巷道风速", "风筒位置"],
- findingIds: ["risk-circulation"]
- },
- {
- id: "facility-door",
- sceneKey: "facility",
- name: "风门状态一致性识别",
- description: "识别风门未闭合、闭锁失效、传感器与视频状态冲突。",
- icon: "door-open",
- model: "多模态状态融合模型",
- cycle: "实时",
- sources: ["风门传感器", "视频画面", "闭锁状态"],
- findingIds: ["risk-door"]
- },
- {
- id: "facility-window",
- sceneKey: "facility",
- name: "调节风窗异常识别",
- description: "识别风窗开度偏差、执行机构异常和配风响应滞后。",
- icon: "settings-2",
- model: "开度-风量回归模型",
- cycle: "10 分钟",
- sources: ["风窗开度", "计划风量", "实测风量"],
- findingIds: ["risk-window"]
- },
- {
- id: "facility-leakage",
- sceneKey: "facility",
- name: "密闭与漏风识别",
- description: "识别密闭压差异常、墙体裂隙和相邻风路异常漏风。",
- icon: "network",
- model: "压差与风量平衡模型",
- cycle: "15 分钟",
- sources: ["密闭压差", "风量平衡", "巡检记录"],
- findingIds: ["risk-seal-leak"]
- },
- {
- id: "system-resistance",
- sceneKey: "system",
- name: "通风阻力异常识别",
- description: "识别巷道阻力突增、持续上升和阻力分布失衡。",
- icon: "activity",
- model: "阻力趋势预测模型",
- cycle: "每小时",
- sources: ["风压监测", "风量数据", "阻力测定报告"],
- findingIds: ["risk-resistance"]
- },
- {
- id: "system-main-fan",
- sceneKey: "system",
- name: "主通风机运行异常识别",
- description: "识别风机电流、风压、振动和效率偏离稳定区间。",
- icon: "cpu",
- model: "主通风机健康度模型",
- cycle: "5 分钟",
- sources: ["风机工况", "电气参数", "振动监测"],
- findingIds: ["risk-main-fan"]
- },
- {
- id: "system-reliability",
- sceneKey: "system",
- name: "通风系统可靠性识别",
- description: "评估主要风路、备用路径和故障切换条件下的系统可靠性。",
- icon: "route",
- model: "通风网络可靠性模型",
- cycle: "每日",
- sources: ["通风网络", "设施状态", "应急预案"],
- findingIds: ["risk-reliability"]
- },
- {
- id: "volume-face",
- sceneKey: "volume",
- name: "采掘工作面风量不足识别",
- description: "对比计划配风和实测风量,识别采掘工作面风量不足。",
- icon: "gauge",
- model: "配风偏差识别模型",
- cycle: "10 分钟",
- sources: ["计划风量", "实测风量", "工作面状态"],
- findingIds: ["risk-volume"]
- },
- {
- id: "volume-allocation",
- sceneKey: "volume",
- name: "用风点配风偏差识别",
- description: "识别用风点风量偏差、风窗调节滞后和风量分配失衡。",
- icon: "network",
- model: "风量平衡分析模型",
- cycle: "10 分钟",
- sources: ["用风点风量", "风窗状态", "通风网络"],
- findingIds: ["risk-allocation"]
- },
- {
- id: "volume-speed",
- sceneKey: "volume",
- name: "巷道风速异常识别",
- description: "识别风速低于规程下限、风速突降和断面异常影响。",
- icon: "wind",
- model: "风速阈值与趋势模型",
- cycle: "5 分钟",
- sources: ["风速监测", "巷道断面", "历史曲线"],
- findingIds: ["risk-speed"]
- },
- {
- id: "disaster-gas",
- sceneKey: "disaster",
- name: "瓦斯超限识别",
- description: "识别瓦斯浓度超限、快速上升和异常积聚风险。",
- icon: "radio-tower",
- model: "瓦斯超限预警模型",
- cycle: "实时",
- sources: ["瓦斯监测", "风量数据", "视频画面"],
- findingIds: ["risk-gas"]
- },
- {
- id: "disaster-dust",
- sceneKey: "disaster",
- name: "粉尘与防尘异常识别",
- description: "识别粉尘浓度异常、喷雾失效和防尘设施未联动。",
- icon: "cloud-dust",
- model: "粉尘风险识别模型",
- cycle: "10 分钟",
- sources: ["粉尘监测", "喷雾状态", "视频识别"],
- findingIds: ["risk-dust"]
- },
- {
- id: "disaster-fire",
- sceneKey: "disaster",
- name: "自然发火与 CO 风险识别",
- description: "识别采空区 CO、温度和氧气浓度变化形成的自然发火风险。",
- icon: "flame",
- model: "自然发火趋势模型",
- cycle: "10 分钟",
- sources: ["CO 监测", "温度数据", "灌浆注氮记录"],
- findingIds: ["risk-fire"]
- }
- ];
- const NODE_DETAILS = {
- "main-fan": {
- title: "主通风机",
- values: [["运行频率", "49.8 Hz"], ["风量", "7,860 m³/min"], ["状态", "正常"]],
- note: "主通风机运行平稳,当前能耗较昨日下降 1.3%。",
- status: "normal"
- },
- intake: {
- title: "副井进风井",
- values: [["进风量", "7,920 m³/min"], ["温度", "18.6 ℃"], ["状态", "正常"]],
- note: "进风参数稳定,未发现异常波动。",
- status: "normal"
- },
- "air-gate": {
- title: "2# 调节风门",
- values: [["开度", "42%"], ["状态", "不一致"], ["视频置信度", "91%"]],
- note: "视频显示风门未完全闭合,传感器反馈处于开启状态,建议现场复核。",
- status: "warning"
- },
- gas: {
- title: "N1202 瓦斯监测点",
- values: [["瓦斯浓度", "0.86%"], ["报警阈值", "0.80%"], ["趋势", "上升"]],
- note: "已关联局部通风机 T-17 与 2# 调节风门,正在执行耦合诊断。",
- status: "danger"
- },
- "north-face": {
- title: "N1201 采煤工作面",
- values: [["风量", "1,356 m³/min"], ["瓦斯", "0.42%"], ["状态", "正常"]],
- note: "工作面通防参数稳定,满足当前生产计划。",
- status: "normal"
- },
- "air-volume": {
- title: "S1301 测风站",
- values: [["实测风量", "1,286 m³/min"], ["计划风量", "1,404 m³/min"], ["偏差", "-8.4%"]],
- note: "风量低于计划值,已生成调节风窗与复测任务。",
- status: "warning"
- },
- dust: {
- title: "S1302 粉尘监测点",
- values: [["粉尘浓度", "4.2 mg/m³"], ["上限", "4.0 mg/m³"], ["喷雾状态", "间歇"]],
- note: "粉尘浓度短时偏高,建议检查喷雾降尘装置运行状态。",
- status: "warning"
- },
- return: {
- title: "回风井底",
- values: [["回风量", "8,120 m³/min"], ["负压", "2.42 kPa"], ["状态", "正常"]],
- note: "回风系统运行平稳,当前阻力处于合理区间。",
- status: "normal"
- }
- };
- const BUSINESS_DOCUMENTS = {
- n1202: {
- type: "PDF",
- title: "N1202 工作面通风安全技术措施",
- category: "通风专业",
- code: "TF-CS-2026-086",
- owner: "王强",
- pages: "42 页",
- size: "2.8 MB",
- date: "2026-10-07",
- status: "抽取完成",
- confidence: 92.4,
- summary: "通过版面分析、表格识别和领域实体抽取,已完成工作面通风方式、需风量、瓦斯管理及局部通风参数等关键字段结构化。",
- fields: [
- ["工作面名称", "N1202 综采工作面", "98.6%"],
- ["通风方式", "U 型通风", "96.8%"],
- ["设计需风量", "1,404 m³/min", "94.2%"],
- ["瓦斯浓度上限", "0.80%", "97.1%"],
- ["局部通风机型号", "FBDNo6.3/2×30", "91.5%"],
- ["风筒直径", "1,000 mm", "93.8%"],
- ["测风周期", "每 10 天一次", "89.7%"],
- ["措施责任人", "王强", "95.4%"]
- ]
- },
- resistance: {
- type: "DOC",
- title: "2026 年 9 月通风阻力测定报告",
- category: "通风专业",
- code: "TF-BB-2026-091",
- owner: "赵磊",
- pages: "28 页",
- size: "1.6 MB",
- date: "2026-09-30",
- status: "抽取完成",
- confidence: 90.8,
- summary: "已抽取通风阻力测点、巷道参数、风机工况和阻力变化趋势,并与系统内历史测点完成对象挂接。",
- fields: [
- ["测定日期", "2026-09-28", "99.2%"],
- ["测定路线", "北翼回风系统", "95.7%"],
- ["最大阻力值", "2,860 Pa", "93.1%"],
- ["主通风机风量", "7,860 m³/min", "96.4%"],
- ["通风阻力增幅", "6.2%", "91.8%"],
- ["主要异常区段", "北翼回风巷", "89.6%"],
- ["测定负责人", "赵磊", "97.2%"]
- ]
- },
- airlog: {
- type: "XLS",
- title: "矿井测风记录日报表",
- category: "监测监控",
- code: "TF-BB-2026-092",
- owner: "刘伟",
- pages: "1,240 行",
- size: "856 KB",
- date: "2026-10-08",
- status: "抽取中 76%",
- confidence: 88.5,
- summary: "正在按测风站、用风地点和日期进行表格结构识别,已完成 318 个数据项中的 242 个字段抽取。",
- fields: [
- ["日报日期", "2026-10-08", "99.1%"],
- ["测风站数量", "18 个", "96.3%"],
- ["总进风量", "7,920 m³/min", "94.8%"],
- ["总回风量", "8,120 m³/min", "94.1%"],
- ["异常测点", "S1301 测风站", "87.6%"],
- ["风量偏差", "-8.4%", "92.4%"]
- ]
- },
- s1302: {
- type: "PDF",
- title: "S1302 工作面综合防尘设计",
- category: "粉尘防治",
- code: "TF-SJ-2026-034",
- owner: "陈晨",
- pages: "36 页",
- size: "3.1 MB",
- date: "2026-10-05",
- status: "等待解析",
- confidence: 0,
- summary: "资料已入库,等待执行版面解析、表格结构识别和防尘设施参数抽取。",
- fields: [
- ["工作面名称", "S1302 综采工作面", "待抽取"],
- ["防尘方式", "煤层注水 + 喷雾降尘", "待抽取"],
- ["喷雾点数量", "待识别", "待抽取"],
- ["除尘风机型号", "待识别", "待抽取"],
- ["设计负责人", "陈晨", "已识别"]
- ]
- },
- regulation: {
- type: "PDF",
- title: "矿井通风管理制度汇编",
- category: "制度管理",
- code: "TF-GC-2026-012",
- owner: "周洋",
- pages: "118 页",
- size: "5.4 MB",
- date: "2026-09-18",
- status: "抽取完成",
- confidence: 89.6,
- summary: "已完成章节结构识别、制度条款切分和责任岗位抽取,可关联通风系统管理诊断规则。",
- fields: [
- ["制度版本", "2026 版", "98.2%"],
- ["制度章节数", "12 章", "96.7%"],
- ["测风管理制度", "第 4 章", "94.3%"],
- ["瓦斯检查制度", "第 6 章", "95.1%"],
- ["责任岗位数", "28 个", "90.4%"],
- ["修订日期", "2026-09-15", "97.8%"]
- ]
- },
- acceptance: {
- type: "DOC",
- title: "南翼通风系统调整验收资料",
- category: "通风专业",
- code: "TF-YS-2026-021",
- owner: "孙敏",
- pages: "22 页",
- size: "1.2 MB",
- date: "2026-09-26",
- status: "抽取完成",
- confidence: 93.1,
- summary: "已抽取调整范围、验收指标、现场测风结果和验收结论,并与南翼通风设施台账完成关联。",
- fields: [
- ["验收对象", "南翼通风系统调整工程", "98.4%"],
- ["验收日期", "2026-09-25", "99.0%"],
- ["风量验收结果", "合格", "96.8%"],
- ["设施验收数量", "36 项", "94.2%"],
- ["遗留问题", "2 项", "91.4%"],
- ["验收负责人", "孙敏", "97.1%"]
- ]
- }
- };
- function injectIcons(root = document) {
- root.querySelectorAll("[data-icon]").forEach((element) => {
- if (element.querySelector("svg")) return;
- const icon = ICONS[element.dataset.icon] || ICONS["circle-alert"];
- element.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${icon}</svg>`;
- });
- }
- function escapeHtml(value) {
- return String(value)
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
- }
- function showToast(message, type = "success") {
- const container = document.querySelector("#toastContainer");
- if (!container) return;
- const toast = document.createElement("div");
- toast.className = "toast";
- const iconName = type === "warning" ? "triangle-alert" : "check-circle-2";
- toast.innerHTML = `<span data-icon="${iconName}"></span><span>${escapeHtml(message)}</span>`;
- container.appendChild(toast);
- injectIcons(toast);
- window.setTimeout(() => {
- toast.classList.add("removing");
- window.setTimeout(() => toast.remove(), 220);
- }, 2600);
- }
- function navigateTo(viewName, options = {}) {
- const targetView = document.querySelector(`#view-${viewName}`);
- if (!targetView) return;
- const navigationView = viewName === "business-data" ? "data" : viewName;
- document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view === targetView));
- document.querySelectorAll(".nav-item").forEach((item) => item.classList.toggle("active", item.dataset.view === navigationView));
- document.querySelector("#pageTitle").textContent = VIEW_META[viewName]?.[0] || "";
- document.querySelector("#pageSubtitle").textContent = VIEW_META[viewName]?.[1] || "";
- closeMobileSidebar();
- if (options.updateHash !== false && window.location.hash !== `#${viewName}`) {
- window.history.replaceState(null, "", `#${viewName}`);
- }
- if (!options.preserveScroll) {
- window.scrollTo({ top: 0, behavior: "smooth" });
- }
- }
- function openMobileSidebar() {
- document.querySelector("#sidebar")?.classList.add("open");
- document.querySelector("#sidebarScrim")?.classList.add("show");
- document.body.style.overflow = "hidden";
- }
- function closeMobileSidebar() {
- document.querySelector("#sidebar")?.classList.remove("open");
- document.querySelector("#sidebarScrim")?.classList.remove("show");
- document.body.style.overflow = "";
- }
- function getAnswer(question) {
- if (/风门|漏风/.test(question)) {
- return {
- text: "风门漏风排查应重点检查门框密封、闭锁装置、门板变形、墙体裂隙和传感器状态。建议结合视频与风量变化进行联合判断,确认漏风后及时封堵并复测风量。",
- sources: ["《矿井通风设施管理标准》第 5.3 节", "通风设施漏风处置案例 2024-026"]
- };
- }
- if (/循环风|局部通风机/.test(question)) {
- return {
- text: "判定局部通风机循环风,应比较局部通风机吸风口与巷道风流中的瓦斯浓度,并结合风筒出口到吸风口的距离、巷道风量和风流方向综合判断。发现循环风后应立即调整风筒位置或巷道风量。",
- sources: ["《煤矿安全规程》第 163 条", "局部通风机循环风判定专家规则 R-021"]
- };
- }
- if (/粉尘|防尘/.test(question)) {
- return {
- text: "粉尘治理应形成“监测、喷雾、净化、冲洗、个体防护”的闭环。采掘工作面需重点检查喷雾降尘、净化水幕、除尘风机和风流净化装置的联动状态,并对高浓度点位持续跟踪。",
- sources: ["《煤矿井下粉尘综合防治技术规范》", "综合防尘管理流程 QF-04"]
- };
- }
- return {
- text: "瓦斯浓度超限后,应立即停止作业、切断电源、撤出人员,并核查局部通风机、风筒和回风系统状态。处置完成后,需要复测瓦斯浓度并形成闭环记录,严禁未查明原因直接恢复生产。",
- sources: ["《煤矿安全规程》第 173 条", "瓦斯超限处置案例 2024-018"]
- };
- }
- function renderAnswer(container, question, compact = false) {
- if (!container) return;
- const answer = getAnswer(question);
- if (compact) {
- container.innerHTML = `
- <div class="answer-head"><span data-icon="bot"></span><strong>通防知识助手</strong><span>正在检索 2,164 条知识</span></div>
- <p>${escapeHtml(answer.text)}</p>
- <div class="answer-sources">${answer.sources.map((source) => `<span>${escapeHtml(source)}</span>`).join("")}</div>
- `;
- injectIcons(container);
- return;
- }
- container.innerHTML = `
- <div class="panel-head">
- <div><span class="eyebrow">智能问答</span><h2>回答与依据</h2></div>
- <span class="subtle-badge success">知识溯源已开启</span>
- </div>
- <div class="ai-answer">
- <div class="ai-avatar"><span data-icon="bot"></span></div>
- <div class="ai-answer-body">
- <p><strong>问题:</strong>${escapeHtml(question)}</p>
- <p>${escapeHtml(answer.text)}</p>
- <div class="source-citations">
- ${answer.sources
- .map(
- (source, index) =>
- `<button type="button"><span data-icon="file-text"></span>${escapeHtml(source)} <b>${96 - index * 5}%</b></button>`
- )
- .join("")}
- </div>
- </div>
- </div>
- `;
- injectIcons(container);
- }
- function openEvidenceDrawer(row) {
- const title = row.querySelector(".defect-name strong")?.textContent?.trim() || "隐患详情";
- const meta = row.querySelector(".defect-name small")?.textContent?.trim() || "实时识别";
- const confidence = row.querySelector(".confidence b")?.textContent?.trim() || "88.0%";
- const scene = row.dataset.scene || row.querySelector(".scene-tag")?.textContent?.trim() || "通防业务";
- const state = row.dataset.state || row.querySelector(".state-tag")?.textContent?.trim() || "待处理";
- const conclusion = row.dataset.conclusion || "多源数据联合分析表明,该异常具有较强业务关联性。";
- const recommendation = row.dataset.recommendation || "按照知识库处置流程完成参数复测与隐患排除。";
- const drawer = document.createElement("div");
- drawer.className = "evidence-drawer";
- drawer.innerHTML = `
- <div class="drawer-scrim" data-close-drawer></div>
- <aside class="drawer-panel" role="dialog" aria-modal="true" aria-label="隐患识别证据详情">
- <div class="drawer-head">
- <div>
- <span class="eyebrow">Hazard Evidence</span>
- <h2>隐患识别证据链</h2>
- </div>
- <button class="icon-button small" type="button" aria-label="关闭" data-close-drawer><span data-icon="x"></span></button>
- </div>
- <div class="drawer-summary">
- <div class="drawer-summary-icon"><span data-icon="triangle-alert"></span></div>
- <div>
- <div class="drawer-tags"><span>${escapeHtml(scene)}</span><span>${escapeHtml(state)}</span></div>
- <h3>${escapeHtml(title)}</h3>
- <p>${escapeHtml(meta)}</p>
- </div>
- <strong>${escapeHtml(confidence)}</strong>
- </div>
- <div class="drawer-section">
- <div class="drawer-section-head"><h3>识别结论</h3><span class="subtle-badge success">可追溯</span></div>
- <p class="drawer-conclusion">${escapeHtml(conclusion)}</p>
- </div>
- <div class="drawer-section">
- <div class="drawer-section-head"><h3>证据来源</h3><span>4 项</span></div>
- <div class="drawer-evidence-list">
- <div><span class="drawer-evidence-icon blue" data-icon="radio-tower"></span><p><strong>实时监测数据</strong><small>连续 11 分钟异常,数据完整率 100%</small></p><b>98%</b></div>
- <div><span class="drawer-evidence-icon teal" data-icon="wind"></span><p><strong>通风网络关联</strong><small>已关联上下游风路和 2 个通风设施</small></p><b>94%</b></div>
- <div><span class="drawer-evidence-icon amber" data-icon="video"></span><p><strong>视频智能识别</strong><small>识别到设施状态与传感器反馈不一致</small></p><b>91%</b></div>
- <div><span class="drawer-evidence-icon purple" data-icon="book-open"></span><p><strong>知识与案例</strong><small>命中 3 条专家规则和 2 个相似案例</small></p><b>89%</b></div>
- </div>
- </div>
- <div class="drawer-section">
- <div class="drawer-section-head"><h3>建议处置</h3></div>
- <ol class="drawer-steps">
- <li>${escapeHtml(recommendation)}</li>
- <li>按照知识库处置流程完成参数复测与隐患排除。</li>
- <li>上传复核证据,系统自动触发二次识别并向责任人反馈结果。</li>
- </ol>
- </div>
- <div class="drawer-actions">
- <button class="secondary-button" type="button" data-close-drawer>暂不处理</button>
- <button class="primary-button" type="button" data-create-task>生成整改任务</button>
- </div>
- </aside>
- `;
- document.body.appendChild(drawer);
- injectIcons(drawer);
- requestAnimationFrame(() => drawer.classList.add("open"));
- drawer.querySelectorAll("[data-close-drawer]").forEach((button) => {
- button.addEventListener("click", () => closeDrawer(drawer));
- });
- drawer.querySelector("[data-create-task]")?.addEventListener("click", () => {
- closeDrawer(drawer);
- showToast("整改任务已生成并推送至责任人");
- });
- }
- function closeDrawer(drawer) {
- drawer.classList.remove("open");
- window.setTimeout(() => drawer.remove(), 220);
- }
- function animateNumber(element, target, decimals = 0) {
- const start = Number(String(element.textContent).replaceAll(",", "")) || 0;
- const duration = 520;
- const startTime = performance.now();
- const targetNumber = Number(target);
- function frame(now) {
- const progress = Math.min((now - startTime) / duration, 1);
- const eased = 1 - Math.pow(1 - progress, 3);
- const value = start + (targetNumber - start) * eased;
- element.textContent = decimals ? value.toFixed(decimals) : Math.round(value).toLocaleString("zh-CN");
- if (progress < 1) requestAnimationFrame(frame);
- }
- requestAnimationFrame(frame);
- }
- function closeDocumentPreview() {
- const modal = document.querySelector("#documentPreviewModal");
- if (modal) modal.hidden = true;
- document.body.style.overflow = "";
- }
- function closeDocumentReprocess() {
- const modal = document.querySelector("#documentReprocessModal");
- if (modal) modal.hidden = true;
- document.body.style.overflow = "";
- }
- async function openDocumentReprocess(documentId) {
- const modal = document.querySelector("#documentReprocessModal");
- const form = document.querySelector("#documentReprocessForm");
- const templateSelect = document.querySelector("#reprocessTemplateSelect");
- const errorPanel = document.querySelector("#documentReprocessError");
- const errorText = document.querySelector("#documentReprocessErrorText");
- const documentData = window.VentAPI?.getBusinessDocument(documentId);
- if (!modal || !form || !documentData) return;
- if (
- documentData.status.includes("解析中") ||
- documentData.status.includes("抽取中")
- ) {
- showToast("资料正在处理中,请等待当前任务完成", "warning");
- return;
- }
- form.reset();
- form.elements.document_id.value = documentId;
- const templates = window.VentAPI?.state?.templates || [];
- if (templateSelect) {
- templateSelect.innerHTML =
- '<option value="">按文件名重新识别</option>' +
- templates
- .map(
- (template) =>
- `<option value="${escapeHtml(template.id)}">${escapeHtml(template.name)}</option>`
- )
- .join("");
- }
- let latestRun = null;
- try {
- latestRun = await window.VentAPI?.getDocumentProcessing?.(documentId);
- } catch {
- latestRun = null;
- }
- const parseMode = latestRun?.metadata?.parse_mode ||
- (String(latestRun?.parser_engine || "").includes("precision") ? "precision" : "lightweight");
- const parseModeInput = form.querySelector(`input[name="parse_mode"][value="${parseMode}"]`);
- if (parseModeInput) parseModeInput.checked = true;
- if (templateSelect && latestRun?.template_id) {
- templateSelect.value = latestRun.template_id;
- }
- const errorMessage = latestRun?.error || documentData.parseError || "";
- if (errorPanel && errorText) {
- errorPanel.hidden = !errorMessage;
- errorText.textContent = errorMessage;
- }
- modal.hidden = false;
- document.body.style.overflow = "hidden";
- }
- function downloadBusinessDocument(documentId) {
- if (!window.VentAPI?.getDocumentFileUrl) {
- showToast("文件下载服务不可用", "warning");
- return;
- }
- const link = document.createElement("a");
- link.href = window.VentAPI.getDocumentFileUrl(documentId, true);
- document.body.appendChild(link);
- link.click();
- link.remove();
- }
- function formatDocumentPreviewText(value) {
- const source = String(value || "");
- const parsed = new DOMParser().parseFromString(source, "text/html");
- parsed.body.querySelectorAll("td, th").forEach((cell) => {
- cell.append(parsed.createTextNode("\t"));
- });
- parsed.body
- .querySelectorAll("p, div, tr, li, h1, h2, h3, h4, h5, h6, pre, blockquote")
- .forEach((element) => {
- element.append(parsed.createTextNode("\n"));
- });
- parsed.body.querySelectorAll("br").forEach((element) => {
- element.replaceWith(parsed.createTextNode("\n"));
- });
- return (parsed.body.textContent || "")
- .replace(/[ \t]+\n/g, "\n")
- .replace(/\n[ \t]+/g, "\n")
- .replace(/\n{3,}/g, "\n\n")
- .trim();
- }
- async function openDocumentPreview(documentId) {
- const modal = document.querySelector("#documentPreviewModal");
- const body = document.querySelector("#documentPreviewBody");
- const title = document.querySelector("#documentPreviewTitle");
- const typeBadge = document.querySelector("#documentPreviewType");
- const downloadButton = document.querySelector("#documentPreviewDownload");
- if (!modal || !body || !window.VentAPI?.getDocumentPreview) return;
- modal.hidden = false;
- document.body.style.overflow = "hidden";
- if (typeBadge) typeBadge.textContent = "正在加载";
- body.innerHTML = `<div class="document-preview-empty"><span data-icon="refresh-cw"></span><strong>正在加载原文</strong><p>请稍候,系统正在读取资料内容。</p></div>`;
- injectIcons(body);
- try {
- const preview = await window.VentAPI.getDocumentPreview(documentId);
- if (title) title.textContent = preview.title || "原文预览";
- const typeLabels = {
- pdf: "PDF 原文",
- text: "解析文本",
- download_only: "仅支持下载",
- unavailable: "原文不可用"
- };
- if (typeBadge) typeBadge.textContent = typeLabels[preview.preview_type] || "原文预览";
- if (downloadButton) {
- downloadButton.disabled = !preview.can_download;
- downloadButton.dataset.documentId = preview.can_download ? documentId : "";
- }
- if (preview.preview_type === "pdf" && preview.source_available) {
- body.innerHTML = `<iframe src="${escapeHtml(preview.file_url)}#toolbar=1&navpanes=0" title="${escapeHtml(preview.file_name || preview.title)}"></iframe>`;
- return;
- }
- if (preview.text_available) {
- body.innerHTML = `<pre class="document-preview-text">${escapeHtml(formatDocumentPreviewText(preview.text))}</pre>`;
- return;
- }
- if (preview.preview_type === "download_only") {
- body.innerHTML = `
- <div class="document-preview-empty">
- <span data-icon="download"></span>
- <strong>当前格式暂不支持在线预览</strong>
- <p>可以下载原始文件后使用本机 Office 或 Excel 打开。</p>
- </div>
- `;
- injectIcons(body);
- return;
- }
- body.innerHTML = `
- <div class="document-preview-empty">
- <span data-icon="file-text"></span>
- <strong>暂无可预览内容</strong>
- <p>原始文件不存在或尚未完成解析,暂时无法展示原文。</p>
- </div>
- `;
- injectIcons(body);
- } catch (error) {
- body.innerHTML = `
- <div class="document-preview-empty">
- <span data-icon="circle-alert"></span>
- <strong>原文加载失败</strong>
- <p>${escapeHtml(error?.message || error || "请稍后重试")}</p>
- </div>
- `;
- injectIcons(body);
- }
- }
- function renderBusinessDocument(documentId) {
- const documentData = window.VentAPI?.getBusinessDocument(documentId) || BUSINESS_DOCUMENTS[documentId];
- const panel = document.querySelector("#businessDetailPanel");
- if (!documentData || !panel) return;
- const statusClass = documentData.status.includes("完成")
- ? "success"
- : documentData.status.includes("抽取中")
- ? "running"
- : "pending";
- const reprocessLabel = documentData.status.includes("失败") ? "失败重试" : "重新解析";
- panel.innerHTML = `
- <div class="panel-head">
- <div><span class="eyebrow">智能抽取</span><h2>资料解析详情</h2></div>
- <span class="business-detail-status ${statusClass}">${escapeHtml(documentData.status)}</span>
- </div>
- <div class="business-document-title">
- <span class="document-type ${documentData.type.toLowerCase()}">${escapeHtml(documentData.type)}</span>
- <div>
- <h3>${escapeHtml(documentData.title)}</h3>
- <p>${escapeHtml(documentData.code)} · ${escapeHtml(documentData.category)}</p>
- </div>
- </div>
- <div class="business-document-meta">
- <div><span>资料责任人</span><strong>${escapeHtml(documentData.owner)}</strong></div>
- <div><span>页数 / 行数</span><strong>${escapeHtml(documentData.pages)}</strong></div>
- <div><span>文件大小</span><strong>${escapeHtml(documentData.size)}</strong></div>
- <div><span>入库日期</span><strong>${escapeHtml(documentData.date)}</strong></div>
- </div>
- ${documentData.parseError
- ? `
- <div class="document-reprocess-error">
- <span data-icon="triangle-alert"></span>
- <div><strong>上次处理失败</strong><p>${escapeHtml(documentData.parseError)}</p></div>
- </div>
- `
- : ""}
- <div class="business-section">
- <div class="business-section-head">
- <h3>关键信息</h3>
- <button class="text-button" type="button" data-edit-information>编辑信息 <span data-icon="sliders-horizontal"></span></button>
- </div>
- ${documentData.fieldGroups?.length
- ? `
- <div class="business-field-groups">
- ${documentData.fieldGroups
- .map(
- (group, index) => `
- <details class="business-field-group" ${index === 0 ? "open" : ""}>
- <summary>
- <strong>${escapeHtml(group.name)}</strong>
- <span>${group.fields.length} 项</span>
- </summary>
- <div class="business-field-list">
- ${group.fields
- .map(
- (field) => `
- <div class="business-field-row" data-field-id="${escapeHtml(field.id)}">
- <span>${escapeHtml(field.label)}</span>
- <span class="business-field-value">
- <strong class="business-field-display">${escapeHtml(field.value)}</strong>
- <input class="business-field-input" type="text" value="${escapeHtml(field.value)}" data-field-id="${escapeHtml(field.id)}" data-original="${escapeHtml(field.value)}" disabled />
- </span>
- </div>
- `
- )
- .join("")}
- </div>
- </details>
- `
- )
- .join("")}
- </div>
- `
- : `
- <div class="business-field-list">
- ${(documentData.fields || [])
- .map(
- ([label, value]) => `
- <div class="business-field-row">
- <span>${escapeHtml(label)}</span>
- <strong>${escapeHtml(value)}</strong>
- </div>
- `
- )
- .join("")}
- </div>
- `}
- </div>
- <div class="business-document-note">
- <span data-icon="sparkles"></span>
- <p>${escapeHtml(documentData.summary)}</p>
- </div>
- <div class="business-detail-actions">
- <button class="secondary-button" type="button" data-preview-document><span data-icon="file-text"></span>查看原文</button>
- <button class="secondary-button" type="button" data-download-document><span data-icon="download"></span>下载原文件</button>
- <button class="secondary-button" type="button" data-reprocess-document><span data-icon="refresh-cw"></span>${escapeHtml(reprocessLabel)}</button>
- <button class="primary-button" type="button" data-toast="资料已同步至通防知识中心"><span data-icon="book-open"></span>同步知识库</button>
- </div>
- `;
- injectIcons(panel);
- panel.querySelector("[data-preview-document]")?.addEventListener("click", () => {
- openDocumentPreview(documentId);
- });
- panel.querySelector("[data-download-document]")?.addEventListener("click", () => {
- downloadBusinessDocument(documentId);
- });
- panel.querySelector("[data-reprocess-document]")?.addEventListener("click", () => {
- openDocumentReprocess(documentId);
- });
- panel.querySelectorAll("[data-toast]").forEach((button) => {
- button.addEventListener("click", () => showToast(button.dataset.toast));
- });
- const editButton = panel.querySelector("[data-edit-information]");
- const businessSection = panel.querySelector(".business-section");
- let editingInformation = false;
- editButton?.addEventListener("click", async () => {
- const inputs = [...panel.querySelectorAll(".business-field-input")];
- if (!editingInformation) {
- editingInformation = true;
- businessSection?.classList.add("editing");
- inputs.forEach((input) => {
- input.disabled = false;
- });
- editButton.innerHTML = '保存修改 <span data-icon="check-circle-2"></span>';
- injectIcons(editButton);
- return;
- }
- const changedInputs = inputs.filter((input) => input.value !== input.dataset.original);
- if (!changedInputs.length) {
- editingInformation = false;
- businessSection?.classList.remove("editing");
- inputs.forEach((input) => {
- input.disabled = true;
- });
- editButton.innerHTML = '编辑信息 <span data-icon="sliders-horizontal"></span>';
- injectIcons(editButton);
- showToast("没有需要保存的修改", "warning");
- return;
- }
- editButton.disabled = true;
- try {
- const updatedFields = await Promise.all(
- changedInputs.map((input) =>
- window.VentAPI?.updateBusinessDocumentField(
- documentId,
- input.dataset.fieldId,
- input.value
- )
- )
- );
- const extractionErrors = updatedFields.filter(
- (field) => field?.edited && field?.extraction_correct === false
- ).length;
- const pendingJudgements = updatedFields.filter(
- (field) => field?.edit_judgement_status === "pending"
- ).length;
- const revertedFields = updatedFields.filter(
- (field) => field?.edit_judgement_source === "reverted"
- ).length;
- showToast(
- revertedFields
- ? `已恢复 ${revertedFields} 项原字段值,准确率已修正`
- : pendingJudgements
- ? `已保存 ${changedInputs.length} 项修改,正在后台判定`
- : extractionErrors
- ? `已保存 ${changedInputs.length} 项修改,其中 ${extractionErrors} 项判定为抽取错误`
- : `已保存 ${changedInputs.length} 项修改`
- );
- await window.VentAPI?.refreshBusinessDocument?.(documentId);
- } catch (error) {
- editButton.disabled = false;
- showToast(`保存失败:${error?.message || error}`, "warning");
- }
- });
- }
- const hazardState = {
- initialized: false,
- activeScene: HAZARD_SCENES[0].key,
- selectedAgentId: null,
- runningAgents: new Set(),
- runs: new Map(),
- risks: new Map(),
- riskFilter: "all",
- riskScene: "all",
- runCount: 0
- };
- const HAZARD_INITIAL_RISK_IDS = [
- "risk-gas",
- "risk-local-fan",
- "risk-door",
- "risk-volume",
- "risk-resistance"
- ];
- function wait(ms) {
- return new Promise((resolve) => window.setTimeout(resolve, ms));
- }
- function getHazardScene(sceneKey) {
- return HAZARD_SCENES.find((scene) => scene.key === sceneKey) || HAZARD_SCENES[0];
- }
- function getHazardAgent(agentId) {
- return HAZARD_AGENTS.find((agent) => agent.id === agentId) || null;
- }
- function hazardSceneKeyFromName(sceneName) {
- return HAZARD_SCENES.find((scene) => scene.name === sceneName)?.key || "system";
- }
- function hazardRiskKey(risk) {
- return String(risk.defect_name || risk.defect_code || risk.id || "").trim();
- }
- function hazardAgentForRisk(risk) {
- const sceneKey = risk.scene_key || hazardSceneKeyFromName(risk.scene);
- return (
- HAZARD_AGENTS.find((agent) => agent.id === risk.source_agent_id) ||
- HAZARD_AGENTS.find((agent) => agent.sceneKey === sceneKey) ||
- HAZARD_AGENTS[0]
- );
- }
- function knownHazardRisk(risk) {
- if (risk.id && HAZARD_RISK_LIBRARY[risk.id]) {
- return HAZARD_RISK_LIBRARY[risk.id];
- }
- const name = String(risk.defect_name || "");
- const code = String(risk.defect_code || risk.code || "");
- if (name.includes("瓦斯") || code.includes("GAS")) {
- return HAZARD_RISK_LIBRARY["risk-gas"];
- }
- if (name.includes("粉尘") || name.includes("防尘") || code.includes("DUST")) {
- return HAZARD_RISK_LIBRARY["risk-dust"];
- }
- if (name.includes("风门") || name.includes("设施状态") || code.includes("FACILITY")) {
- return HAZARD_RISK_LIBRARY["risk-door"];
- }
- if (name.includes("停风") || name.includes("局部通风机") || code.includes("LOCAL")) {
- return HAZARD_RISK_LIBRARY["risk-local-fan"];
- }
- if (name.includes("阻力") || code.includes("SYS")) {
- return HAZARD_RISK_LIBRARY["risk-resistance"];
- }
- if (name.includes("风量") || code.includes("AIRFLOW") || code.includes("AIR")) {
- return name.includes("工作面")
- ? HAZARD_RISK_LIBRARY["risk-volume"]
- : HAZARD_RISK_LIBRARY["risk-allocation"];
- }
- return null;
- }
- function normalizeHazardRisk(risk, index = 0) {
- const known = knownHazardRisk(risk);
- const normalizedRisk = known
- ? {
- ...known,
- ...risk,
- id: known.id,
- defect_name: known.defect_name,
- defect_code: known.defect_code,
- scene_key: known.scene_key,
- scene: known.scene,
- evidence: known.evidence
- }
- : risk;
- const sceneKey = normalizedRisk.scene_key || hazardSceneKeyFromName(normalizedRisk.scene);
- const scene = getHazardScene(sceneKey);
- const sourceAgent = hazardAgentForRisk({ ...normalizedRisk, scene_key: sceneKey });
- const confidenceValue = Number(normalizedRisk.confidence || sourceAgent?.confidence || 88);
- const confidence = confidenceValue <= 1 ? confidenceValue * 100 : confidenceValue;
- const severity = ["high", "medium", "low"].includes(normalizedRisk.severity)
- ? normalizedRisk.severity
- : "low";
- const evidence = Array.isArray(normalizedRisk.evidence) && normalizedRisk.evidence.length
- ? normalizedRisk.evidence
- : sceneKey === "disaster"
- ? ["radio-tower", "video", "file-text"]
- : sceneKey === "volume"
- ? ["gauge", "file-text"]
- : ["radio-tower", "activity", "file-text"];
- return {
- id: String(normalizedRisk.id || `risk-${index}-${hazardRiskKey(normalizedRisk) || Date.now()}`),
- defect_name: String(normalizedRisk.defect_name || normalizedRisk.defect_code || "未命名隐患"),
- defect_code: String(normalizedRisk.defect_code || `RISK-${index + 1}`),
- scene_key: sceneKey,
- scene: normalizedRisk.scene || scene.name,
- severity,
- confidence: Math.max(0, Math.min(confidence, 100)),
- status: String(normalizedRisk.status || "pending"),
- conclusion: String(normalizedRisk.conclusion || "智能体已识别到异常特征,等待人工复核。"),
- recommendation: String(normalizedRisk.recommendation || "安排现场复核并完成处置闭环。"),
- occurred_at: String(normalizedRisk.occurred_at || "刚刚"),
- evidence,
- source_agent_id: normalizedRisk.source_agent_id || sourceAgent?.id || "",
- source_agent: normalizedRisk.source_agent || sourceAgent?.name || "隐患识别智能体",
- is_new: Boolean(normalizedRisk.is_new)
- };
- }
- function agentFindingRisks(agent) {
- if (!agent) return [];
- return (agent.findingIds || [])
- .map((riskId) => HAZARD_RISK_LIBRARY[riskId])
- .filter(Boolean)
- .map((risk, index) =>
- normalizeHazardRisk(
- {
- ...risk,
- source_agent_id: agent.id,
- source_agent: agent.name
- },
- index
- )
- );
- }
- function mergeHazardRisks(risks) {
- const incoming = Array.isArray(risks) ? risks : [];
- incoming.forEach((risk, index) => {
- const normalized = normalizeHazardRisk(risk, index);
- const key = hazardRiskKey(normalized);
- const existing = hazardState.risks.get(key);
- if (existing) {
- hazardState.risks.set(key, {
- ...existing,
- ...normalized,
- id: existing.id,
- is_new: Boolean(normalized.is_new || existing.is_new)
- });
- return;
- }
- hazardState.risks.set(key, normalized);
- });
- renderHazardCenterMeta();
- renderHazardRiskList();
- }
- function setHazardRisks(risks) {
- const incoming = Array.isArray(risks) && risks.length
- ? risks
- : HAZARD_INITIAL_RISK_IDS.map((riskId) => HAZARD_RISK_LIBRARY[riskId]);
- mergeHazardRisks(incoming);
- return [...hazardState.risks.values()];
- }
- function hazardRunMeta(agentId) {
- return hazardState.runs.get(agentId) || null;
- }
- function hazardAgentStatus(agent) {
- if (hazardState.runningAgents.has(agent.id)) {
- return { className: "running", label: "运行中" };
- }
- const run = hazardRunMeta(agent.id);
- if (!run) return { className: "", label: "待运行" };
- if (run.findings.length) {
- return { className: "risk", label: "已识别" };
- }
- return { className: "clear", label: "未发现" };
- }
- function renderHazardScenarios() {
- const grid = document.querySelector("#hazardScenarioGrid");
- if (!grid) return;
- const risks = [...hazardState.risks.values()];
- grid.innerHTML = HAZARD_SCENES.map((scene) => {
- const agentCount = HAZARD_AGENTS.filter((agent) => agent.sceneKey === scene.key).length;
- const sceneRisks = risks.filter((risk) => risk.scene_key === scene.key);
- const highCount = sceneRisks.filter((risk) => risk.severity === "high").length;
- return `
- <button class="hazard-scenario-card ${scene.key}${hazardState.activeScene === scene.key ? " active" : ""}" type="button" data-hazard-scene="${scene.key}" aria-pressed="${hazardState.activeScene === scene.key}">
- <span class="hazard-scenario-top">
- <span class="hazard-scenario-icon" data-icon="${scene.icon}"></span>
- <span class="hazard-scenario-count">${agentCount} 个智能体</span>
- </span>
- <strong>${escapeHtml(scene.name)}</strong>
- <p><span>${highCount ? `重大隐患 ${highCount}` : "暂无重大隐患"}</span><b>${sceneRisks.length}</b></p>
- </button>
- `;
- }).join("");
- injectIcons(grid);
- }
- function renderHazardAgents() {
- const grid = document.querySelector("#hazardAgentGrid");
- if (!grid) return;
- const scene = getHazardScene(hazardState.activeScene);
- const agents = HAZARD_AGENTS.filter((agent) => agent.sceneKey === scene.key);
- const countBadge = document.querySelector("#hazardSceneAgentCount");
- if (countBadge) countBadge.textContent = `${agents.length} 个智能体`;
- grid.innerHTML = agents
- .map((agent) => {
- const status = hazardAgentStatus(agent);
- const run = hazardRunMeta(agent.id);
- const findingCount = run?.findings?.length || 0;
- const sources = agent.sources
- .slice(0, 3)
- .map((source) => `<span>${escapeHtml(source)}</span>`)
- .join("");
- return `
- <article class="hazard-agent-card ${scene.key}${status.className === "running" ? " running" : ""}${run ? " completed" : ""}${hazardState.selectedAgentId === agent.id ? " selected" : ""}" data-agent-id="${agent.id}">
- <div class="hazard-agent-card-head">
- <span class="hazard-agent-icon" data-icon="${agent.icon}"></span>
- <div class="hazard-agent-title">
- <strong>${escapeHtml(agent.name)}</strong>
- <span>${escapeHtml(agent.model)}</span>
- </div>
- <span class="hazard-agent-status ${status.className}">${status.label}</span>
- </div>
- <p class="hazard-agent-description">${escapeHtml(agent.description)}</p>
- <div class="hazard-agent-meta">
- <div><span>运行周期</span><strong>${escapeHtml(agent.cycle)}</strong></div>
- <div><span>风险结果</span><strong>${findingCount ? `${findingCount} 项待核查` : "尚未识别"}</strong></div>
- </div>
- <div class="hazard-agent-sources">${sources}</div>
- <div class="hazard-agent-card-foot">
- <span>${run ? `最近运行 ${run.runAtLabel}` : "等待首次运行"}</span>
- <button class="hazard-run-button" type="button" data-run-agent="${agent.id}"${status.className === "running" ? " disabled" : ""}>
- <span data-icon="${status.className === "running" ? "refresh-cw" : "play"}"></span>
- ${status.className === "running" ? "运行中" : "运行"}
- </button>
- </div>
- </article>
- `;
- })
- .join("");
- injectIcons(grid);
- }
- function renderHazardCenterMeta() {
- const risks = [...hazardState.risks.values()];
- const counts = { high: 0, medium: 0, low: 0 };
- risks.forEach((risk) => {
- counts[risk.severity] = (counts[risk.severity] || 0) + 1;
- });
- const values = {
- hazardAgentTotal: HAZARD_AGENTS.length,
- hazardSceneTotal: HAZARD_SCENES.length,
- hazardRunCount: hazardState.runCount,
- hazardRiskTotal: risks.length,
- hazardMetricTotal: risks.length,
- hazardMetricHigh: counts.high,
- hazardMetricMedium: counts.medium,
- hazardMetricLow: counts.low
- };
- Object.entries(values).forEach(([id, value]) => {
- const element = document.querySelector(`#${id}`);
- if (element) element.textContent = String(value);
- });
- const toolbar = document.querySelector("#view-diagnosis .toolbar-copy p");
- if (toolbar) {
- toolbar.textContent = `${HAZARD_SCENES.length} 个业务场景 · ${HAZARD_AGENTS.length} 个专业智能体 · 风险清单 ${risks.length} 项`;
- }
- renderHazardScenarios();
- }
- function hazardSeverityMeta(severity) {
- if (severity === "high") return { label: "重大隐患", className: "red", rank: 0 };
- if (severity === "medium") return { label: "一般隐患", className: "yellow", rank: 1 };
- return { label: "提示风险", className: "blue", rank: 2 };
- }
- function hazardStatusMeta(status) {
- if (["completed", "closed", "verified", "已记录"].includes(status)) {
- return { label: "已记录", className: "closed" };
- }
- if (["processing", "处理中"].includes(status)) {
- return { label: "处理中", className: "processing" };
- }
- return { label: "待核查", className: "pending" };
- }
- function renderHazardRiskList() {
- const list = document.querySelector("#hazardRiskList");
- if (!list) return;
- const risks = [...hazardState.risks.values()]
- .filter((risk) => hazardState.riskFilter === "all" || risk.severity === hazardState.riskFilter)
- .filter((risk) => hazardState.riskScene === "all" || risk.scene_key === hazardState.riskScene)
- .sort((left, right) => {
- if (left.is_new !== right.is_new) return left.is_new ? -1 : 1;
- const severityDiff =
- hazardSeverityMeta(left.severity).rank - hazardSeverityMeta(right.severity).rank;
- if (severityDiff) return severityDiff;
- return String(right.occurred_at).localeCompare(String(left.occurred_at));
- });
- if (!risks.length) {
- list.innerHTML = `
- <div class="hazard-risk-empty">
- <span data-icon="shield-check"></span>
- <span>当前筛选条件下暂无风险</span>
- </div>
- `;
- injectIcons(list);
- return;
- }
- list.innerHTML = risks
- .map((risk) => {
- const scene = getHazardScene(risk.scene_key);
- const severity = hazardSeverityMeta(risk.severity);
- const status = hazardStatusMeta(risk.status);
- const confidence = Number(risk.confidence || 0);
- const evidence = (risk.evidence || [])
- .map((icon) => `<i data-icon="${escapeHtml(icon)}"></i>`)
- .join("");
- return `
- <button class="hazard-risk-row${risk.is_new ? " is-new" : ""}" type="button" data-risk-id="${escapeHtml(risk.id)}" data-scene="${escapeHtml(scene.name)}" data-state="${escapeHtml(status.label)}" data-conclusion="${escapeHtml(risk.conclusion)}" data-recommendation="${escapeHtml(risk.recommendation)}">
- <span class="defect-name"><i class="severity ${risk.severity}"></i><span><strong>${escapeHtml(risk.defect_name)}</strong><small>${escapeHtml(risk.occurred_at)} · ${escapeHtml(risk.defect_code)}</small></span></span>
- <span class="risk-severity"><em class="scene-tag ${severity.className}">${severity.label}</em></span>
- <span class="hazard-risk-source"><i class="model-dot ${scene.key === "disaster" ? "red" : scene.key === "local" ? "orange" : scene.key === "volume" ? "blue" : scene.key === "facility" ? "yellow" : "teal"}"></i><span><strong>${escapeHtml(scene.name)}</strong><small>${escapeHtml(risk.source_agent)}</small></span></span>
- <span class="hazard-risk-evidence"><span class="evidence-icons">${evidence}</span></span>
- <span class="confidence"><b>${confidence.toFixed(1)}%</b><i><em style="width: ${Math.max(0, Math.min(confidence, 100))}%"></em></i></span>
- <span class="hazard-risk-state"><em class="state-tag ${status.className}">${status.label}</em></span>
- <span class="row-action"><i data-icon="chevron-right"></i></span>
- </button>
- `;
- })
- .join("");
- injectIcons(list);
- list.querySelectorAll(".hazard-risk-row").forEach((row) => {
- row.addEventListener("click", () => openEvidenceDrawer(row));
- });
- }
- function renderHazardResultEmpty() {
- const result = document.querySelector("#hazardAgentResult");
- const status = document.querySelector("#hazardResultStatus");
- if (status) {
- status.textContent = "尚未执行";
- status.className = "subtle-badge";
- }
- if (!result) return;
- result.innerHTML = `
- <div class="hazard-result-empty">
- <span data-icon="bot"></span>
- <strong>尚未执行</strong>
- <p>当前没有可展示的识别结果。</p>
- </div>
- `;
- injectIcons(result);
- }
- function renderHazardResultLoading(agent) {
- const result = document.querySelector("#hazardAgentResult");
- const status = document.querySelector("#hazardResultStatus");
- if (status) {
- status.textContent = "运行中";
- status.className = "subtle-badge";
- }
- if (!result) return;
- result.innerHTML = `
- <div class="hazard-result-loading">
- <div>
- <span data-icon="refresh-cw"></span>
- <strong>${escapeHtml(agent.name)}</strong>
- <span>正在汇聚监测、视频、规则与历史案例</span>
- </div>
- </div>
- `;
- injectIcons(result);
- }
- function renderHazardAgentOverview(agent) {
- const result = document.querySelector("#hazardAgentResult");
- const status = document.querySelector("#hazardResultStatus");
- if (!result || !agent) return;
- if (status) {
- status.textContent = "待运行";
- status.className = "subtle-badge";
- }
- result.innerHTML = `
- <div class="hazard-result-verdict">
- <span data-icon="bot"></span>
- <div>
- <strong>${escapeHtml(agent.name)}</strong>
- <p>${escapeHtml(agent.description)}</p>
- </div>
- </div>
- <div class="hazard-result-meta"><span>模型 <b>${escapeHtml(agent.model)}</b></span><span>周期 <b>${escapeHtml(agent.cycle)}</b></span></div>
- <div class="hazard-result-checks">
- <h3>检查范围</h3>
- ${agent.sources.map((source) => `<div class="hazard-check-row"><span data-icon="database"></span><span><strong>${escapeHtml(source)}</strong><small>数据源已接入识别范围</small></span><b>就绪</b></div>`).join("")}
- </div>
- `;
- injectIcons(result);
- }
- function renderHazardResult(agent, findings, source = "demo") {
- const result = document.querySelector("#hazardAgentResult");
- const status = document.querySelector("#hazardResultStatus");
- if (!result || !agent) return;
- const hasRisk = findings.length > 0;
- const highest = findings.some((item) => item.severity === "high")
- ? "high"
- : findings.some((item) => item.severity === "medium")
- ? "medium"
- : "low";
- if (status) {
- status.textContent = hasRisk ? `${findings.length} 项风险` : "未发现隐患";
- status.className = `subtle-badge${hasRisk ? "" : " success"}`;
- }
- const checks = [
- {
- icon: "database",
- className: "",
- label: "数据完整性",
- detail: `${agent.sources.length} 类数据源完成汇聚`,
- value: "100%"
- },
- {
- icon: "cpu",
- className: "",
- label: "模型推理",
- detail: `${agent.model} 已完成计算`,
- value: "完成"
- },
- {
- icon: "file-text",
- className: "warning",
- label: "规则与知识命中",
- detail: hasRisk ? "命中隐患判定规则和关联知识" : "未命中异常规则",
- value: hasRisk ? "命中" : "正常"
- },
- {
- icon: hasRisk ? "triangle-alert" : "shield-check",
- className: hasRisk ? "danger" : "",
- label: "隐患分级",
- detail: hasRisk ? `最高等级:${hazardSeverityMeta(highest).label}` : "未形成风险项",
- value: hasRisk ? "需核查" : "通过"
- }
- ];
- result.innerHTML = `
- <div class="hazard-result-verdict ${hasRisk ? "risk" : "clear"}">
- <span data-icon="${hasRisk ? "triangle-alert" : "shield-check"}"></span>
- <div>
- <strong>${hasRisk ? `识别到 ${findings.length} 项隐患风险` : "未发现新增隐患"}</strong>
- <p>${escapeHtml(hasRisk ? findings.map((item) => item.conclusion).join(";") : "本次运行的检查项均处于正常范围。")}</p>
- </div>
- </div>
- <div class="hazard-result-meta">
- <span>智能体 <b>${escapeHtml(agent.name)}</b></span>
- <span>来源 <b>${source === "api" ? "实时数据" : "演示数据"}</b></span>
- </div>
- <div class="hazard-result-checks">
- <h3>运行检查</h3>
- ${checks.map((check) => `
- <div class="hazard-check-row ${check.className}">
- <span data-icon="${check.icon}"></span>
- <span><strong>${escapeHtml(check.label)}</strong><small>${escapeHtml(check.detail)}</small></span>
- <b>${escapeHtml(check.value)}</b>
- </div>
- `).join("")}
- </div>
- ${hasRisk ? `
- <div class="hazard-result-findings">
- <h3>识别结果</h3>
- ${findings.map((finding) => `
- <div class="hazard-result-finding">
- <div><strong>${escapeHtml(finding.defect_name)}</strong><p>${escapeHtml(finding.recommendation)}</p></div>
- <span>${hazardSeverityMeta(finding.severity).label}</span>
- </div>
- `).join("")}
- </div>
- ` : ""}
- `;
- injectIcons(result);
- }
- async function runHazardAgent(agentId, options = {}) {
- const agent = getHazardAgent(agentId);
- if (!agent || hazardState.runningAgents.has(agent.id)) return null;
- hazardState.selectedAgentId = agent.id;
- hazardState.activeScene = agent.sceneKey;
- hazardState.runningAgents.add(agent.id);
- renderHazardScenarios();
- renderHazardAgents();
- renderHazardResultLoading(agent);
- try {
- let runResult;
- if (window.VentAPI?.runHazardAgent) {
- runResult = await window.VentAPI.runHazardAgent(agent);
- } else {
- await wait(900);
- runResult = { findings: agentFindingRisks(agent), source: "demo" };
- }
- const findings = (runResult?.findings?.length
- ? runResult.findings
- : agentFindingRisks(agent)
- ).map((finding, index) =>
- normalizeHazardRisk(
- {
- ...finding,
- source_agent_id: agent.id,
- source_agent: agent.name,
- is_new: !hazardState.risks.has(hazardRiskKey(finding))
- },
- index
- )
- );
- hazardState.runs.set(agent.id, {
- findings,
- source: runResult?.source || "demo",
- runAt: new Date(),
- runAtLabel: new Date().toLocaleTimeString("zh-CN", {
- hour: "2-digit",
- minute: "2-digit",
- hour12: false
- })
- });
- hazardState.runCount += 1;
- mergeHazardRisks(findings);
- renderHazardResult(agent, findings, runResult?.source || "demo");
- renderHazardCenterMeta();
- renderHazardAgents();
- if (!options.silent) {
- window.setTimeout(() => {
- findings.forEach((finding) => {
- const riskKey = hazardRiskKey(finding);
- const risk = hazardState.risks.get(riskKey);
- if (risk) hazardState.risks.set(riskKey, { ...risk, is_new: false });
- });
- renderHazardRiskList();
- }, 5200);
- showToast(
- findings.length
- ? `${agent.name}运行完成,识别到 ${findings.length} 项隐患`
- : `${agent.name}运行完成,未发现新增隐患`
- );
- }
- return { agent, findings };
- } catch (error) {
- const fallbackFindings = agentFindingRisks(agent);
- hazardState.runs.set(agent.id, {
- findings: fallbackFindings,
- source: "demo",
- runAt: new Date(),
- runAtLabel: new Date().toLocaleTimeString("zh-CN", {
- hour: "2-digit",
- minute: "2-digit",
- hour12: false
- })
- });
- hazardState.runCount += 1;
- mergeHazardRisks(fallbackFindings);
- renderHazardResult(agent, fallbackFindings, "demo");
- renderHazardCenterMeta();
- renderHazardAgents();
- if (!options.silent) {
- showToast(`${agent.name}已使用本地识别结果,${error?.message || "实时接口不可用"}`, "warning");
- }
- return { agent, findings: fallbackFindings };
- } finally {
- hazardState.runningAgents.delete(agent.id);
- renderHazardAgents();
- }
- }
- async function runCurrentHazardScene(button) {
- const agents = HAZARD_AGENTS.filter(
- (agent) => agent.sceneKey === hazardState.activeScene
- );
- if (!agents.length) return;
- const original = button.innerHTML;
- button.disabled = true;
- button.innerHTML = '<span data-icon="refresh-cw"></span>批量运行中';
- injectIcons(button);
- let findingCount = 0;
- for (const agent of agents) {
- const result = await runHazardAgent(agent.id, { silent: true });
- findingCount += result?.findings?.length || 0;
- }
- button.disabled = false;
- button.innerHTML = original;
- injectIcons(button);
- showToast(
- findingCount
- ? `${getHazardScene(hazardState.activeScene).name}场景运行完成,识别到 ${findingCount} 项隐患`
- : `${getHazardScene(hazardState.activeScene).name}场景运行完成,未发现新增隐患`
- );
- }
- function initHazardCenter() {
- const root = document.querySelector("#view-diagnosis");
- if (!root || hazardState.initialized) return;
- hazardState.initialized = true;
- setHazardRisks(HAZARD_INITIAL_RISK_IDS.map((riskId) => HAZARD_RISK_LIBRARY[riskId]));
- renderHazardScenarios();
- renderHazardAgents();
- renderHazardResultEmpty();
- document.querySelector("#hazardScenarioGrid")?.addEventListener("click", (event) => {
- const card = event.target.closest("[data-hazard-scene]");
- if (!card) return;
- hazardState.activeScene = card.dataset.hazardScene;
- hazardState.selectedAgentId = null;
- renderHazardScenarios();
- renderHazardAgents();
- renderHazardResultEmpty();
- });
- document.querySelector("#hazardAgentGrid")?.addEventListener("click", (event) => {
- const runButton = event.target.closest("[data-run-agent]");
- if (runButton) {
- event.stopPropagation();
- runHazardAgent(runButton.dataset.runAgent);
- return;
- }
- const card = event.target.closest("[data-agent-id]");
- const agent = getHazardAgent(card?.dataset.agentId);
- if (!agent) return;
- hazardState.selectedAgentId = agent.id;
- renderHazardAgents();
- const run = hazardRunMeta(agent.id);
- if (run) {
- renderHazardResult(agent, run.findings, run.source);
- } else {
- renderHazardAgentOverview(agent);
- }
- });
- document.querySelector("#hazardRiskFilters")?.addEventListener("click", (event) => {
- const button = event.target.closest("[data-risk-filter]");
- if (!button) return;
- hazardState.riskFilter = button.dataset.riskFilter;
- document.querySelectorAll("#hazardRiskFilters button").forEach((item) => {
- item.classList.toggle("active", item === button);
- });
- renderHazardRiskList();
- });
- document.querySelector("#hazardRiskSceneFilter")?.addEventListener("change", (event) => {
- hazardState.riskScene = event.currentTarget.value;
- renderHazardRiskList();
- });
- document.querySelector("[data-run-scene-agents]")?.addEventListener("click", (event) => {
- runCurrentHazardScene(event.currentTarget);
- });
- document.querySelector("[data-export-hazards]")?.addEventListener("click", () => {
- showToast(`识别报告已生成,共包含 ${hazardState.risks.size} 项风险`);
- });
- }
- window.getHazardAgent = getHazardAgent;
- window.getHazardAgentFindings = agentFindingRisks;
- window.setHazardRisks = setHazardRisks;
- window.addHazardRisks = mergeHazardRisks;
- document.addEventListener("DOMContentLoaded", () => {
- injectIcons();
- initHazardCenter();
- document.querySelectorAll(".nav-item").forEach((item) => {
- item.addEventListener("click", () => navigateTo(item.dataset.view));
- });
- document.querySelectorAll("[data-jump-view]").forEach((button) => {
- button.addEventListener("click", (event) => {
- event.stopPropagation();
- navigateTo(button.dataset.jumpView);
- });
- });
- document.querySelectorAll("[data-open-business]").forEach((button) => {
- button.addEventListener("click", () => navigateTo("business-data"));
- });
- document.querySelectorAll("[data-open-processing-records]").forEach((button) => {
- button.addEventListener("click", () => {
- window.VentAPI?.openProcessingRecords?.();
- });
- });
- document.querySelectorAll("[data-back-view]").forEach((button) => {
- button.addEventListener("click", () => navigateTo(button.dataset.backView));
- });
- let activeBusinessCategory = "all";
- window.addEventListener("vent:business-category-changed", (event) => {
- activeBusinessCategory = event.detail?.category || "all";
- });
- const businessSearch = document.querySelector("#businessDocumentSearch");
- const businessDocumentList = document.querySelector("#businessDocumentList");
- function getBusinessRows() {
- return [...document.querySelectorAll("#businessDocumentList .document-row")];
- }
- function filterBusinessDocuments() {
- const keyword = businessSearch?.value.trim().toLowerCase() || "";
- getBusinessRows().forEach((row) => {
- const categoryMatches = activeBusinessCategory === "all" || row.dataset.category === activeBusinessCategory;
- const textMatches = !keyword || row.textContent.toLowerCase().includes(keyword);
- row.hidden = !(categoryMatches && textMatches);
- });
- }
- document.querySelectorAll("#businessCategoryList button").forEach((button) => {
- button.addEventListener("click", () => {
- activeBusinessCategory = button.dataset.docCategory;
- document.querySelectorAll("#businessCategoryList button").forEach((item) => item.classList.toggle("active", item === button));
- if (window.VentAPI?.setBusinessCategory) {
- window.VentAPI.setBusinessCategory(activeBusinessCategory);
- return;
- }
- filterBusinessDocuments();
- });
- });
- let businessSearchTimer = null;
- businessSearch?.addEventListener("input", () => {
- window.clearTimeout(businessSearchTimer);
- businessSearchTimer = window.setTimeout(() => {
- const keyword = businessSearch.value.trim();
- if (window.VentAPI?.setBusinessSearch) {
- window.VentAPI.setBusinessSearch(keyword);
- return;
- }
- filterBusinessDocuments();
- }, 350);
- });
- businessDocumentList?.addEventListener("click", (event) => {
- const row = event.target.closest(".document-row");
- if (!row || event.target.closest("[data-delete-document]")) return;
- getBusinessRows().forEach((item) => item.classList.toggle("active", item === row));
- renderBusinessDocument(row.dataset.documentId);
- });
- renderBusinessDocument("n1202");
- const initialView = window.location.hash.slice(1);
- if (initialView && VIEW_META[initialView]) {
- navigateTo(initialView, { preserveScroll: true, updateHash: false });
- }
- document.querySelector("#mobileMenu")?.addEventListener("click", openMobileSidebar);
- document.querySelector("#sidebarClose")?.addEventListener("click", closeMobileSidebar);
- document.querySelector("#sidebarScrim")?.addEventListener("click", closeMobileSidebar);
- const notificationButton = document.querySelector("#notificationButton");
- const notificationPopover = document.querySelector("#notificationPopover");
- notificationButton?.addEventListener("click", (event) => {
- event.stopPropagation();
- notificationPopover?.classList.toggle("open");
- });
- notificationPopover?.addEventListener("click", (event) => event.stopPropagation());
- document.addEventListener("click", () => notificationPopover?.classList.remove("open"));
- document.querySelector("#markRead")?.addEventListener("click", () => {
- document.querySelectorAll(".notification-item.unread").forEach((item) => item.classList.remove("unread"));
- document.querySelector("#notificationButton i")?.remove();
- showToast("消息已全部标记为已读");
- });
- document.querySelectorAll("[data-range-control] button").forEach((button) => {
- button.addEventListener("click", () => {
- const parent = button.closest("[data-range-control]");
- parent?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
- const range = button.dataset.range;
- const metric = document.querySelector('[data-metric="alerts"]');
- const cardLabel = metric?.closest(".kpi-card")?.querySelector("p");
- const values = {
- "今日": { value: 18, label: "今日告警" },
- "近7天": { value: 126, label: "近7天告警" },
- "近30天": { value: 486, label: "近30天告警" }
- };
- if (metric && values[range]) {
- animateNumber(metric, values[range].value);
- if (cardLabel) cardLabel.textContent = values[range].label;
- }
- });
- });
- document.querySelectorAll("[data-refresh]").forEach((button) => {
- button.addEventListener("click", () => {
- const svg = button.querySelector("svg");
- svg?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], {
- duration: 650,
- easing: "ease-in-out"
- });
- showToast(button.closest(".mine-map") ? "通风网络数据已刷新" : "数据已刷新");
- });
- });
- document.querySelectorAll("[data-toast]").forEach((button) => {
- button.addEventListener("click", () => showToast(button.dataset.toast));
- });
- const mapDetail = document.querySelector("#mapDetail");
- const mineMap = document.querySelector("#mineMap");
- function renderNodeDetail(nodeKey) {
- const data = NODE_DETAILS[nodeKey];
- if (!data || !mapDetail) return;
- mapDetail.innerHTML = `
- <div class="map-detail-head">
- <span class="detail-status ${data.status}"></span>
- <strong>${escapeHtml(data.title)}</strong>
- <button type="button" aria-label="关闭节点详情" id="closeMapDetail"><span data-icon="x"></span></button>
- </div>
- <div class="map-detail-values">
- ${data.values
- .map(
- ([label, value]) =>
- `<div><span>${escapeHtml(label)}</span><strong class="${value.includes("异常") || value.includes("不一致") || value === "上升" ? "text-danger" : ""}">${escapeHtml(value)}</strong></div>`
- )
- .join("")}
- </div>
- <p>${escapeHtml(data.note)}</p>
- <button class="text-button" type="button" data-jump-view="diagnosis">查看诊断详情 <span data-icon="arrow-right"></span></button>
- `;
- injectIcons(mapDetail);
- mapDetail.classList.add("show");
- mapDetail.querySelector("#closeMapDetail")?.addEventListener("click", () => mapDetail.classList.remove("show"));
- mapDetail.querySelector("[data-jump-view]")?.addEventListener("click", () => navigateTo("diagnosis"));
- }
- mineMap?.querySelectorAll(".map-node").forEach((node) => {
- const activate = () => {
- mineMap.querySelectorAll(".map-node").forEach((item) => item.classList.toggle("active", item === node));
- renderNodeDetail(node.dataset.node);
- };
- node.addEventListener("click", activate);
- node.addEventListener("keydown", (event) => {
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- activate();
- }
- });
- });
- document.querySelector("#closeMapDetail")?.addEventListener("click", () => mapDetail?.classList.remove("show"));
- document.querySelectorAll("[data-map-layer]").forEach((button) => {
- button.addEventListener("click", () => {
- button.closest(".map-toolbar")?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
- showToast(`已切换至${button.textContent.trim()}图层`);
- });
- });
- const dashboardKnowledgeForm = document.querySelector("#dashboardKnowledgeForm");
- const knowledgeConsoleForm = document.querySelector("#knowledgeConsoleForm");
- function resizeQuestionInput(field) {
- if (!field) return;
- const maxHeight = Number(field.dataset.maxHeight || 96);
- field.style.height = "auto";
- field.style.height = `${Math.min(field.scrollHeight, maxHeight)}px`;
- field.style.overflowY = field.scrollHeight > maxHeight ? "auto" : "hidden";
- }
- function bindQuestionInput(form) {
- const field = form?.querySelector('textarea[name="question"]');
- if (!field) return;
- resizeQuestionInput(field);
- field.addEventListener("input", () => resizeQuestionInput(field));
- field.addEventListener("keydown", (event) => {
- if (event.key !== "Enter" || event.shiftKey) return;
- event.preventDefault();
- form.requestSubmit();
- });
- }
- bindQuestionInput(dashboardKnowledgeForm);
- bindQuestionInput(knowledgeConsoleForm);
- function lockKnowledgeAnswerToGraph() {
- const panel = document.querySelector("#knowledgeAnswerPanel");
- const graphPanel = document.querySelector(".knowledge-grid > article:last-child");
- if (!panel || !graphPanel || panel.dataset.heightLocked === "true") return;
- const graphHeight = Math.round(graphPanel.getBoundingClientRect().height);
- if (graphHeight <= 0) return;
- panel.style.setProperty("--knowledge-answer-fixed-height", `${graphHeight}px`);
- panel.dataset.heightLocked = "true";
- }
- requestAnimationFrame(() => requestAnimationFrame(lockKnowledgeAnswerToGraph));
- dashboardKnowledgeForm?.addEventListener("submit", (event) => {
- event.preventDefault();
- const question = new FormData(dashboardKnowledgeForm).get("question")?.toString().trim();
- if (!question) return;
- renderAnswer(document.querySelector("#answerPreview"), question, true);
- });
- document.querySelectorAll("[data-question]").forEach((button) => {
- button.addEventListener("click", () => {
- const input = dashboardKnowledgeForm?.querySelector('[name="question"]');
- if (input) {
- input.value = button.dataset.question;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- }
- renderAnswer(document.querySelector("#answerPreview"), button.dataset.question, true);
- });
- });
- knowledgeConsoleForm?.addEventListener("submit", (event) => {
- event.preventDefault();
- const question = new FormData(knowledgeConsoleForm).get("question")?.toString().trim();
- if (!question) {
- showToast("请输入需要查询的通防业务问题", "warning");
- return;
- }
- const answerPanel = document.querySelector("#knowledgeAnswerPanel");
- renderAnswer(answerPanel, question);
- showToast("知识库检索完成");
- });
- const globalSearch = document.querySelector("#globalSearch");
- globalSearch?.addEventListener("keydown", (event) => {
- if (event.key !== "Enter") return;
- const value = globalSearch.value.trim();
- if (!value) return;
- navigateTo("knowledge");
- const input = knowledgeConsoleForm?.querySelector('[name="question"]');
- if (input) {
- input.value = value;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- }
- renderAnswer(document.querySelector("#knowledgeAnswerPanel"), value);
- });
- document.addEventListener("keydown", (event) => {
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
- event.preventDefault();
- globalSearch?.focus();
- }
- if (event.key === "Escape") {
- closeMobileSidebar();
- notificationPopover?.classList.remove("open");
- document.querySelector(".evidence-drawer")?.classList.remove("open");
- closeDocumentPreview();
- closeDocumentReprocess();
- window.VentAPI?.closeProcessingRecords?.();
- }
- });
- document.querySelectorAll("[data-preview-close]").forEach((element) => {
- element.addEventListener("click", closeDocumentPreview);
- });
- document.querySelector("#documentPreviewDownload")?.addEventListener("click", (event) => {
- const documentId = event.currentTarget.dataset.documentId;
- if (documentId) downloadBusinessDocument(documentId);
- });
- document.querySelectorAll("[data-reprocess-close]").forEach((element) => {
- element.addEventListener("click", closeDocumentReprocess);
- });
- document.querySelectorAll("[data-records-close]").forEach((element) => {
- element.addEventListener("click", () => window.VentAPI?.closeProcessingRecords?.());
- });
- document.querySelector("#documentReprocessForm")?.addEventListener("submit", async (event) => {
- event.preventDefault();
- const form = event.currentTarget;
- const submit = form.querySelector('[type="submit"]');
- const documentId = form.elements.document_id.value;
- if (!documentId || !window.VentAPI?.reprocessBusinessDocument) return;
- const original = submit.innerHTML;
- submit.disabled = true;
- submit.innerHTML = '<span data-icon="refresh-cw"></span>正在创建任务';
- injectIcons(submit);
- try {
- await window.VentAPI.reprocessBusinessDocument(documentId, {
- parseMode: form.elements.parse_mode.value,
- templateId: form.elements.template_id.value || null
- });
- closeDocumentReprocess();
- showToast("重新解析任务已创建");
- } catch (error) {
- showToast(`重新解析失败:${error?.message || error}`, "warning");
- } finally {
- submit.disabled = false;
- submit.innerHTML = original;
- injectIcons(submit);
- }
- });
- document.querySelectorAll("#diagnosisTabs button").forEach((button) => {
- button.addEventListener("click", () => {
- const scene = button.dataset.scene;
- document.querySelectorAll("#diagnosisTabs button").forEach((item) => item.classList.toggle("active", item === button));
- document.querySelectorAll("#diagnosisTable .diagnosis-row").forEach((row) => {
- row.hidden = scene !== "all" && row.dataset.scene !== scene;
- });
- });
- });
- document.querySelectorAll("#diagnosisTable .diagnosis-row").forEach((row) => {
- row.addEventListener("click", () => openEvidenceDrawer(row));
- });
- document.querySelector("[data-run-diagnosis]")?.addEventListener("click", (event) => {
- const button = event.currentTarget;
- const original = button.innerHTML;
- button.disabled = true;
- button.innerHTML = '<span data-icon="refresh-cw"></span>诊断进行中';
- injectIcons(button);
- button.querySelector("svg")?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], {
- duration: 900,
- iterations: 2
- });
- window.setTimeout(() => {
- button.disabled = false;
- button.innerHTML = original;
- injectIcons(button);
- showToast("全量诊断完成,识别到 18 项缺陷与风险");
- }, 1800);
- });
- document.querySelector("[data-start-evaluation]")?.addEventListener("click", (event) => {
- const button = event.currentTarget;
- const original = button.innerHTML;
- button.disabled = true;
- button.innerHTML = '<span data-icon="refresh-cw"></span>评价计算中';
- injectIcons(button);
- window.setTimeout(() => {
- button.disabled = false;
- button.innerHTML = original;
- injectIcons(button);
- showToast("综合评价已更新,综合得分 92.8,等级 A");
- }, 1600);
- });
- document.querySelectorAll(".settings-nav button").forEach((button) => {
- button.addEventListener("click", () => {
- button.closest(".settings-nav")?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
- showToast(`已切换至${button.textContent.trim()}`);
- });
- });
- window.VentAPI?.init();
- });
|