2026-08-04 00:23:55 +08:00
<!doctype html>
< html lang = "zh-CN" data-theme = "dark" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< meta name = "color-scheme" content = "dark light" >
< link rel = "icon" href = "data:," >
< title > YoVision Bell · 预警处置原型</ title >
< style >
: root {
--bg : #090d18 ;
--surface : #101725 ;
--surface-2 : #151e2e ;
--surface-3 : #1b2638 ;
--text : #f5f7fb ;
--muted : #a3b0c2 ;
--subtle : #73839a ;
--line : #29364a ;
--primary : #8b7cf6 ;
--primary-strong : #7161e8 ;
--primary-soft : rgba ( 139 , 124 , 246 , .14 );
--cyan : #38bdf8 ;
--success : #34d399 ;
--warning : #fbbf24 ;
--danger : #fb7185 ;
--shadow : 0 18 px 46 px rgba ( 0 , 0 , 0 , .25 );
--radius : 14 px ;
--sidebar : 216 px ;
--banner : 28 px ;
}
html [ data-theme = "light" ] {
--bg : #f1f4f8 ;
--surface : #ffffff ;
--surface-2 : #f8fafc ;
--surface-3 : #eaf0f6 ;
--text : #131d2c ;
--muted : #52647a ;
--subtle : #738096 ;
--line : #d2dbe6 ;
--primary-soft : rgba ( 113 , 97 , 232 , .1 );
--shadow : 0 14 px 34 px rgba ( 32 , 52 , 78 , .12 );
}
* { box-sizing : border-box ; }
2026-08-04 11:08:41 +08:00
[ hidden ] { display : none !important ; }
2026-08-04 00:23:55 +08:00
html { min-width : 320 px ; background : var ( -- bg ); }
body { margin : 0 ; min-height : 100 dvh ; background : var ( -- bg ); color : var ( -- text ); font : 14 px / 1.5 Inter , "Segoe UI" , "PingFang SC" , "Microsoft YaHei" , sans-serif ; }
button , input , select , textarea { font : inherit ; }
button , select , input [ type = "checkbox" ], input [ type = "radio" ] { cursor : pointer ; }
button : focus-visible , input : focus-visible , select : focus-visible , textarea : focus-visible , [ tabindex ] : focus-visible { outline : 3 px solid rgba ( 139 , 124 , 246 , .45 ); outline-offset : 2 px ; }
. prototype-banner { position : fixed ; z-index : 1000 ; inset : 0 0 auto 0 ; height : var ( -- banner ); display : flex ; align-items : center ; justify-content : center ; background : #f6c344 ; color : #211900 ; font-size : 12 px ; font-weight : 800 ; letter-spacing : .07 em ; }
. icon { width : 18 px ; height : 18 px ; fill : none ; stroke : currentColor ; stroke-width : 1.8 ; stroke-linecap : round ; stroke-linejoin : round ; flex : 0 0 auto ; }
. app { min-height : 100 dvh ; padding-top : var ( -- banner ); display : grid ; grid-template-columns : var ( -- sidebar ) minmax ( 0 , 1 fr ); }
. sidebar { position : sticky ; top : var ( -- banner ); z-index : 30 ; height : calc ( 100 dvh - var ( -- banner )); padding : 18 px 12 px ; border-right : 1 px solid var ( -- line ); background : var ( -- surface ); display : flex ; flex-direction : column ; }
. brand { display : flex ; align-items : center ; gap : 10 px ; padding : 2 px 10 px 20 px ; }
. brand-mark { width : 34 px ; height : 34 px ; display : grid ; place-items : center ; border-radius : 10 px ; background : linear-gradient ( 145 deg , #8b7cf6 , #38bdf8 ); color : white ; font-weight : 900 ; box-shadow : 0 8 px 22 px rgba ( 113 , 97 , 232 , .25 ); }
. brand strong { display : block ; font-size : 16 px ; }
. brand small { color : var ( -- muted ); }
. nav-label { margin : 8 px 10 px 6 px ; color : var ( -- subtle ); font-size : 11 px ; font-weight : 800 ; text-transform : uppercase ; letter-spacing : .12 em ; }
. nav { display : grid ; gap : 4 px ; }
. nav button { min-height : 44 px ; padding : 0 12 px ; border : 0 ; border-radius : 10 px ; background : transparent ; color : var ( -- muted ); display : flex ; align-items : center ; gap : 10 px ; text-align : left ; transition : .2 s ease ; }
. nav button : hover { color : var ( -- text ); background : var ( -- surface -2 ); }
. nav button [ aria-current = "page" ] { color : var ( -- text ); background : var ( -- primary - soft ); box-shadow : inset 3 px 0 var ( -- primary ); }
. nav . count { margin-left : auto ; min-width : 22 px ; padding : 1 px 6 px ; border-radius : 999 px ; background : var ( -- surface -3 ); color : var ( -- text ); text-align : center ; font-size : 12 px ; }
. shift-card { margin-top : auto ; padding : 12 px ; border : 1 px solid var ( -- line ); border-radius : 11 px ; background : var ( -- surface -2 ); }
. shift-card strong { display : block ; }
. shift-card span { color : var ( -- muted ); font-size : 12 px ; }
. shift-bar { margin-top : 9 px ; height : 5 px ; border-radius : 99 px ; background : var ( -- surface -3 ); overflow : hidden ; }
. shift-bar i { display : block ; width : 63 % ; height : 100 % ; background : var ( -- success ); }
. main { min-width : 0 ; }
. topbar { position : sticky ; top : var ( -- banner ); z-index : 20 ; min-height : 64 px ; padding : 10 px 20 px ; display : flex ; align-items : center ; gap : 12 px ; border-bottom : 1 px solid var ( -- line ); background : color-mix ( in srgb , var ( -- surface ) 94 % , transparent ); backdrop-filter : blur ( 12 px ); }
. site strong { display : block ; font-size : 15 px ; }
. site span { color : var ( -- muted ); font-size : 12 px ; }
. top-actions { margin-left : auto ; display : flex ; gap : 8 px ; align-items : center ; }
2026-08-04 09:04:52 +08:00
. btn { min-height : 40 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; padding : 8 px 13 px ; background : var ( -- surface -2 ); color : var ( -- text ); display : inline-flex ; align-items : center ; justify-content : center ; gap : 8 px ; font-weight : 750 ; text-decoration : none ; transition : .2 s ease ; }
2026-08-04 00:23:55 +08:00
. btn : hover { border-color : var ( -- primary ); }
. btn : disabled { cursor : not-allowed ; opacity : .5 ; }
. btn-primary { background : var ( -- primary - strong ); border-color : var ( -- primary - strong ); color : white ; }
. btn-success { background : #117a5d ; border-color : #159271 ; color : white ; }
. btn-danger { background : rgba ( 251 , 113 , 133 , .1 ); border-color : rgba ( 251 , 113 , 133 , .38 ); color : #ffb4c2 ; }
. btn-quiet { border-color : transparent ; background : transparent ; color : var ( -- muted ); }
. icon-btn { width : 42 px ; padding : 0 ; }
. select , . field , . textarea { width : 100 % ; min-height : 40 px ; padding : 8 px 11 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); color : var ( -- text ); }
. textarea { min-height : 80 px ; resize : vertical ; }
. top-actions . select { width : auto ; min-width : 150 px ; }
. content { padding : 18 px ; }
. view { display : none ; animation : reveal .22 s ease-out ; }
. view . active { display : block ; }
@ keyframes reveal { from { opacity : 0 ; transform : translateY ( 4 px ); } to { opacity : 1 ; transform : none ; } }
. page-head { margin-bottom : 15 px ; display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 16 px ; }
. page-head h1 { margin : 0 0 4 px ; font-size : 24 px ; line-height : 1.25 ; }
. page-head p { margin : 0 ; color : var ( -- muted ); }
. page-actions , . toolbar { display : flex ; align-items : center ; flex-wrap : wrap ; gap : 8 px ; }
. card { border : 1 px solid var ( -- line ); border-radius : var ( -- radius ); background : var ( -- surface ); box-shadow : var ( -- shadow ); }
. card-head { min-height : 52 px ; padding : 11 px 14 px ; border-bottom : 1 px solid var ( -- line ); display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; }
. card-head h2 , . card-head h3 { margin : 0 ; font-size : 15 px ; }
. card-body { padding : 14 px ; }
. badge { display : inline-flex ; align-items : center ; gap : 5 px ; min-height : 24 px ; padding : 2 px 8 px ; border : 1 px solid var ( -- line ); border-radius : 999 px ; color : var ( -- muted ); font-size : 12 px ; white-space : nowrap ; }
. badge . success { color : #78edc2 ; border-color : rgba ( 52 , 211 , 153 , .34 ); background : rgba ( 52 , 211 , 153 , .1 ); }
. badge . warning { color : #ffe18a ; border-color : rgba ( 251 , 191 , 36 , .36 ); background : rgba ( 251 , 191 , 36 , .1 ); }
. badge . danger { color : #ff9eb0 ; border-color : rgba ( 251 , 113 , 133 , .4 ); background : rgba ( 251 , 113 , 133 , .1 ); }
. badge . info { color : #a5e2ff ; border-color : rgba ( 56 , 189 , 248 , .35 ); background : rgba ( 56 , 189 , 248 , .1 ); }
. badge . violet { color : #c9c1ff ; border-color : rgba ( 139 , 124 , 246 , .38 ); background : var ( -- primary - soft ); }
. duty-grid { display : grid ; grid-template-columns : 320 px minmax ( 420 px , 1 fr ) 330 px ; gap : 12 px ; min-height : calc ( 100 dvh - 156 px ); }
. queue , . evidence , . action-panel { min-width : 0 ; overflow : hidden ; }
. queue-head { padding : 10 px ; border-bottom : 1 px solid var ( -- line ); }
. queue-tabs { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ); gap : 4 px ; padding : 4 px ; border-radius : 9 px ; background : var ( -- surface -2 ); }
. queue-tabs button { min-height : 34 px ; border : 0 ; border-radius : 7 px ; background : transparent ; color : var ( -- muted ); }
. queue-tabs button . active { background : var ( -- surface -3 ); color : var ( -- text ); }
. alert-list { padding : 8 px ; display : grid ; gap : 7 px ; max-height : calc ( 100 dvh - 265 px ); overflow-y : auto ; }
. alert-card { width : 100 % ; padding : 11 px ; border : 1 px solid transparent ; border-radius : 10 px ; background : transparent ; color : var ( -- text ); text-align : left ; transition : .18 s ease ; }
. alert-card : hover { background : var ( -- surface -2 ); }
. alert-card . selected { background : var ( -- primary - soft ); border-color : rgba ( 139 , 124 , 246 , .45 ); }
. alert-card-top , . alert-card-foot { display : flex ; align-items : center ; justify-content : space-between ; gap : 8 px ; }
. alert-card h3 { margin : 8 px 0 3 px ; font-size : 14 px ; }
. alert-card p { margin : 0 ; color : var ( -- muted ); font-size : 12 px ; }
. alert-card-foot { margin-top : 9 px ; color : var ( -- subtle ); font-size : 12 px ; }
. severity-dot { width : 8 px ; height : 8 px ; border-radius : 50 % ; background : currentColor ; }
. evidence-summary { padding : 12 px 14 px ; display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 12 px ; border-bottom : 1 px solid var ( -- line ); }
. evidence-summary h2 { margin : 0 0 5 px ; font-size : 18 px ; }
. evidence-summary p { margin : 0 ; color : var ( -- muted ); }
. evidence-tabs { display : flex ; gap : 4 px ; padding : 8 px 12 px 0 ; }
. evidence-tabs button { min-height : 36 px ; padding : 0 12 px ; border : 0 ; border-radius : 8 px 8 px 0 0 ; background : transparent ; color : var ( -- muted ); }
. evidence-tabs button . active { background : var ( -- surface -2 ); color : var ( -- text ); }
. video-stage { position : relative ; aspect-ratio : 16 / 9 ; margin : 0 12 px 12 px ; background : #02060d ; overflow : hidden ; border : 1 px solid var ( -- line ); border-radius : 10 px ; }
. video-stage svg { width : 100 % ; height : 100 % ; display : block ; }
. video-controls { position : absolute ; inset : auto 0 0 ; min-height : 44 px ; padding : 6 px 10 px ; display : flex ; align-items : center ; gap : 9 px ; color : white ; background : linear-gradient ( transparent , rgba ( 0 , 0 , 0 , .88 )); }
. video-controls button { width : 34 px ; height : 34 px ; border : 0 ; border-radius : 7 px ; background : rgba ( 255 , 255 , 255 , .08 ); color : white ; }
. timeline-bar { flex : 1 ; height : 5 px ; border-radius : 99 px ; background : rgba ( 255 , 255 , 255 , .22 ); overflow : hidden ; }
. timeline-bar i { display : block ; width : 54 % ; height : 100 % ; background : var ( -- primary ); }
. fact-grid { margin : 0 12 px 12 px ; display : grid ; grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 8 px ; }
. fact { padding : 10 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); }
. fact span { display : block ; color : var ( -- muted ); font-size : 11 px ; }
. fact strong { display : block ; margin-top : 3 px ; font-size : 13 px ; font-variant-numeric : tabular-nums ; }
. privacy-note { margin : 0 12 px 12 px ; padding : 9 px 10 px ; display : flex ; gap : 8 px ; border-left : 3 px solid var ( -- cyan ); background : rgba ( 56 , 189 , 248 , .08 ); color : var ( -- muted ); font-size : 12 px ; }
. action-scroll { max-height : calc ( 100 dvh - 210 px ); overflow-y : auto ; }
. ack-box { padding : 14 px ; border-bottom : 1 px solid var ( -- line ); }
. ack-box . btn { width : 100 % ; min-height : 48 px ; }
. ack-box p { margin : 9 px 0 0 ; color : var ( -- muted ); font-size : 12 px ; }
. countdown { display : grid ; grid-template-columns : auto 1 fr ; gap : 10 px ; align-items : center ; padding : 12 px ; margin : 12 px ; border : 1 px solid rgba ( 251 , 191 , 36 , .28 ); border-radius : 10 px ; background : rgba ( 251 , 191 , 36 , .07 ); }
. countdown strong { display : block ; font-size : 20 px ; color : #ffe18a ; font-variant-numeric : tabular-nums ; }
. countdown span { color : var ( -- muted ); font-size : 12 px ; }
. escalation { padding : 0 14 px 14 px ; }
. escalation h3 { margin : 0 0 10 px ; font-size : 14 px ; }
. step { position : relative ; padding : 0 0 16 px 27 px ; }
. step :: before { content : "" ; position : absolute ; left : 7 px ; top : 17 px ; bottom : 0 ; width : 1 px ; background : var ( -- line ); }
. step : last-child :: before { display : none ; }
. step-dot { position : absolute ; left : 0 ; top : 3 px ; width : 15 px ; height : 15 px ; border-radius : 50 % ; border : 3 px solid var ( -- surface ); background : var ( -- subtle ); box-shadow : 0 0 0 1 px var ( -- line ); }
. step . done . step-dot { background : var ( -- success ); }
. step . current . step-dot { background : var ( -- warning ); }
. step strong { display : block ; }
. step span { color : var ( -- muted ); font-size : 12 px ; }
. disposition { padding : 14 px ; border-top : 1 px solid var ( -- line ); display : grid ; gap : 9 px ; }
. disposition label { color : var ( -- muted ); font-size : 12 px ; font-weight : 700 ; }
. disposition-actions { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 8 px ; }
. grid { display : grid ; gap : 12 px ; }
. stats { grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin-bottom : 12 px ; }
. stat { padding : 15 px ; min-height : 110 px ; display : flex ; flex-direction : column ; justify-content : space-between ; }
. stat span { color : var ( -- muted ); }
. stat strong { font-size : 27 px ; font-variant-numeric : tabular-nums ; }
. search { position : relative ; min-width : 230 px ; }
. search . icon { position : absolute ; left : 11 px ; top : 11 px ; color : var ( -- subtle ); pointer-events : none ; }
. search input { padding-left : 38 px ; }
. table-wrap { overflow-x : auto ; }
table { width : 100 % ; border-collapse : collapse ; min-width : 870 px ; }
th , td { padding : 12 px 14 px ; border-bottom : 1 px solid var ( -- line ); text-align : left ; vertical-align : middle ; }
th { background : var ( -- surface -2 ); color : var ( -- muted ); font-size : 12 px ; }
td { font-variant-numeric : tabular-nums ; }
tr : last-child td { border-bottom : 0 ; }
tr : hover td { background : color-mix ( in srgb , var ( -- primary - soft ) 50 % , transparent ); }
. rule-grid { grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr )); }
. rule-card { padding : 15 px ; }
. rule-card-top { display : flex ; align-items : center ; justify-content : space-between ; gap : 8 px ; }
. rule-card h2 { margin : 12 px 0 5 px ; font-size : 16 px ; }
. rule-card p { margin : 0 0 14 px ; color : var ( -- muted ); }
. rule-card dl { margin : 0 ; display : grid ; grid-template-columns : 1 fr auto ; gap : 7 px ; color : var ( -- muted ); }
. rule-card dd { margin : 0 ; color : var ( -- text ); }
2026-08-04 09:04:52 +08:00
. rule-card-actions { margin-top : 14 px ; padding-top : 12 px ; display : flex ; flex-wrap : wrap ; gap : 7 px ; border-top : 1 px solid var ( -- line ); }
. rule-context { margin-bottom : 12 px ; padding : 11 px 13 px ; display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; border : 1 px solid rgba ( 56 , 189 , 248 , .34 ); border-radius : 10 px ; background : rgba ( 56 , 189 , 248 , .08 ); }
. rule-context [ hidden ] { display : none ; }
. rule-context p { margin : 0 ; color : var ( -- muted ); }
2026-08-04 11:08:41 +08:00
. site-grid { grid-template-columns : minmax ( 260 px , .72 fr ) minmax ( 520 px , 1.45 fr ); }
. site-list { padding : 8 px ; display : grid ; gap : 7 px ; }
. site-item { width : 100 % ; min-height : 62 px ; padding : 10 px 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); color : var ( -- text ); display : flex ; justify-content : space-between ; align-items : center ; gap : 10 px ; text-align : left ; }
. site-item . active { border-color : var ( -- primary ); background : var ( -- primary - soft ); }
. site-item small { display : block ; color : var ( -- muted ); }
. policy-note { margin : 12 px 14 px 0 ; padding : 10 px 12 px ; border-left : 3 px solid var ( -- warning ); background : rgba ( 251 , 191 , 36 , .08 ); color : var ( -- muted ); }
. context-strip { margin-bottom : 12 px ; padding : 11 px 13 px ; border : 1 px solid rgba ( 56 , 189 , 248 , .34 ); border-radius : 10 px ; background : rgba ( 56 , 189 , 248 , .08 ); display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; }
. context-strip p { margin : 0 ; color : var ( -- muted ); }
2026-08-04 09:04:52 +08:00
. linked-zone { grid-column : 1 /- 1 ; padding : 11 px ; display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface -2 ); }
. linked-zone strong , . linked-zone small { display : block ; }
. linked-zone small { color : var ( -- muted ); }
. workflow-note { grid-column : 1 /- 1 ; margin : 0 ; padding : 10 px 11 px ; border-left : 3 px solid var ( -- primary ); background : var ( -- primary - soft ); color : var ( -- muted ); font-size : 12 px ; }
2026-08-04 00:23:55 +08:00
. chain-grid { grid-template-columns : minmax ( 0 , 1.4 fr ) minmax ( 300 px , .8 fr ); }
. chain-row { padding : 13 px 14 px ; display : grid ; grid-template-columns : 40 px 1 fr auto ; gap : 11 px ; align-items : center ; border-bottom : 1 px solid var ( -- line ); }
. chain-row : last-child { border-bottom : 0 ; }
. chain-num { width : 32 px ; height : 32 px ; display : grid ; place-items : center ; border-radius : 50 % ; background : var ( -- primary - soft ); color : #cfc8ff ; font-weight : 800 ; }
. chain-row small { color : var ( -- muted ); }
. chart-grid { grid-template-columns : 1.35 fr .65 fr ; }
. chart { padding : 16 px ; }
. chart h2 { margin : 0 ; font-size : 15 px ; }
. chart p { margin : 4 px 0 14 px ; color : var ( -- muted ); }
. bars { height : 220 px ; display : flex ; align-items : end ; gap : 16 px ; padding : 10 px 6 px 26 px ; border-bottom : 1 px solid var ( -- line ); }
. bar { position : relative ; flex : 1 ; min-width : 24 px ; height : var ( -- h ); border-radius : 7 px 7 px 0 0 ; background : linear-gradient ( 180 deg , var ( -- primary ), #4f46a8 ); }
. bar span { position : absolute ; inset : calc ( 100 % + 5 px ) 0 auto ; text-align : center ; color : var ( -- muted ); font-size : 11 px ; }
. donut { width : 170 px ; height : 170 px ; margin : 18 px auto ; border-radius : 50 % ; background : conic-gradient ( var ( -- success ) 0 61 % , var ( -- warning ) 61 % 83 % , var ( -- danger ) 83 % ); position : relative ; }
. donut :: after { content : "83%\A 24h 内闭环" ; white-space : pre ; position : absolute ; inset : 27 px ; border-radius : 50 % ; display : grid ; place-items : center ; text-align : center ; background : var ( -- surface ); color : var ( -- text ); font-weight : 800 ; }
. legend { display : flex ; flex-wrap : wrap ; gap : 10 px ; color : var ( -- muted ); font-size : 12 px ; }
. legend i { display : inline-block ; width : 9 px ; height : 9 px ; margin-right : 5 px ; border-radius : 2 px ; }
. state-layer { display : none ; min-height : 420 px ; place-items : center ; padding : 24 px ; border : 1 px dashed var ( -- line ); border-radius : var ( -- radius ); background : var ( -- surface ); text-align : center ; }
. state-layer . show { display : grid ; }
. state-layer . icon { width : 40 px ; height : 40 px ; color : var ( -- primary ); }
. state-layer h2 { margin : 12 px 0 4 px ; font-size : 18 px ; }
. state-layer p { margin : 0 0 14 px ; max-width : 460 px ; color : var ( -- muted ); }
. skeleton { width : min ( 460 px , 90 % ); display : grid ; gap : 10 px ; }
. skeleton i { height : 15 px ; border-radius : 6 px ; background : linear-gradient ( 90 deg , var ( -- surface -3 ), var ( -- line ), var ( -- surface -3 )); background-size : 200 % 100 % ; animation : shimmer 1.4 s infinite ; }
. skeleton i : nth-child ( 2 ) { width : 72 % ; }
@ keyframes shimmer { to { background-position : -200 % 0 ; } }
dialog { width : min ( 610 px , calc ( 100 % - 28 px )); max-height : 88 dvh ; padding : 0 ; border : 1 px solid var ( -- line ); border-radius : 16 px ; background : var ( -- surface ); color : var ( -- text ); box-shadow : 0 24 px 80 px rgba ( 0 , 0 , 0 , .5 ); }
2026-08-04 09:04:52 +08:00
# ruleDialog { width : min ( 760 px , calc ( 100 % - 28 px )); }
2026-08-04 00:23:55 +08:00
dialog :: backdrop { background : rgba ( 4 , 7 , 15 , .68 ); backdrop-filter : blur ( 4 px ); }
. dialog-head , . dialog-foot { padding : 14 px 18 px ; border-bottom : 1 px solid var ( -- line ); display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; }
. dialog-head h2 { margin : 0 ; font-size : 17 px ; }
. dialog-body { padding : 18 px ; overflow : auto ; }
. dialog-foot { border : 0 ; border-top : 1 px solid var ( -- line ); justify-content : flex-end ; }
. form-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 12 px ; }
. form-field { display : grid ; gap : 6 px ; }
. form-field . full { grid-column : 1 /- 1 ; }
. form-field label { color : var ( -- muted ); font-size : 12 px ; font-weight : 700 ; }
2026-08-04 11:08:41 +08:00
. radio-card { display : block ; margin-bottom : 9 px ; padding : 11 px 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); }
2026-08-04 00:23:55 +08:00
. toast { position : fixed ; right : 20 px ; bottom : 20 px ; z-index : 1100 ; max-width : min ( 420 px , calc ( 100 % - 40 px )); padding : 12 px 14 px ; border : 1 px solid rgba ( 52 , 211 , 153 , .35 ); border-radius : 10 px ; background : #0b2c25 ; color : #d9fff3 ; box-shadow : var ( -- shadow ); transform : translateY ( 20 px ); opacity : 0 ; pointer-events : none ; transition : .2 s ease ; }
. toast . show { transform : none ; opacity : 1 ; }
. muted { color : var ( -- muted ); }
. mono { font-family : ui-monospace , SFMono-Regular , Consolas , monospace ; font-variant-numeric : tabular-nums ; }
. mobile-nav { display : none ; }
@ media ( max-width : 1240px ) {
: root { --sidebar : 78 px ; }
. brand-copy , . nav span : not ( . count ), . nav-label , . shift-card { display : none ; }
. brand { justify-content : center ; padding-inline : 0 ; }
. nav button { justify-content : center ; padding : 0 ; }
. nav . count { position : absolute ; margin : -24 px 0 0 25 px ; }
. duty-grid { grid-template-columns : 285 px minmax ( 390 px , 1 fr ); }
. action-panel { grid-column : 1 /- 1 ; }
2026-08-04 09:07:23 +08:00
. action-scroll { max-height : none ; display : grid ; grid-template-columns : repeat ( 3 , 1 fr ); }
. disposition { border-top : 0 ; border-left : 1 px solid var ( -- line ); }
}
@ media ( max-width : 850px ) {
: root { --sidebar : 0 px ; }
. app { display : block ; padding-bottom : 66 px ; }
. sidebar { display : none ; }
. topbar { padding : 8 px 12 px ; }
. site span , . top-actions . select { display : none ; }
. content { padding : 12 px ; }
2026-08-04 11:08:41 +08:00
. btn , . select , . field , . textarea { min-height : 44 px ; }
. icon-btn { min-width : 44 px ; }
2026-08-04 09:07:23 +08:00
. duty-grid , . chain-grid , . chart-grid { grid-template-columns : 1 fr ; }
. alert-list , . action-scroll { max-height : none ; }
. action-scroll { display : block ; }
. disposition { border-left : 0 ; border-top : 1 px solid var ( -- line ); }
. stats { grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
2026-08-04 11:08:41 +08:00
. site-grid { grid-template-columns : 1 fr ; }
2026-08-04 09:07:23 +08:00
. rule-grid { grid-template-columns : 1 fr 1 fr ; }
. fact-grid { grid-template-columns : repeat ( 2 , 1 fr ); }
. mobile-nav { position : fixed ; z-index : 50 ; left : 8 px ; right : 8 px ; bottom : 8 px ; display : grid ; grid-template-columns : repeat ( 5 , 1 fr ); padding : 5 px ; border : 1 px solid var ( -- line ); border-radius : 14 px ; background : var ( -- surface ); box-shadow : var ( -- shadow ); }
. mobile-nav button { min-height : 48 px ; border : 0 ; border-radius : 9 px ; background : transparent ; color : var ( -- muted ); display : grid ; place-items : center ; font-size : 11 px ; }
. mobile-nav button . active { color : #c9c1ff ; background : var ( -- primary - soft ); }
}
@ media ( max-width : 560px ) {
. page-head { display : grid ; }
. stats , . rule-grid { grid-template-columns : 1 fr ; }
. form-grid { grid-template-columns : 1 fr ; }
. form-field . full { grid-column : auto ; }
. video-stage { margin-inline : 8 px ; }
. fact-grid { margin-inline : 8 px ; }
. evidence-summary { display : grid ; }
. disposition-actions { grid-template-columns : 1 fr ; }
}
@ media ( prefers-reduced-motion : reduce ) { *, * :: before , * :: after { animation-duration : .01 ms !important ; animation-iteration-count : 1 !important ; transition-duration : .01 ms !important ; scroll-behavior : auto !important ; } }
</ style >
</ head >
< body >
< svg aria-hidden = "true" width = "0" height = "0" style = "position:absolute" >
< symbol id = "i-inbox" viewBox = "0 0 24 24" >< path d = "M4 4h16v16H4Z" />< path d = "m4 14 5-1 2 3h2l2-3 5 1" /></ symbol >
< symbol id = "i-event" viewBox = "0 0 24 24" >< path d = "M6 3h12v18H6z" />< path d = "M9 8h6m-6 4h6m-6 4h4" /></ symbol >
< symbol id = "i-rule" viewBox = "0 0 24 24" >< path d = "M4 6h7m6 0h3M4 12h3m6 0h7M4 18h9m6 0h1" />< circle cx = "14" cy = "6" r = "2" />< circle cx = "10" cy = "12" r = "2" />< circle cx = "16" cy = "18" r = "2" /></ symbol >
< symbol id = "i-phone" viewBox = "0 0 24 24" >< path d = "M5 4h4l2 5-3 2a16 16 0 0 0 5 5l2-3 5 2v4c-8 2-17-7-15-15Z" /></ symbol >
< symbol id = "i-chart" viewBox = "0 0 24 24" >< path d = "M4 20V9m6 11V4m6 16v-7m4 7H2" /></ symbol >
< symbol id = "i-shield" viewBox = "0 0 24 24" >< path d = "m12 3 8 3v6c0 5-3 8-8 9-5-1-8-4-8-9V6Z" />< path d = "m9 12 2 2 4-5" /></ symbol >
< symbol id = "i-search" viewBox = "0 0 24 24" >< circle cx = "11" cy = "11" r = "7" />< path d = "m20 20-4-4" /></ symbol >
< symbol id = "i-sun" viewBox = "0 0 24 24" >< circle cx = "12" cy = "12" r = "4" />< path d = "M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></ symbol >
< symbol id = "i-bell" viewBox = "0 0 24 24" >< path d = "M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9" />< path d = "M10 21h4" /></ symbol >
< symbol id = "i-check" viewBox = "0 0 24 24" >< path d = "m5 12 4 4L19 6" /></ symbol >
< symbol id = "i-alert" viewBox = "0 0 24 24" >< path d = "M12 3 2 20h20Z" />< path d = "M12 9v4m0 3v.01" /></ symbol >
< symbol id = "i-play" viewBox = "0 0 24 24" >< path d = "m8 5 11 7-11 7Z" /></ symbol >
< symbol id = "i-expand" viewBox = "0 0 24 24" >< path d = "M8 3H3v5m13-5h5v5M8 21H3v-5m13 5h5v-5" /></ symbol >
< symbol id = "i-download" viewBox = "0 0 24 24" >< path d = "M12 3v12m0 0 5-5m-5 5-5-5M4 20h16" /></ symbol >
< symbol id = "i-lock" viewBox = "0 0 24 24" >< rect x = "4" y = "10" width = "16" height = "11" rx = "2" />< path d = "M8 10V7a4 4 0 0 1 8 0v3" /></ symbol >
< symbol id = "i-clock" viewBox = "0 0 24 24" >< circle cx = "12" cy = "12" r = "9" />< path d = "M12 7v6l4 2" /></ symbol >
< symbol id = "i-more" viewBox = "0 0 24 24" >< circle cx = "5" cy = "12" r = "1" />< circle cx = "12" cy = "12" r = "1" />< circle cx = "19" cy = "12" r = "1" /></ symbol >
</ svg >
< div class = "prototype-banner" > PROTOTYPE - 仅供枚举交互,非实现依据</ div >
< div class = "app" >
< aside class = "sidebar" aria-label = "Bell 主导航" >
< div class = "brand" >< div class = "brand-mark" > Y</ div >< div class = "brand-copy" >< strong > YoVision</ strong >< small > Bell · Duty Console</ small ></ div ></ div >
< div class = "nav-label" > Operations</ div >
< nav class = "nav" >
< button data-view = "duty" aria-current = "page" >< svg class = "icon" >< use href = "#i-inbox" /></ svg >< span > 值班台</ span >< span class = "count" > 6</ span ></ button >
< button data-view = "events" >< svg class = "icon" >< use href = "#i-event" /></ svg >< span > 事件中心</ span ></ button >
< button data-view = "rules" >< svg class = "icon" >< use href = "#i-rule" /></ svg >< span > 规则策略</ span ></ button >
< button data-view = "chains" >< svg class = "icon" >< use href = "#i-phone" /></ svg >< span > 升级链</ span ></ button >
< button data-view = "reports" >< svg class = "icon" >< use href = "#i-chart" /></ svg >< span > 运营报表</ span ></ button >
2026-08-04 11:08:41 +08:00
< button data-view = "sites" >< svg class = "icon" >< use href = "#i-rule" /></ svg >< span > 站点与 Area</ span ></ button >
2026-08-04 09:07:23 +08:00
< button data-view = "audit" >< svg class = "icon" >< use href = "#i-shield" /></ svg >< span > 审计日志</ span ></ button >
</ nav >
< div class = "shift-card" >< strong > 安保值班组 · 白班</ strong >< span > 08:00– 20:00 · 4 人在线</ span >< div class = "shift-bar" >< i ></ i ></ div ></ div >
</ aside >
< main class = "main" id = "main-content" tabindex = "-1" >
< header class = "topbar" >
2026-08-04 11:08:41 +08:00
< div class = "site" >< strong > 青藤教育集团 · 青藤寄宿学校</ strong >< span > 当前账号:林工 · 租户管理员 · Asia/Shanghai</ span ></ div >
2026-08-04 09:07:23 +08:00
< div class = "top-actions" >
< select class = "select" id = "stateSelect" aria-label = "原型状态演示" >< option value = "normal" > 正常状态</ option >< option value = "loading" > 加载中</ option >< option value = "weak" > 弱网 / 证据失败</ option >< option value = "empty" > 队列为空</ option >< option value = "denied" > 无权限</ option ></ select >
< button class = "btn icon-btn" id = "themeToggle" aria-label = "切换明暗主题" title = "切换明暗主题" >< svg class = "icon" >< use href = "#i-sun" /></ svg ></ button >
< button class = "btn icon-btn" aria-label = "查看通知" title = "查看通知" >< svg class = "icon" >< use href = "#i-bell" /></ svg ></ button >
</ div >
</ header >
< div class = "content" >
< section class = "state-layer" id = "stateLayer" aria-live = "polite" ></ section >
< section class = "view active" data-page = "duty" >
< div class = "page-head" >< div >< h1 > 实时值班台</ h1 >< p > 先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。</ p ></ div >< div class = "page-actions" >< span class = "badge success" > 值班组在线</ span >< button class = "btn" id = "muteButton" > 限时静默</ button ></ div ></ div >
< div class = "duty-grid" >
< section class = "card queue" aria-label = "待处置预警队列" >
< div class = "card-head" >< h2 > 待处置预警</ h2 >< span class = "badge danger" > 6 个未 ack</ span ></ div >
2026-08-04 11:08:41 +08:00
< div class = "queue-head" >< div class = "queue-tabs" aria-label = "预警队列筛选" >< button class = "active" aria-pressed = "true" > 全部 9</ button >< button aria-pressed = "false" > 高危 3</ button >< button aria-pressed = "false" > 我的 2</ button ></ div ></ div >
2026-08-04 09:07:23 +08:00
< div class = "alert-list" id = "alertList" ></ div >
</ section >
< section class = "card evidence" aria-label = "事件事实与证据" >
< div class = "evidence-summary" >< div >< span class = "badge danger" id = "selectedSeverity" > 高危 · S1</ span >< h2 id = "selectedTitle" > 危险区域闯入</ h2 >< p id = "selectedMeta" > 北围墙 01 · 23:14:08 · EVT-01J8…7A2</ p ></ div >< button class = "btn icon-btn" aria-label = "更多事件操作" >< svg class = "icon" >< use href = "#i-more" /></ svg ></ button ></ div >
2026-08-04 11:08:41 +08:00
< div class = "evidence-tabs" aria-label = "证据类型筛选" >< button class = "active" aria-pressed = "true" > 视频片段</ button >< button aria-pressed = "false" > 抓拍 2</ button >< button aria-pressed = "false" > 结构化事实</ button ></ div >
2026-08-04 09:07:23 +08:00
< div class = "video-stage" id = "videoStage" >
< svg viewBox = "0 0 960 540" role = "img" aria-label = "北围墙 01 事件视频模拟画面,匿名目标进入危险区域" >
< defs >< linearGradient id = "bellSky" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >< stop stop-color = "#102337" />< stop offset = "1" stop-color = "#060a11" /></ linearGradient >< pattern id = "bellScan" width = "8" height = "8" patternUnits = "userSpaceOnUse" >< path d = "M0 8h8" stroke = "#fff" stroke-opacity = ".035" /></ pattern ></ defs >
< rect width = "960" height = "540" fill = "url(#bellSky)" />< path d = "M0 340 180 260 355 316 520 232 690 306 830 230 960 285V540H0Z" fill = "#142638" />< rect y = "414" width = "960" height = "126" fill = "#111820" />< path d = "M0 416h960M0 474h960" stroke = "#6d8496" stroke-opacity = ".48" />< path d = "M150 305v235m250-240v240m280-255v255m170-280v280" stroke = "#758b9d" stroke-width = "4" />< polygon points = "92,362 305,318 558,342 525,480 126,478" fill = "#fb7185" fill-opacity = ".15" stroke = "#fb7185" stroke-width = "4" stroke-dasharray = "10 7" />< rect x = "354" y = "330" width = "63" height = "148" fill = "none" stroke = "#fbbf24" stroke-width = "4" />< text x = "354" y = "319" fill = "#ffe18a" font-size = "17" > person · 0.92</ text >< circle cx = "385" cy = "310" r = "24" fill = "none" stroke = "#fbbf24" stroke-width = "4" />< rect width = "960" height = "540" fill = "url(#bellScan)" />< text x = "26" y = "38" fill = "#d6e8f5" font-size = "18" > 北围墙 01 · 事件片段</ text >< text x = "26" y = "66" fill = "#91a6b8" font-size = "14" > 事发前 8s · 事发后 12s · 匿名目标</ text >
</ svg >
< div class = "video-controls" >< button aria-label = "播放片段" >< svg class = "icon" >< use href = "#i-play" /></ svg ></ button >< span class = "mono" > 00:11 / 00:20</ span >< div class = "timeline-bar" >< i ></ i ></ div >< button aria-label = "全屏查看" >< svg class = "icon" >< use href = "#i-expand" /></ svg ></ button >< button aria-label = "下载授权证据" >< svg class = "icon" >< use href = "#i-download" /></ svg ></ button ></ div >
</ div >
< div class = "fact-grid" >
< div class = "fact" >< span > 规则</ span >< strong id = "factRule" > 非授权人员进入</ strong ></ div >< div class = "fact" >< span > 置信度</ span >< strong > 0.92</ strong ></ div >< div class = "fact" >< span > 持续时间</ span >< strong > 8.4s</ strong ></ div >< div class = "fact" >< span > 证据状态</ span >< strong style = "color:var(--success)" > 已校验</ strong ></ div >
</ div >
< div class = "privacy-note" >< svg class = "icon" >< use href = "#i-lock" /></ svg >< span > 仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。</ span ></ div >
</ section >
< aside class = "card action-panel" aria-label = "预警确认与处置" >
< div class = "card-head" >< h2 > 处置与升级</ h2 >< span class = "badge warning" id = "ackState" > 等待 ack</ span ></ div >
< div class = "action-scroll" >
< div class = "ack-box" >< button class = "btn btn-primary" id = "ackButton" >< svg class = "icon" >< use href = "#i-check" /></ svg > 确认接手</ button >< p id = "ackHelp" > 一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</ p ></ div >
< div >< div class = "countdown" >< svg class = "icon" style = "color:var(--warning);width:26px;height:26px" >< use href = "#i-clock" /></ svg >< div >< strong id = "countdown" > 01:42</ strong >< span > 未 ack 将升级到校级负责人</ span ></ div ></ div >
< div class = "escalation" >< h3 > 投递时间线</ h3 >
< div class = "step done" >< i class = "step-dot" ></ i >< strong > 值班室 Web + 本地声光</ strong >< span > 23:14:10 · 已送达</ span ></ div >
< div class = "step current" >< i class = "step-dot" ></ i >< strong > 值班员短信</ strong >< span > 23:14:13 · 已送达,未确认</ span ></ div >
< div class = "step" >< i class = "step-dot" ></ i >< strong > 校级负责人语音</ strong >< span > 预计 23:16:10 · 尚未发送</ span ></ div >
</ div >
</ div >
< div class = "disposition" >< label for = "outcome" > 处置结果</ label >< select class = "select" id = "outcome" >< option value = "" > 待选择</ option >< option > 已现场核查,无人员受伤</ option >< option > 已通知安保人员处理</ option >< option > 已转交校方负责人</ option ></ select >< label for = "note" > 处置备注</ label >< textarea class = "textarea" id = "note" placeholder = "记录已采取的动作,不改写原始事件事实" ></ textarea >< div class = "disposition-actions" >< button class = "btn btn-danger" id = "falsePositive" > 标记误报</ button >< button class = "btn btn-success" id = "completeAlert" > 完成处置</ button ></ div ></ div >
</ div >
</ aside >
</ div >
</ section >
< section class = "view" data-page = "events" >
< div class = "page-head" >< div >< h1 > 事件中心</ h1 >< p > 事件是不可变事实;预警与处置结果作为关联记录单独展示。</ p ></ div >< div class = "page-actions" >< button class = "btn" >< svg class = "icon" >< use href = "#i-download" /></ svg > 导出筛选结果</ button ></ div ></ div >
< div class = "grid stats" >
< article class = "card stat" >< span > 今日事件</ span >< strong > 47</ strong >< span class = "muted" > 较昨日 -8%</ span ></ article >< article class = "card stat" >< span > 已触发预警</ span >< strong > 18</ strong >< span class = "muted" > 规则命中后独立创建</ span ></ article >< article class = "card stat" >< span > 误报反馈</ span >< strong > 5</ strong >< span class = "muted" > 进入标注队列</ span ></ article >< article class = "card stat" >< span > 证据可用率</ span >< strong > 97.9%</ strong >< span class = "muted" > 1 个片段可重试</ span ></ article >
</ div >
< article class = "card" >< div class = "card-head" >< div class = "toolbar" >< div class = "search" >< svg class = "icon" >< use href = "#i-search" /></ svg >< input class = "field" aria-label = "搜索事件" placeholder = "事件编号、点位或规则" ></ div >< select class = "select" aria-label = "严重度筛选" >< option > 全部严重度</ option >< option > S1 高危</ option >< option > S2 重要</ option ></ select >< select class = "select" aria-label = "结果筛选" >< option > 全部结果</ option >< option > 已处置</ option >< option > 误报</ option ></ select ></ div >< span class = "muted" > 共 47 条</ span ></ div >< div class = "table-wrap" >< table >< thead >< tr >< th > 事件编号</ th >< th > 时间</ th >< th > 点位</ th >< th > 规则</ th >< th > 事实</ th >< th > 预警</ th >< th > 结果</ th >< th > 证据</ th ></ tr ></ thead >< tbody >
< tr >< td class = "mono" > EVT-01J8…7A2</ td >< td > 23:14:08</ td >< td > 北围墙 01</ td >< td > 非授权人员进入</ td >< td >< span class = "badge danger" > 已确认</ span ></ td >< td > Alert #1842</ td >< td > 处置中</ td >< td >< span class = "badge success" > 可用</ span ></ td ></ tr >
< tr >< td class = "mono" > EVT-01J8…69P</ td >< td > 22:48:31</ td >< td > 宿舍东通道</ td >< td > 区域聚集</ td >< td >< span class = "badge info" > 已记录</ span ></ td >< td > 未触发</ td >< td > 试运行样本</ td >< td >< span class = "badge success" > 可用</ span ></ td ></ tr >
< tr >< td class = "mono" > EVT-01J8…52C</ td >< td > 22:19:04</ td >< td > 实验楼入口</ td >< td > 危险区域闯入</ td >< td >< span class = "badge danger" > 已确认</ span ></ td >< td > Alert #1839</ td >< td > 误报</ td >< td >< span class = "badge warning" > 重试中</ span ></ td ></ tr >
</ tbody ></ table ></ div ></ article >
</ section >
< section class = "view" data-page = "rules" >
2026-08-04 10:05:56 +08:00
< div class = "rule-context" id = "ruleContext" hidden >< p >< strong > 已带入摄像头与区域上下文:</ strong >< span id = "ruleContextCamera" > 北围墙 01</ span > · < span id = "ruleContextZone" > ZONE-007</ span > ,可继续完成规则草稿。</ p >< button class = "btn btn-primary" id = "continueRuleDraft" > 继续配置</ button ></ div >
2026-08-04 09:07:23 +08:00
< div class = "page-head" >< div >< h1 > 规则策略</ h1 >< p > Bell 管理场景、范围、时段、持续时间、试运行与发布;检测区域来自对应摄像头的版本化空间配置。</ p ></ div >< div class = "page-actions" >< button class = "btn btn-primary" id = "openRuleDialog" > 新建规则</ button ></ div ></ div >
< div class = "grid rule-grid" >
2026-08-04 10:05:56 +08:00
< article class = "card rule-card" >< div class = "rule-card-top" >< span class = "badge success" > 正式生效</ span >< button class = "btn btn-quiet icon-btn" aria-label = "更多操作" >< svg class = "icon" >< use href = "#i-more" /></ svg ></ button ></ div >< h2 > 非授权人员进入</ h2 >< p > 危险区域 × 全天 × 持续 3 秒</ p >< dl >< dt > 覆盖范围</ dt >< dd > 4 个点位</ dd >< dt > 检测区域</ dt >< dd > 北围墙危险区域 v7</ dd >< dt > 继承来源</ dt >< dd > 站点模板</ dd >< dt > 今日命中</ dt >< dd > 8</ dd ></ dl >< div class = "rule-card-actions" >< button class = "btn" data-edit-rule > 编辑规则</ button >< a class = "btn" href = "../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule" > 校准检测区域</ a ></ div ></ article >
2026-08-04 09:07:23 +08:00
< article class = "card rule-card" >< div class = "rule-card-top" >< span class = "badge warning" id = "trialBadge" > 试运行</ span >< button class = "btn btn-quiet icon-btn" aria-label = "更多操作" >< svg class = "icon" >< use href = "#i-more" /></ svg ></ button ></ div >< h2 > 区域内聚集</ h2 >< p > 人数 ≥ 6 × 持续 20 秒 × 夜间</ p >< dl >< dt > 覆盖范围</ dt >< dd > 3 个点位</ dd >< dt > 检测区域</ dt >< dd > 3 个区域版本</ dd >< dt > 继承来源</ dt >< dd > 设备覆盖</ dd >< dt > 样本命中</ dt >< dd > 14</ dd ></ dl >< div class = "rule-card-actions" >< button class = "btn" data-edit-rule > 查看试运行</ button >< button class = "btn btn-primary" id = "publishTrialRule" > 正式发布</ button ></ div ></ article >
2026-08-04 10:05:56 +08:00
< article class = "card rule-card" >< div class = "rule-card-top" >< span class = "badge violet" > 草稿</ span >< button class = "btn btn-quiet icon-btn" aria-label = "更多操作" >< svg class = "icon" >< use href = "#i-more" /></ svg ></ button ></ div >< h2 > 方向越线</ h2 >< p > 警戒线 A → B × 上学时段</ p >< dl >< dt > 覆盖范围</ dt >< dd > 1 个点位</ dd >< dt > 检测区域</ dt >< dd > 宿舍东通道 · 警戒线 v3</ dd >< dt > 继承来源</ dt >< dd > 设备草稿</ dd >< dt > 最近修改</ dt >< dd > 2h 前</ dd ></ dl >< div class = "rule-card-actions" >< button class = "btn" data-edit-rule > 继续编辑</ button >< a class = "btn" href = "../sense/index.html?view=device-detail&camera=CAM-S2-011&zone=ZONE-011&return_to=bell-rule" > 查看警戒线</ a ></ div ></ article >
2026-08-04 00:23:55 +08:00
</ div >
</ section >
< section class = "view" data-page = "chains" >
< div class = "page-head" >< div >< h1 > 升级链</ h1 >< p > 生产前至少两条独立路径;无回执不能当作成功。</ p ></ div >< div class = "page-actions" >< button class = "btn btn-primary" > 编辑升级链</ button ></ div ></ div >
< div class = "grid chain-grid" >< article class = "card" >< div class = "card-head" >< h2 > 校园高危事件 · 默认链</ h2 >< span class = "badge success" > 已发布 v4</ span ></ div >
< div class = "chain-row" >< div class = "chain-num" > 1</ div >< div >< strong > 值班室 Web + 本地声光</ strong >< small > 立即 · 站点局域网内</ small ></ div >< span class = "badge success" > 主路径</ span ></ div >
< div class = "chain-row" >< div class = "chain-num" > 2</ div >< div >< strong > 当班值班员短信</ strong >< small > 立即 · 送达后等待 2 分钟</ small ></ div >< span class = "badge info" > 独立路径</ span ></ div >
< div class = "chain-row" >< div class = "chain-num" > 3</ div >< div >< strong > 校级负责人语音</ strong >< small > 未 ack 时升级 · 等待 3 分钟</ small ></ div >< span class = "badge warning" > 兜底</ span ></ div >
</ article >< article class = "card" >< div class = "card-head" >< h2 > 静默规则</ h2 >< span class = "badge info" > 无永久选项</ span ></ div >< div class = "card-body" >< label for = "muteDuration" class = "muted" > 维护窗口</ label >< select id = "muteDuration" class = "select" style = "margin-top:6px" >< option > 30 分钟</ option >< option > 1 小时</ option >< option > 2 小时</ option >< option > 4 小时(上限)</ option ></ select >< p class = "muted" > 静默仅影响选定规则和点位,到时自动恢复;S1 事件不可静默。</ p >< button class = "btn" > 创建维护窗口</ button ></ div ></ article ></ div >
</ section >
< section class = "view" data-page = "reports" >
< div class = "page-head" >< div >< h1 > 运营报表</ h1 >< p > 算法效果和系统 SLA 分开统计;图表同时提供表格导出。</ p ></ div >< div class = "page-actions" >< button class = "btn" >< svg class = "icon" >< use href = "#i-download" /></ svg > 导出 CSV</ button ></ div ></ div >
< div class = "grid chart-grid" >< article class = "card chart" >< h2 > 近 7 日预警量</ h2 >< p > 按站点日期汇总;可切换到每路每天误报数。</ p >< div class = "bars" role = "img" aria-label = "近七日预警量依次为 18、22、15、31、27、24、19" >< div class = "bar" style = "--h:58%" >< span > 周一</ span ></ div >< div class = "bar" style = "--h:70%" >< span > 周二</ span ></ div >< div class = "bar" style = "--h:48%" >< span > 周三</ span ></ div >< div class = "bar" style = "--h:100%" >< span > 周四</ span ></ div >< div class = "bar" style = "--h:87%" >< span > 周五</ span ></ div >< div class = "bar" style = "--h:77%" >< span > 周六</ span ></ div >< div class = "bar" style = "--h:61%" >< span > 周日</ span ></ div ></ div ></ article >< article class = "card chart" >< h2 > 处置结果</ h2 >< p > 本月已闭环事件。</ p >< div class = "donut" role = "img" aria-label = "正常处置 61%,误报 22%,升级未闭环 17%" ></ div >< div class = "legend" >< span >< i style = "background:var(--success)" ></ i > 正常处置 61%</ span >< span >< i style = "background:var(--warning)" ></ i > 误报 22%</ span >< span >< i style = "background:var(--danger)" ></ i > 未闭环 17%</ span ></ div ></ article ></ div >
</ section >
2026-08-04 11:08:41 +08:00
< section class = "view" data-page = "sites" >
< div class = "page-head" >< div >< h1 > 站点与 Area</ h1 >< p > Bell 是 Tenant、Site、Area、配额与隐私准入策略的唯一真相源;Sense 只消费版本化投影。</ p ></ div >< div class = "page-actions" >< button class = "btn" > 管理角色权限</ button >< button class = "btn btn-primary" id = "newSite" > 新建站点</ button ></ div ></ div >
< div class = "grid stats" >< article class = "card stat" >< span > 租户</ span >< strong > 1</ strong >< span class = "muted" > 青藤教育集团</ span ></ article >< article class = "card stat" >< span > 站点</ span >< strong > 2</ strong >< span class = "muted" > 1 个试点中</ span ></ article >< article class = "card stat" >< span > 视频配额</ span >< strong > 16 / 128</ strong >< span class = "muted" > 默认 16,不是硬上限</ span ></ article >< article class = "card stat" >< span > 策略投影</ span >< strong > v42</ strong >< span class = "muted" > Sense 2 分钟前已同步</ span ></ article ></ div >
< div class = "grid site-grid" >< article class = "card" >< div class = "card-head" >< h2 > 站点</ h2 >< button class = "btn btn-quiet" > 筛选</ button ></ div >< div class = "site-list" >< button class = "site-item active" >< span >< strong > 青藤寄宿学校</ strong >< small > site-ivy · S2 校园包</ small ></ span >< span class = "badge success" > 启用</ span ></ button >< button class = "site-item" >< span >< strong > 城东试验点</ strong >< small > site-east · 未绑定场景包</ small ></ span >< span class = "badge warning" > 准备中</ span ></ button ></ div ></ article >
< article class = "card" >< div class = "card-head" >< div >< h2 > 青藤寄宿学校 · Area</ h2 >< span class = "muted" > 层级、报表口径和设备准入共用同一逻辑 ID</ span ></ div >< button class = "btn btn-primary" id = "newArea" > 新建 Area</ button ></ div >< div class = "policy-note" >< strong > 策略变更不会静默处置已有设备。</ strong > 若改为“仅非成像”且 Area 内已有摄像头,必须显式迁移设备或取消变更。</ div >< div class = "table-wrap" >< table >< thead >< tr >< th > Area</ th >< th > 父级</ th >< th > capture_policy</ th >< th > 设备</ th >< th > 策略版本</ th >< th > 投影状态</ th >< th > 操作</ th ></ tr ></ thead >< tbody >< tr >< td >< strong > 北围墙</ strong >< br >< span class = "muted" > area-north-wall</ span ></ td >< td > 校园</ td >< td >< span class = "badge success" > 允许成像</ span ></ td >< td > 4 视频</ td >< td > v42</ td >< td >< span class = "badge success" > 已同步</ span ></ td >< td >< button class = "btn" data-edit-area = "north" > 编辑策略</ button ></ td ></ tr >< tr >< td >< strong > 宿舍卧室</ strong >< br >< span class = "muted" > area-dorm-room</ span ></ td >< td > 宿舍楼</ td >< td >< span class = "badge warning" > 仅非成像</ span ></ td >< td > 0 视频 · 1 雷达规划</ td >< td > v38</ td >< td >< span class = "badge success" > 已同步</ span ></ td >< td >< button class = "btn" data-edit-area = "dorm" > 编辑策略</ button ></ td ></ tr >< tr >< td >< strong > 实验楼入口</ strong >< br >< span class = "muted" > area-lab-gate</ span ></ td >< td > 实验楼</ td >< td >< span class = "badge success" > 允许成像</ span ></ td >< td > 2 视频</ td >< td > v41</ td >< td >< span class = "badge warning" > 待 Sense 拉取</ span ></ td >< td >< button class = "btn" data-edit-area = "lab" > 编辑策略</ button ></ td ></ tr ></ tbody ></ table ></ div ></ article ></ div >
</ section >
2026-08-04 00:23:55 +08:00
< section class = "view" data-page = "audit" >
2026-08-04 11:08:41 +08:00
< div class = "context-strip" id = "auditContext" hidden >< p >< strong > 来自 Sense 设备日志:</ strong > 租户 < span id = "auditTenant" > —</ span > · 站点 < span id = "auditSite" > —</ span > · 设备 < span id = "auditDevice" > —</ span > 。已锁定筛选上下文。</ p >< a class = "btn" id = "auditReturn" href = "../sense/index.html?view=device-detail" > 返回设备详情 ↗</ a ></ div >
2026-08-04 00:23:55 +08:00
< div class = "page-head" >< div >< h1 > 审计日志</ h1 >< p > 查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。</ p ></ div >< div class = "page-actions" >< button class = "btn" > 导出审计记录</ button ></ div ></ div >
2026-08-04 11:08:41 +08:00
< article class = "card" >< div class = "card-head" >< div class = "search" >< svg class = "icon" >< use href = "#i-search" /></ svg >< input class = "field" id = "auditSearch" aria-label = "搜索审计日志" placeholder = "操作、角色或对象编号" ></ div >< span class = "badge info" > 只读</ span ></ div >< div class = "table-wrap" >< table >< thead >< tr >< th > 时间</ th >< th > 角色</ th >< th > 动作</ th >< th > 对象</ th >< th > 结果</ th >< th > 来源</ th ></ tr ></ thead >< tbody id = "auditRows" >
2026-08-04 00:23:55 +08:00
< tr >< td > 23:14:18</ td >< td > 站点值班员</ td >< td > ACK_ALERT</ td >< td class = "mono" > ALT-1842</ td >< td >< span class = "badge success" > 成功</ span ></ td >< td > 值班室 Web</ td ></ tr >< tr >< td > 22:52:06</ td >< td > 站点管理员</ td >< td > PUBLISH_RULE</ td >< td class = "mono" > RULE-007</ td >< td >< span class = "badge success" > 成功</ span ></ td >< td > 管理端</ td ></ tr >< tr >< td > 22:20:31</ td >< td > 只读用户</ td >< td > VIEW_EVIDENCE</ td >< td class = "mono" > EVT-01J8…52C</ td >< td >< span class = "badge danger" > 拒绝</ span ></ td >< td > 响应式 H5</ td ></ tr >
</ tbody ></ table ></ div ></ article >
</ section >
</ div >
</ main >
</ div >
< nav class = "mobile-nav" aria-label = "移动端主导航" >< button class = "active" data-view = "duty" > 值班</ button >< button data-view = "events" > 事件</ button >< button data-view = "rules" > 规则</ button >< button data-view = "chains" > 升级</ button >< button data-view = "reports" > 报表</ button ></ nav >
2026-08-04 09:04:52 +08:00
< dialog id = "ruleDialog" aria-labelledby = "ruleTitle" >< form method = "dialog" >< div class = "dialog-head" >< div >< h2 id = "ruleTitle" > 配置告警规则</ h2 >< span class = "muted" > 规则草稿 · 尚未正式生效</ span ></ div >< button class = "btn btn-quiet icon-btn" value = "cancel" aria-label = "关闭对话框" > × </ button ></ div >< div class = "dialog-body" >< div class = "form-grid" >
< div class = "form-field" >< label for = "ruleScene" > 场景模板</ label >< select id = "ruleScene" class = "select" >< option > 非授权人员进入</ option >< option > 区域内聚集</ option >< option > 方向越线</ option ></ select ></ div >
< div class = "form-field" >< label for = "ruleScope" > 继承与覆盖</ label >< select id = "ruleScope" class = "select" >< option > 当前设备覆盖</ option >< option > 继承站点模板</ option >< option > 覆盖站点模板</ option ></ select ></ div >
< div class = "form-field" >< label for = "ruleCamera" > 摄像头</ label >< select id = "ruleCamera" class = "select" >< option value = "CAM-S2-001" > 北围墙 01</ option >< option value = "CAM-S2-006" > 实验楼入口 02</ option >< option value = "CAM-S2-011" > 宿舍东通道</ option ></ select ></ div >
2026-08-04 10:05:56 +08:00
< div class = "form-field" >< label for = "ruleZone" > 检测区域</ label >< select id = "ruleZone" class = "select" >< option value = "ZONE-007" > 北围墙危险区域 · v7</ option >< option value = "ZONE-011" > 北围墙方向警戒线 · v3</ option ></ select ></ div >
< div class = "linked-zone" >< span >< strong > 空间坐标由摄像头详情维护</ strong >< small > 区域版本变化不会绕过规则试运行或自动正式发布。</ small ></ span >< a class = "btn" id = "editRuleZone" href = "../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule" > 编辑检测区域</ a ></ div >
2026-08-04 09:04:52 +08:00
< div class = "form-field" >< label for = "ruleSchedule" > 生效时段</ label >< select id = "ruleSchedule" class = "select" >< option > 全天</ option >< option > 夜间 22:00– 06:00</ option >< option > 自定义课表</ option ></ select ></ div >
< div class = "form-field" >< label for = "ruleDuration" > 持续时间</ label >< select id = "ruleDuration" class = "select" >< option > 超过 3 秒</ option >< option > 立即</ option >< option > 超过 10 秒</ option >< option > 超过 20 秒</ option ></ select ></ div >
< div class = "form-field full" >< label >< input id = "startDryRun" type = "checkbox" checked aria-label = "保存后开始试运行" > 保存后开始试运行,不触发正式预警</ label ></ div >
< p class = "workflow-note" > 产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。</ p >
</ div ></ div >< div class = "dialog-foot" >< button class = "btn" value = "cancel" > 取消</ button >< button class = "btn btn-primary" id = "saveRuleDraft" value = "default" > 保存并开始试运行</ button ></ div ></ form ></ dialog >
2026-08-04 00:23:55 +08:00
< dialog id = "falseDialog" aria-labelledby = "falseTitle" >< form method = "dialog" >< div class = "dialog-head" >< h2 id = "falseTitle" > 标记为误报</ h2 >< button class = "btn btn-quiet icon-btn" value = "cancel" aria-label = "关闭对话框" > × </ button ></ div >< div class = "dialog-body" >< p > 这只会追加事件 outcome,不会修改原始事件事实和证据。</ p >< div class = "form-grid" >< div class = "form-field full" >< label for = "falseReason" > 误报原因</ label >< select id = "falseReason" class = "select" required >< option value = "" > 请选择</ option >< option > 区域配置不准确</ option >< option > 目标分类错误</ option >< option > 时段策略不准确</ option >< option > 其他</ option ></ select ></ div >< div class = "form-field full" >< label for = "falseNote" > 补充说明(可选)</ label >< textarea id = "falseNote" class = "textarea" placeholder = "便于算法团队归因,不填写真实个人信息" ></ textarea ></ div >< div class = "form-field full" >< label >< input type = "checkbox" checked > 进入标注复核队列</ label ></ div ></ div ></ div >< div class = "dialog-foot" >< button class = "btn" value = "cancel" > 取消</ button >< button class = "btn btn-danger" id = "confirmFalse" value = "default" > 确认标记</ button ></ div ></ form ></ dialog >
< dialog id = "muteDialog" aria-labelledby = "muteTitle" >< form method = "dialog" >< div class = "dialog-head" >< h2 id = "muteTitle" > 创建限时静默</ h2 >< button class = "btn btn-quiet icon-btn" value = "cancel" aria-label = "关闭对话框" > × </ button ></ div >< div class = "dialog-body" >< div class = "form-grid" >< div class = "form-field" >< label for = "muteScope" > 影响范围</ label >< select id = "muteScope" class = "select" >< option > 北围墙 01 · 当前规则</ option >< option > 北围墙区域</ option ></ select ></ div >< div class = "form-field" >< label for = "muteTime" > 持续时间</ label >< select id = "muteTime" class = "select" >< option > 30 分钟</ option >< option > 1 小时</ option >< option > 2 小时</ option >< option > 4 小时(上限)</ option ></ select ></ div >< div class = "form-field full" >< label for = "muteCause" > 原因</ label >< textarea id = "muteCause" class = "textarea" required placeholder = "例如:现场施工维护,已安排人工巡查" ></ textarea ></ div ></ div >< p class = "muted" > 到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。</ p ></ div >< div class = "dialog-foot" >< button class = "btn" value = "cancel" > 取消</ button >< button class = "btn btn-primary" id = "confirmMute" value = "default" > 确认并记录审计</ button ></ div ></ form ></ dialog >
2026-08-04 11:08:41 +08:00
< dialog id = "areaDialog" aria-labelledby = "areaTitle" >< form method = "dialog" >< div class = "dialog-head" >< h2 id = "areaTitle" > 编辑 Area 准入策略</ h2 >< button class = "btn btn-quiet icon-btn" value = "cancel" aria-label = "关闭对话框" > × </ button ></ div >< div class = "dialog-body" >< div class = "form-grid" >< div class = "form-field" >< label for = "areaName" > Area</ label >< input id = "areaName" class = "field" value = "北围墙" ></ div >< div class = "form-field" >< label for = "areaParent" > 父级</ label >< select id = "areaParent" class = "select" >< option > 校园</ option >< option > 教学区</ option >< option > 宿舍楼</ option ></ select ></ div >< div class = "form-field full" >< label for = "areaPolicy" > capture_policy</ label >< select id = "areaPolicy" class = "select" >< option value = "video_allowed" > 允许成像设备</ option >< option value = "non_imaging_only" > 仅允许非成像设备</ option ></ select ></ div >< div class = "form-field full" >< label for = "areaReason" > 变更原因</ label >< textarea id = "areaReason" class = "textarea" placeholder = "必填,进入全局审计" ></ textarea ></ div ></ div >< p class = "muted" > 保存生成新策略版本;Sense 投影同步前会显示旧版本,不允许绕过后端准入检查。</ p ></ div >< div class = "dialog-foot" >< button class = "btn" value = "cancel" > 取消</ button >< button type = "button" class = "btn btn-primary" id = "saveAreaPolicy" > 保存策略版本</ button ></ div ></ form ></ dialog >
< dialog id = "policyConflictDialog" aria-labelledby = "policyConflictTitle" >< form method = "dialog" >< div class = "dialog-head" >< h2 id = "policyConflictTitle" > 策略与已有设备冲突</ h2 >< button class = "btn btn-quiet icon-btn" value = "cancel" aria-label = "关闭对话框" > × </ button ></ div >< div class = "dialog-body" >< p > 北围墙当前有 4 台成像设备。改为“仅非成像”前必须选择显式处置;系统不会自动停用、移动或保留违规设备。</ p >< label class = "radio-card" >< input type = "radio" name = "conflictAction" value = "move" > < strong > 先迁移 4 台设备到允许成像的 Area</ strong >< br >< span class = "muted" > 创建迁移任务,全部成功后再发布策略。</ span ></ label >< label class = "radio-card" >< input type = "radio" name = "conflictAction" value = "cancel" checked > < strong > 取消本次策略变更</ strong >< br >< span class = "muted" > 保持 v42 和现有设备不变。</ span ></ label ></ div >< div class = "dialog-foot" >< button class = "btn" value = "cancel" > 返回编辑</ button >< button class = "btn btn-primary" value = "default" id = "confirmPolicyConflict" > 确认选择</ button ></ div ></ form ></ dialog >
2026-08-04 00:23:55 +08:00
< div class = "toast" id = "toast" role = "status" aria-live = "polite" ></ div >
< script >
const $ = ( selector , root = document ) => root . querySelector ( selector );
const $$ = ( selector , root = document ) => [... root . querySelectorAll ( selector )];
const toast = $ ( '#toast' ); let toastTimer ;
function notify ( message ) { toast . textContent = message ; toast . classList . add ( 'show' ); clearTimeout ( toastTimer ); toastTimer = setTimeout (() => toast . classList . remove ( 'show' ), 3800 ); }
const alerts = [
{ id : 'ALT-1842' , severity : 'S1 高危' , badge : 'danger' , title : '危险区域闯入' , site : '北围墙 01' , time : '23:14:08' , age : '1 分钟' , rule : '非授权人员进入' , delivery : '短信已送达' },
{ id : 'ALT-1841' , severity : 'S1 高危' , badge : 'danger' , title : '方向越线' , site : '宿舍东通道' , time : '23:12:46' , age : '3 分钟' , rule : '夜间越线 A → B' , delivery : '声光已触发' },
{ id : 'ALT-1839' , severity : 'S2 重要' , badge : 'warning' , title : '区域聚集' , site : '操场西北角' , time : '23:08:21' , age : '7 分钟' , rule : '人数 ≥ 6 持续 20 秒' , delivery : 'Web 已看到' },
{ id : 'ALT-1836' , severity : 'S2 重要' , badge : 'warning' , title : '危险区域滞留' , site : '配电房门口' , time : '22:57:03' , age : '18 分钟' , rule : '区域内停留 ≥ 10 秒' , delivery : '等待短信回执' }
];
const alertList = $ ( '#alertList' );
function renderAlerts ( selected = 0 ) { alertList . innerHTML = alerts . map (( a , i )=> `<button class="alert-card ${ i === selected ? 'selected' : '' } " data-index=" ${ i } "><div class="alert-card-top"><span class="badge ${ a . badge } "><i class="severity-dot"></i> ${ a . severity } </span><span class="muted"> ${ a . age } </span></div><h3> ${ a . title } </h3><p> ${ a . site } · ${ a . time } </p><div class="alert-card-foot"><span> ${ a . delivery } </span><span class="mono"> ${ a . id } </span></div></button>` ). join ( '' ); $$ ( '.alert-card' , alertList ). forEach ( b => b . addEventListener ( 'click' ,()=> selectAlert ( Number ( b . dataset . index )))); }
function selectAlert ( index ) { renderAlerts ( index ); const a = alerts [ index ]; $ ( '#selectedSeverity' ). className = `badge ${ a . badge } ` ; $ ( '#selectedSeverity' ). textContent = a . severity ; $ ( '#selectedTitle' ). textContent = a . title ; $ ( '#selectedMeta' ). textContent = ` ${ a . site } · ${ a . time } · ${ a . id } ` ; $ ( '#factRule' ). textContent = a . rule ; $ ( '#ackState' ). className = 'badge warning' ; $ ( '#ackState' ). textContent = '等待 ack' ; $ ( '#ackButton' ). disabled = false ; $ ( '#ackButton' ). innerHTML = '<svg class="icon"><use href="#i-check"/></svg>确认接手' ; $ ( '#ackHelp' ). textContent = '一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。' ; }
renderAlerts ();
function switchView ( name ) { $$ ( '.view' ). forEach ( v => v . classList . toggle ( 'active' , v . dataset . page === name )); $$ ( '[data-view]' ). forEach ( b =>{ const on = b . dataset . view === name ; b . toggleAttribute ( 'aria-current' , on ); b . classList . toggle ( 'active' , on );}); $ ( '#stateSelect' ). value = 'normal' ; renderState ( 'normal' ); $ ( '#main-content' ). focus ({ preventScroll : true }); }
$$ ( '[data-view]' ). forEach ( b => b . addEventListener ( 'click' ,()=> switchView ( b . dataset . view )));
2026-08-04 11:08:41 +08:00
$$ ( '.queue-tabs button' ). forEach ( b => b . addEventListener ( 'click' ,()=>{ $$ ( '.queue-tabs button' ). forEach ( x =>{ x . classList . toggle ( 'active' , x === b ); x . setAttribute ( 'aria-pressed' , x === b )}); notify ( `队列已切换到“ ${ b . textContent } ”` );}));
$$ ( '.evidence-tabs button' ). forEach ( b => b . addEventListener ( 'click' ,()=>{ $$ ( '.evidence-tabs button' ). forEach ( x =>{ x . classList . toggle ( 'active' , x === b ); x . setAttribute ( 'aria-pressed' , x === b )}); notify ( `正在演示“ ${ b . textContent } ”标签结构` );}));
2026-08-04 00:23:55 +08:00
function renderState ( state ) { const layer = $ ( '#stateLayer' ), views = $$ ( '.view' ); layer . classList . toggle ( 'show' , state !== 'normal' ); views . forEach ( v => v . style . display = state === 'normal' ? '' : 'none' ); if ( state === 'normal' ) return ; const states = {
loading : `<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>` ,
weak : `<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal');notify('证据重试任务已加入后台队列')">后台重试证据</button></div>` ,
empty : `<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" onclick="switchView('events')">查看事件中心</button></div>` ,
denied : `<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal')">返回可访问范围</button></div>` }; layer . innerHTML = states [ state ]; }
$ ( '#stateSelect' ). addEventListener ( 'change' , e => renderState ( e . target . value ));
$ ( '#themeToggle' ). addEventListener ( 'click' ,()=>{ const h = document . documentElement ; h . dataset . theme = h . dataset . theme === 'dark' ? 'light' : 'dark' ; notify ( `已切换为 ${ h . dataset . theme === 'dark' ? '深色' : '浅色' } 主题` );});
$ ( '#ackButton' ). addEventListener ( 'click' , async e =>{ const b = e . currentTarget ; b . disabled = true ; b . textContent = '确认中…' ; await new Promise ( r => setTimeout ( r , 650 )); b . innerHTML = '<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手' ; $ ( '#ackState' ). className = 'badge success' ; $ ( '#ackState' ). textContent = '已 ack' ; $ ( '#ackHelp' ). textContent = '23:15:02 · 当前值班员 · 服务端确认成功' ; notify ( '确认成功,升级计时已停止并记录审计。' );});
$ ( '#completeAlert' ). addEventListener ( 'click' ,()=>{ if ( ! $ ( '#outcome' ). value ){ notify ( '请先选择处置结果。' ); $ ( '#outcome' ). focus (); return ;} if ( $ ( '#ackState' ). textContent !== '已 ack' ){ notify ( '请先确认接手,再完成处置。' ); $ ( '#ackButton' ). focus (); return ;} notify ( '处置结果已追加,原始事件事实未被修改。' );});
$ ( '#falsePositive' ). addEventListener ( 'click' ,()=> $ ( '#falseDialog' ). showModal ());
$ ( '#confirmFalse' ). addEventListener ( 'click' , e =>{ if ( ! $ ( '#falseReason' ). value ){ e . preventDefault (); notify ( '请选择误报原因。' ); $ ( '#falseReason' ). focus (); return ;} notify ( '已标记误报并进入复核队列,原始事件保持不变。' );});
$ ( '#muteButton' ). addEventListener ( 'click' ,()=> $ ( '#muteDialog' ). showModal ());
$ ( '#confirmMute' ). addEventListener ( 'click' , e =>{ if ( ! $ ( '#muteCause' ). value . trim ()){ e . preventDefault (); notify ( '请填写静默原因。' ); $ ( '#muteCause' ). focus (); return ;} notify ( '已创建限时静默,到期将自动恢复并通知值班组。' );});
2026-08-04 11:08:41 +08:00
$$ ( '[data-edit-area]' ). forEach ( button => button . addEventListener ( 'click' ,()=>{ const isDorm = button . dataset . editArea === 'dorm' ; $ ( '#areaName' ). value = isDorm ? '宿舍卧室' : button . dataset . editArea === 'lab' ? '实验楼入口' : '北围墙' ; $ ( '#areaPolicy' ). value = isDorm ? 'non_imaging_only' : 'video_allowed' ; $ ( '#areaDialog' ). showModal ();}));
$ ( '#newArea' ). addEventListener ( 'click' ,()=>{ $ ( '#areaName' ). value = '' ; $ ( '#areaPolicy' ). value = 'video_allowed' ; $ ( '#areaDialog' ). showModal ();});
$ ( '#newSite' ). addEventListener ( 'click' ,()=> notify ( '新建站点需先填写时区、场景包、配额和管理员,本原型只枚举入口。' ));
$ ( '#saveAreaPolicy' ). addEventListener ( 'click' ,()=>{ if ( ! $ ( '#areaReason' ). value . trim ()){ notify ( '请填写变更原因。' ); $ ( '#areaReason' ). focus (); return ;} if ( $ ( '#areaName' ). value === '北围墙' && $ ( '#areaPolicy' ). value === 'non_imaging_only' ){ $ ( '#areaDialog' ). close (); $ ( '#policyConflictDialog' ). showModal (); return ;} $ ( '#areaDialog' ). close (); notify ( 'Area 策略新版本已保存,等待 Sense 拉取投影。' );});
$ ( '#confirmPolicyConflict' ). addEventListener ( 'click' ,()=>{ const action = $ ( 'input[name="conflictAction"]:checked' ). value ; notify ( action === 'move' ? '已创建 4 台设备迁移任务;完成前不会发布策略。' : '已取消策略变更,v42 和已有设备保持不变。' );});
2026-08-04 09:04:52 +08:00
const ruleDialog = $ ( '#ruleDialog' );
function openRuleEditor () { ruleDialog . showModal (); }
$ ( '#openRuleDialog' ). addEventListener ( 'click' , openRuleEditor );
$ ( '#continueRuleDraft' ). addEventListener ( 'click' , openRuleEditor );
$$ ( '[data-edit-rule]' ). forEach ( button => button . addEventListener ( 'click' , openRuleEditor ));
2026-08-04 10:05:56 +08:00
function syncSenseZoneLink (){ $ ( '#editRuleZone' ). href = `../sense/index.html?view=device-detail&camera= ${ encodeURIComponent ( $ ( '#ruleCamera' ). value ) } &zone= ${ encodeURIComponent ( $ ( '#ruleZone' ). value ) } &return_to=bell-rule` ; }
$ ( '#ruleCamera' ). addEventListener ( 'change' ,()=>{ syncSenseZoneLink (); notify ( '已切换摄像头,请确认检测区域版本与画面仍匹配。' ); });
$ ( '#ruleZone' ). addEventListener ( 'change' , syncSenseZoneLink );
2026-08-04 09:04:52 +08:00
$ ( '#saveRuleDraft' ). addEventListener ( 'click' ,()=> notify ( $ ( '#startDryRun' ). checked ? '规则草稿已保存并进入试运行,不会触发正式预警。' : '规则草稿已保存,尚未开始试运行。' ));
$ ( '#publishTrialRule' ). addEventListener ( 'click' ,()=>{ if ( confirm ( '正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?' )){ $ ( '#trialBadge' ). className = 'badge success' ; $ ( '#trialBadge' ). textContent = '正式生效' ; notify ( '规则版本已正式发布,变更已记录审计并可回滚。' );}});
2026-08-04 00:23:55 +08:00
let remain = 102 ;
setInterval (()=>{ if ( $ ( '#ackState' ). textContent === '已 ack' || remain <= 0 ) return ; remain -- ; const m = String ( Math . floor ( remain / 60 )). padStart ( 2 , '0' ), s = String ( remain % 60 ). padStart ( 2 , '0' ); $ ( '#countdown' ). textContent = ` ${ m } : ${ s } ` ;}, 1000 );
2026-08-04 09:04:52 +08:00
const routeParams = new URLSearchParams ( location . search );
if ( routeParams . get ( 'view' )) switchView ( routeParams . get ( 'view' ));
2026-08-04 11:08:41 +08:00
if ( routeParams . get ( 'view' ) === 'audit' && ( routeParams . has ( 'device' ) || routeParams . has ( 'site' ))) {
const tenant = routeParams . get ( 'tenant' ) || '当前租户' , site = routeParams . get ( 'site' ) || '当前站点' , device = routeParams . get ( 'device' ) || '全部设备' ;
$ ( '#auditContext' ). hidden = false ; $ ( '#auditTenant' ). textContent = tenant ; $ ( '#auditSite' ). textContent = site ; $ ( '#auditDevice' ). textContent = device ; $ ( '#auditSearch' ). value = device ;
$ ( '#auditRows' ). insertAdjacentHTML ( 'afterbegin' , `<tr><td>10:47:36</td><td>站点管理员</td><td>PAUSE_INFERENCE</td><td class="mono"> ${ device } </td><td><span class="badge success">成功</span></td><td>Sense outbox · relay 已送达</td></tr>` );
$ ( '#auditReturn' ). href = `../sense/index.html?view=device-detail&device= ${ encodeURIComponent ( device ) } &return_to=bell-audit` ;
notify ( '已应用来自 Sense 的租户、站点和设备审计筛选。' );
}
2026-08-04 09:04:52 +08:00
if ( routeParams . has ( 'camera' )) {
switchView ( 'rules' ); $ ( '#ruleContext' ). hidden = false ;
const camera = routeParams . get ( 'camera' ); if ([... $ ( '#ruleCamera' ). options ]. some ( option => option . value === camera )) $ ( '#ruleCamera' ). value = camera ;
2026-08-04 10:05:56 +08:00
const zone = routeParams . get ( 'zone' ); if ( zone && [... $ ( '#ruleZone' ). options ]. some ( option => option . value === zone )) $ ( '#ruleZone' ). value = zone ;
$ ( '#ruleContextCamera' ). textContent = $ ( '#ruleCamera' ). selectedOptions [ 0 ]. textContent ; $ ( '#ruleContextZone' ). textContent = zone || $ ( '#ruleZone' ). value ;
syncSenseZoneLink ();
2026-08-04 09:04:52 +08:00
notify ( '已保留摄像头与检测区域上下文,可继续配置规则。' );
}
2026-08-04 00:23:55 +08:00
</ script >
</ body >
</ html >