@@ -0,0 +1,688 @@
<!doctype html>
< html lang = "zh-CN" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< meta name = "color-scheme" content = "light" >
< title > Silver Pose · 浅色 Windows 演示原型< / title >
< style >
: root {
--surface-base : #eaf1f8 ;
--surface-layer : #ffffff ;
--surface-overlay : #f6f9fc ;
--text-primary : #1e293b ;
--text-secondary : #52657c ;
--text-muted : #72839a ;
--stroke : #c8d4e3 ;
--stroke-subtle : #dce5ef ;
--accent : #2563eb ;
--accent-hover : #1d4ed8 ;
--success : #15803d ;
--success-fill : #e9f7ee ;
--caution : #b45309 ;
--caution-fill : #fff7e6 ;
--critical : #c62828 ;
--critical-fill : #fff0f0 ;
--offline : #64748b ;
--offline-fill : #f1f5f9 ;
--focus : #2563eb ;
--radius-card : 10 px ;
--radius-control : 7 px ;
--space-1 : 4 px ;
--space-2 : 8 px ;
--space-3 : 12 px ;
--space-4 : 16 px ;
--space-5 : 20 px ;
--space-6 : 24 px ;
--font-ui : "Segoe UI Variable" , "Microsoft YaHei UI" , "Microsoft YaHei" , sans-serif ;
--font-number : "Cascadia Mono" , Consolas , monospace ;
}
* { box-sizing : border-box ; }
html { background : var ( - - surface - base ) ; font-family : var ( - - font - ui ) ; }
body {
min-width : 320 px ;
min-height : 100 vh ;
margin : 0 ;
color : var ( - - text - primary ) ;
background :
radial-gradient ( circle at 92 % -10 % , rgba ( 183 , 211 , 242 , .72 ) , transparent 34 rem ) ,
linear-gradient ( 145 deg , #f3f7fb 0 % , var ( - - surface - base ) 60 % , #e5eef8 100 % ) ;
}
button , input {
font : inherit ;
}
button {
min-height : 40 px ;
border : 1 px solid var ( - - stroke ) ;
border-radius : var ( - - radius - control ) ;
padding : 8 px 13 px ;
color : var ( - - text - primary ) ;
background : var ( - - surface - layer ) ;
font-weight : 600 ;
line-height : 1.25 ;
cursor : pointer ;
transition : background 130 ms ease , border-color 130 ms ease , box-shadow 130 ms ease , transform 130 ms ease ;
}
button : hover { border-color : #9cb1c9 ; background : #f8fbff ; }
button : active { transform : translateY ( 1 px ) ; }
button : focus-visible , input : focus-visible , [ tabindex ] : focus-visible {
outline : 3 px solid rgba ( 37 , 99 , 235 , .35 ) ;
outline-offset : 2 px ;
}
. app-shell {
width : min ( 1440 px , calc ( 100 % - 40 px ) ) ;
min-height : 100 vh ;
margin : 0 auto ;
padding : 20 px 0 28 px ;
}
. topbar {
display : flex ;
align-items : center ;
min-height : 56 px ;
gap : var ( - - space -4 ) ;
padding : 8 px 12 px 8 px 16 px ;
border : 1 px solid rgba ( 186 , 202 , 220 , .95 ) ;
border-radius : var ( - - radius - card ) ;
background : rgba ( 255 , 255 , 255 , .92 ) ;
box-shadow : 0 8 px 24 px rgba ( 41 , 66 , 99 , .08 ) ;
}
. brand , . source-summary , . topbar-right , . section-heading , . toolbar , . chip , . state-title , . meta-line , . event-row , . setting-label , . setting-actions , . dialog-heading , . footer-note {
display : flex ;
align-items : center ;
}
. brand { min-width : 238 px ; gap : 10 px ; }
. brand-mark {
display : grid ;
width : 30 px ;
height : 30 px ;
place-items : center ;
border-radius : 8 px ;
color : #fff ;
background : linear-gradient ( 135 deg , #3473e6 , #1e4fae ) ;
box-shadow : inset 0 1 px 0 rgba ( 255 , 255 , 255 , .28 ) ;
font-size : 12 px ;
font-weight : 750 ;
letter-spacing : .04 em ;
}
. brand strong { display : block ; font-size : 16 px ; letter-spacing : .01 em ; }
. brand small { display : block ; margin-top : 1 px ; color : var ( - - text - secondary ) ; font-size : 12 px ; }
. source-summary { flex : 1 ; justify-content : center ; min-width : 0 ; gap : 9 px ; color : var ( - - text - secondary ) ; font-size : 13 px ; }
. source-indicator { width : 9 px ; height : 9 px ; border : 2 px solid var ( - - success ) ; border-radius : 50 % ; background : #fff ; }
. source-name { overflow : hidden ; color : var ( - - text - primary ) ; font-weight : 600 ; text-overflow : ellipsis ; white-space : nowrap ; }
. topbar-right { gap : 12 px ; color : var ( - - text - secondary ) ; font-family : var ( - - font - number ) ; font-size : 12 px ; }
. demo-tag { padding : 4 px 8 px ; border : 1 px solid #b7c9df ; border-radius : 99 px ; color : #315274 ; background : #f4f8fc ; font-family : var ( - - font - ui ) ; font-size : 11 px ; font-weight : 650 ; }
. tabs {
display : flex ;
width : max-content ;
max-width : 100 % ;
margin : 14 px 0 0 ;
padding : 4 px ;
gap : 2 px ;
border : 1 px solid var ( - - stroke ) ;
border-radius : 9 px ;
background : rgba ( 255 , 255 , 255 , .76 ) ;
}
. tabs button { min-height : 36 px ; border : 0 ; border-radius : 6 px ; background : transparent ; color : var ( - - text - secondary ) ; }
. tabs button [ aria-selected = "true" ] { color : #173e7b ; background : #eaf2ff ; box-shadow : inset 0 -2 px 0 var ( - - accent ) ; }
main { padding-top : var ( - - space -4 ) ; }
[ hidden ] { display : none !important ; }
. page-intro { margin : 0 0 var ( - - space -3 ) ; }
. page-intro h1 { margin : 0 ; font-size : 21 px ; line-height : 1.3 ; }
. page-intro p { margin : 5 px 0 0 ; color : var ( - - text - secondary ) ; font-size : 13 px ; line-height : 1.55 ; }
. inline-note { color : #315274 ; font-weight : 600 ; }
. toolbar {
justify-content : space-between ;
flex-wrap : wrap ;
gap : 10 px ;
margin-bottom : var ( - - space -4 ) ;
padding : 10 px 12 px ;
border : 1 px solid var ( - - stroke ) ;
border-radius : var ( - - radius - card ) ;
background : rgba ( 255 , 255 , 255 , .88 ) ;
}
. state-switches , . tool-actions { display : flex ; flex-wrap : wrap ; gap : 8 px ; }
. state-switches button { min-height : 38 px ; color : var ( - - text - secondary ) ; }
. state-switches button [ aria-pressed = "true" ] { border-color : currentColor ; font-weight : 700 ; }
. state-switches button [ data-state-target = "normal" ] [ aria-pressed = "true" ] { color : var ( - - success ) ; background : var ( - - success - fill ) ; }
. state-switches button [ data-state-target = "confirmed" ] [ aria-pressed = "true" ] { color : var ( - - critical ) ; background : var ( - - critical - fill ) ; }
. state-switches button [ data-state-target = "offline" ] [ aria-pressed = "true" ] { color : var ( - - offline ) ; background : var ( - - offline - fill ) ; }
. primary-action { border-color : var ( - - accent ) ; color : #fff ; background : var ( - - accent ) ; }
. primary-action : hover { border-color : var ( - - accent - hover ) ; color : #fff ; background : var ( - - accent - hover ) ; }
. secondary-action { color : #24486f ; background : #f7fbff ; }
. monitor-layout { display : grid ; grid-template-columns : minmax ( 0 , 8 fr ) minmax ( 292 px , 4 fr ) ; gap : var ( - - space -4 ) ; }
. card { overflow : hidden ; border : 1 px solid var ( - - stroke ) ; border-radius : var ( - - radius - card ) ; background : var ( - - surface - layer ) ; box-shadow : 0 6 px 18 px rgba ( 50 , 77 , 111 , .06 ) ; }
. card-header { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : var ( - - space -3 ) ; padding : 15 px 16 px 12 px ; border-bottom : 1 px solid var ( - - stroke - subtle ) ; }
. card-header h2 { margin : 0 ; font-size : 15 px ; line-height : 1.35 ; }
. card-header p { margin : 3 px 0 0 ; color : var ( - - text - secondary ) ; font-size : 12 px ; line-height : 1.45 ; }
. scene-wrap { position : relative ; aspect-ratio : 16 / 9 ; min-height : 300 px ; overflow : hidden ; background : #dfeaf5 ; }
. scene-wrap :: after { position : absolute ; inset : 0 ; border : 2 px solid transparent ; content : "" ; pointer-events : none ; }
. scene-svg { display : block ; width : 100 % ; height : 100 % ; }
. scene-topline { position : absolute ; z-index : 2 ; top : 14 px ; right : 14 px ; left : 14 px ; display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; }
. scene-badge { display : inline-flex ; align-items : center ; gap : 7 px ; padding : 6 px 9 px ; border : 1 px solid #9fc7ae ; border-radius : 6 px ; color : var ( - - success ) ; background : rgba ( 255 , 255 , 255 , .93 ) ; font-family : var ( - - font - number ) ; font-size : 12 px ; font-weight : 700 ; }
. badge-dot { width : 8 px ; height : 8 px ; border : 2 px solid currentColor ; border-radius : 50 % ; background : #fff ; }
. scene-time { padding : 5 px 7 px ; border : 1 px solid rgba ( 136 , 160 , 188 , .75 ) ; border-radius : 5 px ; color : #315274 ; background : rgba ( 255 , 255 , 255 , .9 ) ; font-family : var ( - - font - number ) ; font-size : 11 px ; }
. track-label { position : absolute ; z-index : 2 ; top : 47 % ; left : 47 % ; padding : 5 px 7 px ; border : 1 px solid #63a77e ; border-radius : 5 px ; color : #0f6f33 ; background : rgba ( 255 , 255 , 255 , .93 ) ; font-family : var ( - - font - number ) ; font-size : 11 px ; font-weight : 700 ; }
. reconnect-overlay { position : absolute ; z-index : 3 ; inset : 0 ; display : grid ; place-items : center ; padding : 24 px ; color : #334155 ; background : rgba ( 241 , 245 , 249 , .82 ) ; opacity : 0 ; pointer-events : none ; text-align : center ; transition : opacity 130 ms ease ; }
. reconnect-overlay strong { display : block ; font-size : 18 px ; }
. reconnect-overlay span { display : block ; margin-top : 6 px ; color : var ( - - offline ) ; font-size : 13 px ; }
. scene-caption { padding : 11 px 16 px ; border-top : 1 px solid var ( - - stroke - subtle ) ; color : var ( - - text - secondary ) ; background : #fbfdff ; font-size : 12 px ; line-height : 1.5 ; }
. normal-person , . fall-person { transform-origin : 50 % 50 % ; transition : opacity 160 ms ease ; }
. fall-person { opacity : 0 ; }
. skeleton { fill : none ; stroke : #15803d ; stroke-linecap : round ; stroke-linejoin : round ; stroke-width : 6 ; }
. joint { fill : #fff ; stroke : #15803d ; stroke-width : 3 ; }
. person-box { fill : none ; stroke : #15803d ; stroke-dasharray : 9 6 ; stroke-width : 2 ; }
. side-stack { display : grid ; align-content : start ; gap : var ( - - space -4 ) ; }
. state-body , . evidence-body { padding : 15 px 16 px ; }
. state-title { gap : 10 px ; }
. state-mark { display : grid ; width : 32 px ; height : 32 px ; place-items : center ; border : 1 px solid #9fc7ae ; border-radius : 8 px ; color : var ( - - success ) ; background : var ( - - success - fill ) ; font-size : 15 px ; font-weight : 750 ; }
. state-title h2 { margin : 0 ; color : var ( - - success ) ; font-size : 15 px ; }
. state-title p { margin : 2 px 0 0 ; color : var ( - - text - secondary ) ; font-size : 12 px ; }
. state-description { margin : 13 px 0 0 ; padding : 10 px 11 px ; border : 1 px solid #cfe2d6 ; border-radius : 7 px ; color : #2d5540 ; background : #f5fbf7 ; font-size : 12 px ; line-height : 1.52 ; }
. metric-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 8 px ; margin-top : 13 px ; }
. metric { padding : 9 px 10 px ; border : 1 px solid var ( - - stroke - subtle ) ; border-radius : 7 px ; background : #fbfdff ; }
. metric span { display : block ; color : var ( - - text - muted ) ; font-size : 11 px ; }
. metric strong { display : block ; margin-top : 4 px ; color : #293c54 ; font-family : var ( - - font - number ) ; font-size : 12 px ; }
. metric strong . status-success { color : var ( - - success ) ; }
. evidence-list { display : grid ; gap : 1 px ; }
. evidence-row { display : grid ; grid-template-columns : 18 px minmax ( 0 , 1 fr ) auto ; align-items : center ; gap : 8 px ; padding : 9 px 0 ; border-bottom : 1 px solid var ( - - stroke - subtle ) ; font-size : 12 px ; }
. evidence-row : last-child { border-bottom : 0 ; }
. evidence-shape { width : 13 px ; height : 13 px ; border : 2 px solid var ( - - success ) ; border-radius : 50 % ; }
. evidence-row strong { font-weight : 650 ; }
. evidence-row span : last-child { color : var ( - - text - secondary ) ; text-align : right ; }
. artifact-box { margin-top : 12 px ; padding : 10 px ; border : 1 px solid #cfe2d6 ; border-radius : 7 px ; background : #f5fbf7 ; }
. artifact-box strong { display : block ; color : var ( - - success ) ; font-size : 12 px ; }
. artifact-box span { display : block ; margin-top : 4 px ; color : var ( - - text - secondary ) ; font-family : var ( - - font - number ) ; font-size : 11 px ; line-height : 1.45 ; word-break : break-word ; }
. event-card { margin-top : var ( - - space -4 ) ; }
. event-list { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; }
. event-row { min-width : 0 ; gap : 10 px ; padding : 13 px 15 px ; border-right : 1 px solid var ( - - stroke - subtle ) ; }
. event-row : last-child { border-right : 0 ; }
. event-symbol { flex : 0 0 auto ; width : 11 px ; height : 11 px ; border : 2 px solid var ( - - success ) ; border-radius : 50 % ; }
. event-symbol . caution { border-color : var ( - - caution ) ; }
. event-symbol . offline { border-color : var ( - - offline ) ; }
. event-copy { min-width : 0 ; }
. event-copy strong { display : block ; overflow : hidden ; font-size : 12 px ; text-overflow : ellipsis ; white-space : nowrap ; }
. event-copy span { display : block ; margin-top : 3 px ; color : var ( - - text - secondary ) ; font-size : 11 px ; line-height : 1.35 ; }
. settings-status { display : inline-flex ; align-items : center ; gap : 7 px ; padding : 7 px 9 px ; border : 1 px solid #b9cce2 ; border-radius : 7 px ; color : #315274 ; background : #f4f8fc ; font-size : 12 px ; font-weight : 650 ; }
. settings-status-dot { width : 8 px ; height : 8 px ; border : 2 px solid currentColor ; border-radius : 50 % ; }
. settings-status [ data-config-state = "dirty" ] { border-color : #ecd09a ; color : var ( - - caution ) ; background : var ( - - caution - fill ) ; }
. settings-status [ data-config-state = "saved" ] { border-color : #b9cce2 ; color : #315274 ; background : #f4f8fc ; }
. settings-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : var ( - - space -4 ) ; }
. settings-card . wide { grid-column : 1 / -1 ; }
. settings-body { padding : 16 px ; }
. key-value { display : grid ; grid-template-columns : minmax ( 108 px , .75 fr ) minmax ( 0 , 1.25 fr ) ; gap : 10 px ; padding : 9 px 0 ; border-bottom : 1 px solid var ( - - stroke - subtle ) ; font-size : 13 px ; }
. key-value : last-child { border-bottom : 0 ; }
. key-value span { color : var ( - - text - secondary ) ; }
. key-value strong { color : #293c54 ; font-weight : 650 ; }
. readiness { color : var ( - - success ) !important ; }
. setting-form { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 14 px 16 px ; }
. setting-label { align-items : flex-start ; flex-direction : column ; gap : 6 px ; color : #384b63 ; font-size : 12 px ; font-weight : 650 ; }
. setting-label small { color : var ( - - text - muted ) ; font-weight : 400 ; }
. setting-label input { width : 100 % ; min-height : 38 px ; border : 1 px solid var ( - - stroke ) ; border-radius : 6 px ; padding : 7 px 9 px ; color : var ( - - text - primary ) ; background : #fff ; font-family : var ( - - font - number ) ; }
. setting-actions { justify-content : space-between ; flex-wrap : wrap ; gap : 12 px ; margin-top : 16 px ; padding-top : 15 px ; border-top : 1 px solid var ( - - stroke - subtle ) ; }
. config-versions { color : var ( - - text - secondary ) ; font-size : 12 px ; line-height : 1.5 ; }
. config-versions strong { color : #315274 ; font-family : var ( - - font - number ) ; }
. settings-buttons { display : flex ; flex-wrap : wrap ; gap : 8 px ; }
. settings-note { margin : 10 px 0 0 ; color : var ( - - text - secondary ) ; font-size : 12 px ; line-height : 1.55 ; }
. footer-note { justify-content : space-between ; gap : 12 px ; padding : 15 px 2 px 0 ; color : var ( - - text - secondary ) ; font-size : 11 px ; line-height : 1.5 ; }
. footer-note strong { color : #315274 ; }
. sr-only { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 , 0 , 0 , 0 ) ; white-space : nowrap ; border : 0 ; }
. toast { position : fixed ; z-index : 10 ; right : 24 px ; bottom : 24 px ; max-width : min ( 430 px , calc ( 100 % - 32 px ) ) ; padding : 11 px 13 px ; border : 1 px solid #9bb8d8 ; border-radius : 8 px ; color : #24486f ; background : #f4f8fc ; box-shadow : 0 12 px 30 px rgba ( 45 , 72 , 104 , .18 ) ; font-size : 13 px ; opacity : 0 ; pointer-events : none ; transform : translateY ( 8 px ) ; transition : opacity 150 ms ease , transform 150 ms ease ; }
. toast . is-visible { opacity : 1 ; transform : translateY ( 0 ) ; }
dialog { width : min ( 440 px , calc ( 100 % - 32 px ) ) ; border : 1 px solid #e1aaaa ; border-radius : 10 px ; padding : 0 ; color : var ( - - text - primary ) ; background : #fff ; box-shadow : 0 22 px 68 px rgba ( 68 , 25 , 25 , .23 ) ; }
dialog :: backdrop { background : rgba ( 30 , 41 , 59 , .28 ) ; }
. dialog-heading { gap : 11 px ; padding : 18 px 20 px 14 px ; border-bottom : 1 px solid #f0cdcd ; background : var ( - - critical - fill ) ; }
. dialog-symbol { display : grid ; width : 30 px ; height : 30 px ; place-items : center ; border : 1 px solid #e2a2a2 ; border-radius : 50 % ; color : var ( - - critical ) ; background : #fff ; font-size : 16 px ; font-weight : 800 ; }
. dialog-heading h2 { margin : 0 ; color : var ( - - critical ) ; font-size : 18 px ; }
. dialog-heading p { margin : 3 px 0 0 ; color : #7a3535 ; font-size : 12 px ; }
. dialog-body { padding : 16 px 20 px ; }
. dialog-body p { margin : 0 ; color : #384b63 ; font-size : 13 px ; line-height : 1.6 ; }
. dialog-details { display : grid ; gap : 8 px ; margin-top : 13 px ; padding : 11 px ; border : 1 px solid #f0d0d0 ; border-radius : 7 px ; background : #fffafa ; font-size : 12 px ; }
. dialog-details div { display : flex ; justify-content : space-between ; gap : 10 px ; }
. dialog-details span { color : var ( - - text - secondary ) ; }
. dialog-details strong { color : #703232 ; font-family : var ( - - font - number ) ; text-align : right ; }
. dialog-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; padding : 0 20 px 18 px ; }
. dialog-actions button { border-color : var ( - - critical ) ; color : #fff ; background : var ( - - critical ) ; }
. dialog-actions button : hover { border-color : #a91f1f ; background : #a91f1f ; }
. app-shell [ data-state = "confirmed" ] . scene-wrap :: after { border-color : var ( - - critical ) ; box-shadow : inset 0 0 0 1 px rgba ( 198 , 40 , 40 , .12 ) ; }
. app-shell [ data-state = "confirmed" ] . scene-badge , . app-shell [ data-state = "confirmed" ] . track-label { border-color : #df9b9b ; color : var ( - - critical ) ; background : #fff8f8 ; }
. app-shell [ data-state = "confirmed" ] . badge-dot { background : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . normal-person { opacity : 0 ; }
. app-shell [ data-state = "confirmed" ] . fall-person { opacity : 1 ; }
. app-shell [ data-state = "confirmed" ] . state-mark { border-color : #dfa1a1 ; color : var ( - - critical ) ; background : var ( - - critical - fill ) ; }
. app-shell [ data-state = "confirmed" ] . state-title h2 { color : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . state-description , . app-shell [ data-state = "confirmed" ] . artifact-box { border-color : #efc5c5 ; color : #763a3a ; background : #fff8f8 ; }
. app-shell [ data-state = "confirmed" ] . artifact-box strong , . app-shell [ data-state = "confirmed" ] . metric strong . status-success , . app-shell [ data-state = "confirmed" ] . evidence-shape { color : var ( - - critical ) ; border-color : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . event-symbol { border-color : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . skeleton { stroke : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . joint { stroke : var ( - - critical ) ; }
. app-shell [ data-state = "confirmed" ] . person-box { stroke : var ( - - critical ) ; }
. app-shell [ data-state = "offline" ] . scene-wrap :: after { border-color : #9aaabd ; }
. app-shell [ data-state = "offline" ] . source-indicator { border-color : var ( - - offline ) ; }
. app-shell [ data-state = "offline" ] . scene-svg { filter : grayscale ( .75 ) opacity ( .64 ) ; }
. app-shell [ data-state = "offline" ] . reconnect-overlay { opacity : 1 ; }
. app-shell [ data-state = "offline" ] . scene-badge , . app-shell [ data-state = "offline" ] . track-label { border-color : #b5c1cf ; color : var ( - - offline ) ; background : #f8fafc ; }
. app-shell [ data-state = "offline" ] . state-mark { border-color : #bdc8d4 ; color : var ( - - offline ) ; background : var ( - - offline - fill ) ; }
. app-shell [ data-state = "offline" ] . state-title h2 , . app-shell [ data-state = "offline" ] . metric strong . status-success { color : var ( - - offline ) ; }
. app-shell [ data-state = "offline" ] . state-description , . app-shell [ data-state = "offline" ] . artifact-box { border-color : #d6dee7 ; color : #52657c ; background : #f8fafc ; }
. app-shell [ data-state = "offline" ] . artifact-box strong , . app-shell [ data-state = "offline" ] . evidence-shape { color : var ( - - offline ) ; border-color : var ( - - offline ) ; }
@ media ( max-width : 1007px ) {
. app-shell { width : min ( 100 % , calc ( 100 % - 28 px ) ) ; padding-top : 14 px ; }
. monitor-layout { grid-template-columns : minmax ( 0 , 1.25 fr ) minmax ( 270 px , .75 fr ) ; gap : 12 px ; }
. event-list { grid-template-columns : 1 fr ; }
. event-row { border-right : 0 ; border-bottom : 1 px solid var ( - - stroke - subtle ) ; }
. event-row : last-child { border-bottom : 0 ; }
}
@ media ( max-width : 760px ) {
. topbar , . toolbar , . setting-actions , . footer-note { align-items : flex-start ; flex-wrap : wrap ; }
. source-summary { order : 3 ; flex-basis : 100 % ; justify-content : flex-start ; }
. topbar-right { margin-left : auto ; }
. monitor-layout { grid-template-columns : 1 fr ; }
. settings-grid { grid-template-columns : 1 fr ; }
. settings-card . wide { grid-column : auto ; }
. setting-form { grid-template-columns : 1 fr 1 fr ; }
}
@ media ( max-width : 640px ) {
. app-shell { width : min ( 100 % , calc ( 100 % - 20 px ) ) ; padding-top : 10 px ; }
. topbar { padding : 10 px 12 px ; }
. brand { min-width : 0 ; }
. topbar-right { display : none ; }
. tabs { width : 100 % ; }
. tabs button { flex : 1 ; }
. toolbar { padding : 10 px ; }
. state-switches , . tool-actions { width : 100 % ; }
. state-switches button { flex : 1 1 132 px ; }
. tool-actions button { flex : 1 ; }
. scene-wrap { min-height : 220 px ; }
. scene-topline { top : 10 px ; right : 10 px ; left : 10 px ; }
. card-header , . state-body , . evidence-body , . settings-body { padding-right : 12 px ; padding-left : 12 px ; }
. setting-form { grid-template-columns : 1 fr ; }
. settings-buttons { width : 100 % ; }
. settings-buttons button { flex : 1 ; }
. key-value { grid-template-columns : 1 fr ; gap : 4 px ; }
. footer-note { padding-bottom : 8 px ; }
. toast { right : 16 px ; bottom : 16 px ; }
}
@ media ( max-width : 600px ) {
. source-summary { font-size : 12 px ; }
. scene-time { display : none ; }
. scene-badge { max-width : 100 % ; }
. metric-grid { grid-template-columns : 1 fr 1 fr ; }
}
@ media ( prefers-reduced-motion : reduce ) {
* , * :: before , * :: after { scroll-behavior : auto !important ; transition-duration : 0.01 ms !important ; animation-duration : 0.01 ms !important ; }
}
< / style >
< / head >
< body >
< div class = "app-shell" id = "app" data-state = "normal" data-running = "true" >
< header class = "topbar" >
< div class = "brand" aria-label = "Silver Pose 跌倒事件演示" >
< span class = "brand-mark" aria-hidden = "true" > SP< / span >
< div > < strong > Silver Pose< / strong > < small > 摔倒事件演示 · 单路本地监控< / small > < / div >
< / div >
< div class = "source-summary" aria-label = "当前演示来源状态" >
< span class = "source-indicator" aria-hidden = "true" > < / span >
< span class = "source-name" > 大厅东侧 · 固定俯视演示机位< / span >
< span id = "source-status" > 来源在线 · 模拟画面< / span >
< / div >
< div class = "topbar-right" > < span class = "demo-tag" > 演示数据< / span > < time id = "clock" > 12:08:42< / time > < / div >
< / header >
< nav class = "tabs" role = "tablist" aria-label = "主视图" >
< button id = "monitor-tab" type = "button" role = "tab" aria-selected = "true" aria-controls = "monitor-panel" data-tab-target = "monitor" > 实时监控< / button >
< button id = "settings-tab" type = "button" role = "tab" aria-selected = "false" aria-controls = "settings-panel" tabindex = "-1" data-tab-target = "settings" > 设置< / button >
< / nav >
< main >
< section id = "monitor-panel" role = "tabpanel" aria-labelledby = "monitor-tab" >
< div class = "page-intro" >
< h1 > 实时监控< / h1 >
< p > 展示同一人员的姿态和事件状态。< span class = "inline-note" > 演示数据,未连接真实摄像头或模型。< / span > < / p >
< / div >
< div class = "toolbar" aria-label = "原型状态和操作" >
< div class = "state-switches" aria-label = "切换演示状态" >
< button type = "button" data-state-target = "normal" aria-pressed = "true" > 1 正常监控< / button >
< button type = "button" data-state-target = "confirmed" aria-pressed = "false" > 2 确认摔倒< / button >
< button type = "button" data-state-target = "offline" aria-pressed = "false" > 3 连接异常< / button >
< / div >
< div class = "tool-actions" >
< button id = "monitor-toggle" type = "button" class = "primary-action" aria-pressed = "true" > < span > 停止模拟监控< / span > < / button >
< button id = "artifact-button" type = "button" class = "secondary-action" > 查看截图位置< / button >
< / div >
< / div >
< div class = "monitor-layout" >
< article class = "card" aria-label = "实时监控画面" >
< div class = "card-header" > < div > < h2 > 大厅东侧 · 画面预览< / h2 > < p id = "scene-caption" > 全身大部分可见,姿态质量满足监控条件。< / p > < / div > < span class = "demo-tag" > 16:9 示例画面< / span > < / div >
< div class = "scene-wrap" >
< div class = "scene-topline" > < div class = "scene-badge" > < span class = "badge-dot" aria-hidden = "true" > < / span > < span id = "scene-state" > NORMAL< / span > < / div > < time class = "scene-time" id = "scene-time" > 12:08:42< / time > < / div >
< span class = "track-label" id = "track-label" > P-017 · 姿态可用< / span >
< svg class = "scene-svg" viewBox = "0 0 960 540" role = "img" aria-label = "大厅俯视演示画面,含人员姿态示意" >
< defs >
< linearGradient id = "floor" x1 = "0" y1 = "0" x2 = "1" y2 = "1" > < stop offset = "0" stop-color = "#edf4fb" / > < stop offset = "1" stop-color = "#d7e5f2" / > < / linearGradient >
< linearGradient id = "corridor" x1 = "0" y1 = "0" x2 = "0" y2 = "1" > < stop offset = "0" stop-color = "#f8fbff" / > < stop offset = "1" stop-color = "#e8f1f8" / > < / linearGradient >
< / defs >
< rect width = "960" height = "540" fill = "url(#floor)" / >
< path d = "M0 82H960M0 386H960" stroke = "#c5d7e8" stroke-width = "3" / >
< path d = "M95 0V540M315 0V540M645 0V540M865 0V540" stroke = "#d3e0ec" stroke-width = "2" / >
< path d = "M0 150 250 105 710 105 960 150V350L710 400H250L0 350Z" fill = "url(#corridor)" stroke = "#bdcfdf" stroke-width = "3" / >
< path d = "M160 105 160 400M800 105 800 400" stroke = "#d1dfec" stroke-width = "3" / >
< rect x = "42" y = "42" width = "147" height = "51" rx = "8" fill = "#dbe9f5" stroke = "#b9ccdf" / >
< rect x = "770" y = "440" width = "150" height = "55" rx = "8" fill = "#dbe9f5" stroke = "#b9ccdf" / >
< text x = "65" y = "74" fill = "#607a95" font-family = "Segoe UI, Microsoft YaHei" font-size = "17" > 入口区域< / text >
< text x = "796" y = "474" fill = "#607a95" font-family = "Segoe UI, Microsoft YaHei" font-size = "17" > 服务台< / text >
< g class = "normal-person" >
< rect class = "person-box" x = "443" y = "138" width = "118" height = "262" rx = "8" / >
< path class = "skeleton" d = "M500 184 500 237 469 284M500 237 530 284M500 237 482 327M500 237 520 327" / >
< circle class = "joint" cx = "500" cy = "163" r = "18" / >
< circle class = "joint" cx = "500" cy = "184" r = "5" / > < circle class = "joint" cx = "500" cy = "237" r = "5" / >
< circle class = "joint" cx = "469" cy = "284" r = "5" / > < circle class = "joint" cx = "530" cy = "284" r = "5" / >
< circle class = "joint" cx = "482" cy = "327" r = "5" / > < circle class = "joint" cx = "520" cy = "327" r = "5" / >
< / g >
< g class = "fall-person" >
< rect class = "person-box" x = "378" y = "274" width = "266" height = "104" rx = "8" / >
< path class = "skeleton" d = "M427 323 498 323 558 302M498 323 553 354M498 323 454 350M498 323 590 331" / >
< circle class = "joint" cx = "406" cy = "323" r = "17" / >
< circle class = "joint" cx = "427" cy = "323" r = "5" / > < circle class = "joint" cx = "498" cy = "323" r = "5" / >
< circle class = "joint" cx = "558" cy = "302" r = "5" / > < circle class = "joint" cx = "553" cy = "354" r = "5" / >
< circle class = "joint" cx = "454" cy = "350" r = "5" / > < circle class = "joint" cx = "590" cy = "331" r = "5" / >
< / g >
< / svg >
< div class = "reconnect-overlay" > < div > < strong > 视频源正在重连< / strong > < span > 连接异常不生成摔倒事件,也不播放报警。< / span > < / div > < / div >
< / div >
< / article >
< aside class = "side-stack" aria-label = "状态与事件证据" >
< section class = "card" >
< div class = "state-body" >
< div class = "state-title" > < span class = "state-mark" id = "state-mark" aria-hidden = "true" > ✓< / span > < div > < h2 id = "state-heading" > NORMAL · 正常监控< / h2 > < p id = "state-time" > 稳定观察中< / p > < / div > < / div >
< p class = "state-description" id = "state-description" > 当前人员姿态稳定,没有生成摔倒候选或报警副作用。< / p >
< div class = "metric-grid" aria-label = "当前状态指标" >
< div class = "metric" > < span > 人员< / span > < strong id = "metric-people" > 1 · P-017< / strong > < / div >
< div class = "metric" > < span > 姿态质量< / span > < strong id = "metric-quality" > 良好< / strong > < / div >
< div class = "metric" > < span > 事件延迟< / span > < strong id = "metric-latency" > —< / strong > < / div >
< div class = "metric" > < span > 报警< / span > < strong class = "status-success" id = "metric-alert" > 未触发< / strong > < / div >
< / div >
< / div >
< / section >
< section class = "card" >
< div class = "card-header" > < div > < h2 > 事件证据< / h2 > < p id = "evidence-status" > 当前无待处理事件< / p > < / div > < span class = "demo-tag" id = "artifact-status" > 监控中< / span > < / div >
< div class = "evidence-body" >
< div class = "evidence-list" id = "evidence-panel" > < / div >
< div class = "artifact-box" > < strong id = "artifact-title" > 本地声音:未触发< / strong > < span id = "artifact-path" > 当前没有已保存截图。< / span > < / div >
< / div >
< / section >
< / aside >
< / div >
< section class = "card event-card" aria-label = "最近事件" >
< div class = "card-header" > < div > < h2 > 最近事件< / h2 > < p > 原型仅展示摘要;正式版由本地 JSONL 事件记录提供。< / p > < / div > < span class = "demo-tag" id = "event-count" > 无新增确认事件< / span > < / div >
< div class = "event-list" id = "event-list" > < / div >
< / section >
< / section >
< section id = "settings-panel" role = "tabpanel" aria-labelledby = "settings-tab" hidden data-config-state = "saved" >
< div class = "page-intro" >
< h1 > 设置< / h1 >
< p > 编辑的是非敏感草稿配置。保存后仅在下一次开始监控时生效,当前运行配置保持不变。< / p >
< / div >
< div class = "card" style = "margin-bottom:16px" >
< div class = "card-header" > < div > < h2 > 配置应用状态< / h2 > < p > 用于区分正在监控的快照和下一次启动将采用的草稿。< / p > < / div > < span class = "settings-status" id = "config-state" > < span class = "settings-status-dot" aria-hidden = "true" > < / span > < span id = "config-state-text" > 草稿已保存,下一次启动生效< / span > < / span > < / div >
< div class = "settings-body" > < div class = "key-value" > < span > 当前运行配置< / span > < strong > cfg-20260720-01 · 监控开始时冻结< / strong > < / div > < div class = "key-value" > < span > 草稿版本< / span > < strong id = "draft-version" > cfg-20260720-02 · 已保存待应用< / strong > < / div > < / div >
< / div >
< div class = "settings-grid" >
< article class = "card settings-card" > < div class = "card-header" > < div > < h2 > 视频来源< / h2 > < p > 真实来源只由本地环境变量或未跟踪配置提供。< / p > < / div > < / div > < div class = "settings-body" > < div class = "key-value" > < span > 来源变量< / span > < strong > SILVER_POSE_RTSP_URL< / strong > < / div > < div class = "key-value" > < span > 就绪状态< / span > < strong class = "readiness" > 已检测 · 不显示具体地址< / strong > < / div > < p class = "settings-note" > 此原型不读取、保存或回显来源地址及其凭证。< / p > < / div > < / article >
< article class = "card settings-card" > < div class = "card-header" > < div > < h2 > 姿态模型< / h2 > < p > 只读模型来源信息;原型不会执行推理。< / p > < / div > < / div > < div class = "settings-body" > < div class = "key-value" > < span > 能力< / span > < strong > person Pose · 17 关键点< / strong > < / div > < div class = "key-value" > < span > 锁定状态< / span > < strong > 演示基线已记录< / strong > < / div > < p class = "settings-note" > 姿态输出不等同于摔倒概率;事件判断由时序状态机负责。< / p > < / div > < / article >
< article class = "card settings-card wide" > < div class = "card-header" > < div > < h2 > 事件与重连草稿< / h2 > < p > 这些值为原型演示字段。修改后会标为“编辑中”,不会热修改实时监控。< / p > < / div > < / div > < div class = "settings-body" > < form id = "settings-form" > < div class = "setting-form" > < label class = "setting-label" > 确认持续时间(秒)< small > 目标范围 1– 3 秒< / small > < input id = "confirm-seconds" type = "number" min = "1" max = "3" step = "0.1" value = "1.8" > < / label > < label class = "setting-label" > 恢复冷却(秒)< small > 确认后再允许新事件< / small > < input id = "cooldown-seconds" type = "number" min = "1" max = "120" step = "1" value = "18" > < / label > < label class = "setting-label" > 关键点质量阈值< small > 低质量姿态不产生候选< / small > < input id = "quality-threshold" type = "number" min = "0" max = "1" step = "0.01" value = "0.65" > < / label > < label class = "setting-label" > 重连初始等待(秒)< small > 断流期间不报警< / small > < input id = "reconnect-seconds" type = "number" min = "1" max = "60" step = "1" value = "4" > < / label > < label class = "setting-label" > 最大重连等待(秒)< small > 用于指数退避上限< / small > < input id = "reconnect-max-seconds" type = "number" min = "1" max = "300" step = "1" value = "30" > < / label > < label class = "setting-label" > 事件冷却复核(秒)< small > 避免重复副作用< / small > < input id = "event-debounce-seconds" type = "number" min = "1" max = "120" step = "1" value = "12" > < / label > < / div > < div class = "setting-actions" > < div class = "config-versions" > 运行版本:< strong > cfg-20260720-01< / strong > < br > 草稿仅在下一次开始监控时成为新的运行配置。< / div > < div class = "settings-buttons" > < button id = "reset-settings" type = "button" > 恢复演示默认值< / button > < button type = "submit" class = "primary-action" > 保存草稿< / button > < / div > < / div > < / form > < / div > < / article >
< / div >
< / section >
< / main >
< footer class = "footer-note" > < span > < strong > 适用边界:< / strong > 固定俯视大厅或走廊、单路、全身大部分可见的演示条件。< / span > < span > HTML 交互原型 · 不替代事件级验收< / span > < / footer >
< / div >
< div id = "live-status" aria-live = "polite" class = "sr-only" > < / div >
< div id = "toast" class = "toast" role = "status" aria-live = "polite" > < / div >
< dialog id = "fall-dialog" aria-labelledby = "dialog-title" aria-describedby = "dialog-description" >
< div class = "dialog-heading" > < span class = "dialog-symbol" aria-hidden = "true" > !< / span > < div > < h2 id = "dialog-title" > 确认摔倒< / h2 > < p > 已生成一次本地报警副作用< / p > < / div > < / div >
< div class = "dialog-body" > < p id = "dialog-description" > 人员 P-017 的倒地证据已持续满足确认条件。请按现场流程确认情况。< / p > < div class = "dialog-details" > < div > < span > 确认时间< / span > < strong id = "dialog-time" > 12:08:42< / strong > < / div > < div > < span > 确认延迟< / span > < strong > 1.8 秒< / strong > < / div > < div > < span > 事件编号< / span > < strong > FALL-00017< / strong > < / div > < div > < span > 截图状态< / span > < strong > 已保存(演示)< / strong > < / div > < / div > < / div >
< div class = "dialog-actions" > < button id = "acknowledge-button" type = "button" > 我已知晓< / button > < / div >
< / dialog >
< script >
( function ( ) {
"use strict" ;
var app = document . getElementById ( "app" ) ;
var dialog = document . getElementById ( "fall-dialog" ) ;
var acknowledgeButton = document . getElementById ( "acknowledge-button" ) ;
var stateButtons = Array . prototype . slice . call ( document . querySelectorAll ( "[data-state-target]" ) ) ;
var topTabs = Array . prototype . slice . call ( document . querySelectorAll ( "[data-tab-target]" ) ) ;
var monitorPanel = document . getElementById ( "monitor-panel" ) ;
var settingsPanel = document . getElementById ( "settings-panel" ) ;
var liveStatus = document . getElementById ( "live-status" ) ;
var toast = document . getElementById ( "toast" ) ;
var monitorToggle = document . getElementById ( "monitor-toggle" ) ;
var settingsForm = document . getElementById ( "settings-form" ) ;
var resetSettingsButton = document . getElementById ( "reset-settings" ) ;
var settingInputs = Array . prototype . slice . call ( settingsForm . querySelectorAll ( "input" ) ) ;
var configState = document . getElementById ( "config-state" ) ;
var configStateText = document . getElementById ( "config-state-text" ) ;
var draftVersion = document . getElementById ( "draft-version" ) ;
var activeTab = "monitor" ;
var lastInvoker = null ;
var confirmedDialogShown = false ;
var toastTimer = null ;
var draftRevision = 2 ;
var defaults = { } ;
settingInputs . forEach ( function ( input ) { defaults [ input . id ] = input . value ; } ) ;
var views = {
normal : {
sceneState : "NORMAL" ,
source : "来源在线 · 模拟画面" ,
track : "P-017 · 姿态可用" ,
title : "NORMAL · 正常监控" ,
mark : "✓" ,
time : "稳定观察中" ,
description : "当前人员姿态稳定,没有生成摔倒候选或报警副作用。" ,
caption : "全身大部分可见,姿态质量满足监控条件。" ,
people : "1 · P-017" , quality : "良好" , latency : "—" , alert : "未触发" ,
evidenceStatus : "当前无待处理事件" , artifactStatus : "监控中" , artifactTitle : "本地声音:未触发" , artifactPath : "当前没有已保存截图。" ,
evidence : [ [ "视频来源" , "在线" ] , [ "姿态质量" , "通过" ] , [ "状态机" , "NORMAL" ] ] ,
events : [ [ "持续监控" , "P-017 姿态稳定,未生成候选" , "normal" ] , [ "质量门控通过" , "全身关键点可用于后续判断" , "normal" ] , [ "报警保持静默" , "无确认事件,不播放声音" , "normal" ] ] ,
announcement : "已切换到正常监控。当前没有生成摔倒事件或报警。"
} ,
confirmed : {
sceneState : "CONFIRMED · FALL-00017" ,
source : "来源在线 · 事件已确认" ,
track : "P-017 · 确认倒地" ,
title : "CONFIRMED · 已确认摔倒" ,
mark : "!" ,
time : "12:08:42 · 已确认" ,
description : "倒地证据持续 1.8 秒,已创建单个事件并完成声音、弹窗与截图演示。" ,
caption : "红色仅表示确认摔倒事件及其证据;关闭弹窗后,事件证据保持可追溯。" ,
people : "1 · P-017" , quality : "良好" , latency : "1.8 秒" , alert : "已触发一次" ,
evidenceStatus : "确认事件 · 证据已冻结" , artifactStatus : "FALL-00017" , artifactTitle : "声音与截图:已触发(演示)" , artifactPath : "events/20260720/FALL-00017.png · 原型不创建文件" ,
evidence : [ [ "事件编号" , "FALL-00017" ] , [ "确认延迟" , "1.8 秒" ] , [ "报警副作用" , "一次" ] , [ "配置版本" , "cfg-20260720-01" ] ] ,
events : [ [ "确认摔倒" , "12:08:42 · P-017 · FALL-00017" , "confirmed" ] , [ "截图已保存" , "仅展示演示路径,不写入文件" , "confirmed" ] , [ "声音已播放" , "同一事件不重复播放" , "confirmed" ] ] ,
announcement : "已确认摔倒事件 FALL-00017。声音、弹窗和截图证据均只触发一次。"
} ,
offline : {
sceneState : "SOURCE RETRYING" ,
source : "视频源异常 · 正在重连" ,
track : "上一帧姿态已冻结" ,
title : "SOURCE ERROR · 连接异常" ,
mark : "—" ,
time : "重试第 2 次" ,
description : "视频源暂不可用。系统停止推进人员和倒地证据;断流本身不代表摔倒。" ,
caption : "正在重连期间保留最后一帧演示画面,不生成新的人员或摔倒结论。" ,
people : "—" , quality : "不可用" , latency : "—" , alert : "未触发" ,
evidenceStatus : "事件处理已暂停" , artifactStatus : "重连中" , artifactTitle : "本地声音:未触发" , artifactPath : "等待视频源恢复后继续监控。" ,
evidence : [ [ "视频源状态" , "重连中" ] , [ "人员更新" , "已暂停" ] , [ "摔倒事件" , "不触发" ] ] ,
events : [ [ "视频源不可用" , "12:08:42 · 已保留最后一帧" , "offline" ] , [ "第 2 次重连等待中" , "4 秒后再次尝试" , "offline" ] , [ "报警保持静默" , "连接异常不生成摔倒事件" , "offline" ] ] ,
announcement : "视频源连接异常,正在重连。没有生成摔倒事件或报警。"
}
} ;
function setText ( id , value ) { document . querySelector ( id ) . textContent = value ; }
function eventMarkup ( rows ) {
return rows . map ( function ( row ) {
return "<div class='event-row'><span class='event-symbol " + row [ 2 ] + "' aria-hidden='true'></span><div class='event-copy'><strong>" + row [ 0 ] + "</strong><span>" + row [ 1 ] + "</span></div></div>" ;
} ) . join ( "" ) ;
}
function evidenceMarkup ( rows ) {
return rows . map ( function ( row ) {
return "<div class='evidence-row'><span class='evidence-shape' aria-hidden='true'></span><strong>" + row [ 0 ] + "</strong><span>" + row [ 1 ] + "</span></div>" ;
} ) . join ( "" ) ;
}
function showToast ( message ) {
toast . textContent = message ;
toast . classList . add ( "is-visible" ) ;
window . clearTimeout ( toastTimer ) ;
toastTimer = window . setTimeout ( function ( ) { toast . classList . remove ( "is-visible" ) ; } , 3600 ) ;
}
function setActiveTab ( nextTab , moveFocus ) {
if ( nextTab !== "monitor" && nextTab !== "settings" ) return ;
activeTab = nextTab ;
topTabs . forEach ( function ( tab ) {
var selected = tab . dataset . tabTarget === nextTab ;
tab . setAttribute ( "aria-selected" , String ( selected ) ) ;
tab . setAttribute ( "tabindex" , selected ? "0" : "-1" ) ;
if ( selected && moveFocus ) tab . focus ( ) ;
} ) ;
monitorPanel . hidden = nextTab !== "monitor" ;
settingsPanel . hidden = nextTab !== "settings" ;
liveStatus . textContent = nextTab === "settings" ? "已切换至设置。草稿保存后在下一次开始监控时生效。" : "已切换至实时监控。当前运行配置保持不变。" ;
}
function setDemoState ( nextState , invoker ) {
var view = views [ nextState ] ;
if ( ! view ) return ;
if ( invoker ) lastInvoker = invoker ;
app . dataset . state = nextState ;
stateButtons . forEach ( function ( button ) { button . setAttribute ( "aria-pressed" , String ( button . dataset . stateTarget === nextState ) ) ; } ) ;
setText ( "#scene-state" , view . sceneState ) ; setText ( "#source-status" , view . source ) ; setText ( "#track-label" , view . track ) ;
setText ( "#state-heading" , view . title ) ; setText ( "#state-mark" , view . mark ) ; setText ( "#state-time" , view . time ) ;
setText ( "#state-description" , view . description ) ; setText ( "#scene-caption" , view . caption ) ;
setText ( "#metric-people" , view . people ) ; setText ( "#metric-quality" , view . quality ) ; setText ( "#metric-latency" , view . latency ) ; setText ( "#metric-alert" , view . alert ) ;
setText ( "#evidence-status" , view . evidenceStatus ) ; setText ( "#artifact-status" , view . artifactStatus ) ; setText ( "#artifact-title" , view . artifactTitle ) ; setText ( "#artifact-path" , view . artifactPath ) ;
setText ( "#event-count" , nextState === "confirmed" ? "1 个确认事件" : view . artifactStatus ) ;
document . getElementById ( "evidence-panel" ) . innerHTML = evidenceMarkup ( view . evidence ) ;
document . getElementById ( "event-list" ) . innerHTML = eventMarkup ( view . events ) ;
liveStatus . textContent = view . announcement ;
if ( nextState === "confirmed" && ! confirmedDialogShown ) {
confirmedDialogShown = true ;
setText ( "#dialog-time" , document . getElementById ( "scene-time" ) . textContent ) ;
if ( typeof dialog . showModal === "function" && ! dialog . open ) {
dialog . showModal ( ) ;
window . setTimeout ( function ( ) { acknowledgeButton . focus ( ) ; } , 0 ) ;
}
}
if ( nextState !== "confirmed" ) {
confirmedDialogShown = false ;
if ( dialog . open ) dialog . close ( ) ;
}
}
function markConfigDirty ( ) {
settingsPanel . dataset . configState = "dirty" ;
configState . dataset . configState = "dirty" ;
configStateText . textContent = "设置已修改,尚未保存" ;
draftVersion . textContent = "cfg-20260720-0" + ( draftRevision + 1 ) + " · 编辑中" ;
}
function resetSettings ( ) {
settingInputs . forEach ( function ( input ) { input . value = defaults [ input . id ] ; } ) ;
markConfigDirty ( ) ;
showToast ( "已恢复演示默认值;请保存后在下一次启动监控时应用。" ) ;
}
function saveSettings ( ) {
draftRevision += 1 ;
settingsPanel . dataset . configState = "saved" ;
configState . dataset . configState = "saved" ;
configStateText . textContent = "草稿已保存,下一次启动生效" ;
draftVersion . textContent = "cfg-20260720-0" + draftRevision + " · 已保存待应用" ;
liveStatus . textContent = "设置草稿已保存。当前监控仍使用 cfg-20260720-01,下一次开始监控时才使用新版本。" ;
showToast ( "演示草稿已保存;运行配置未被热修改。" ) ;
}
function updateClock ( ) {
var time = new Date ( ) . toLocaleTimeString ( "zh-CN" , { hour12 : false } ) ;
document . getElementById ( "clock" ) . textContent = time ;
document . getElementById ( "scene-time" ) . textContent = time ;
}
topTabs . forEach ( function ( tab ) { tab . addEventListener ( "click" , function ( ) { setActiveTab ( tab . dataset . tabTarget , false ) ; } ) ; } ) ;
stateButtons . forEach ( function ( button ) { button . addEventListener ( "click" , function ( ) { setDemoState ( button . dataset . stateTarget , button ) ; } ) ; } ) ;
settingInputs . forEach ( function ( input ) { input . addEventListener ( "input" , markConfigDirty ) ; } ) ;
resetSettingsButton . addEventListener ( "click" , resetSettings ) ;
settingsForm . addEventListener ( "submit" , function ( event ) { event . preventDefault ( ) ; saveSettings ( ) ; } ) ;
acknowledgeButton . addEventListener ( "click" , function ( ) { dialog . close ( ) ; } ) ;
dialog . addEventListener ( "close" , function ( ) { if ( lastInvoker && typeof lastInvoker . focus === "function" ) lastInvoker . focus ( ) ; } ) ;
monitorToggle . addEventListener ( "click" , function ( ) {
var running = app . dataset . running === "true" ;
app . dataset . running = String ( ! running ) ;
monitorToggle . setAttribute ( "aria-pressed" , String ( ! running ) ) ;
monitorToggle . querySelector ( "span" ) . textContent = running ? "开始模拟监控" : "停止模拟监控" ;
showToast ( running ? "原型已暂停更新:未停止任何真实视频源。" : "原型已恢复更新:仍未连接真实视频源。" ) ;
} ) ;
document . getElementById ( "artifact-button" ) . addEventListener ( "click" , function ( ) {
showToast ( app . dataset . state === "confirmed" ? "演示截图位置:events/20260720/FALL-00017.png(原型不创建文件)。" : "当前没有已保存截图;切换到“确认摔倒”可查看报警工件状态。" ) ;
} ) ;
document . addEventListener ( "keydown" , function ( event ) {
if ( event . altKey || event . ctrlKey || event . metaKey ) return ;
var tabIndex = topTabs . indexOf ( document . activeElement ) ;
if ( tabIndex !== - 1 && ( event . key === "ArrowLeft" || event . key === "ArrowRight" ) ) {
event . preventDefault ( ) ;
var direction = event . key === "ArrowRight" ? 1 : - 1 ;
var nextIndex = ( tabIndex + direction + topTabs . length ) % topTabs . length ;
setActiveTab ( topTabs [ nextIndex ] . dataset . tabTarget , true ) ;
return ;
}
if ( document . activeElement && document . activeElement . matches ( "input" ) ) return ;
var stateMap = { "1" : "normal" , "2" : "confirmed" , "3" : "offline" } ;
if ( activeTab === "monitor" && stateMap [ event . key ] ) setDemoState ( stateMap [ event . key ] , document . querySelector ( "[data-state-target='" + stateMap [ event . key ] + "']" ) ) ;
if ( event . key === "Escape" && dialog . open ) dialog . close ( ) ;
} ) ;
if ( window . location . hash === "#settings" ) setActiveTab ( "settings" , false ) ;
var initialState = window . location . hash === "#confirmed" ? "confirmed" : window . location . hash === "#offline" ? "offline" : "normal" ;
setDemoState ( initialState ) ;
updateClock ( ) ;
window . setInterval ( updateClock , 1000 ) ;
} ( ) ) ;
< / script >
< / body >
< / html >