88( function ( ) { var t = localStorage . getItem ( 'theme' ) ; if ( t !== 'light' && t !== 'dark' ) { var h = new Date ( ) . getHours ( ) ; t = ( h >= 7 && h < 19 ) ?'light' :'dark' ; } document . documentElement . setAttribute ( 'data-theme' , t ) ; } ) ( ) ; </ script >
99< link rel ="preconnect " href ="https://fonts.googleapis.com ">
1010< link rel ="preconnect " href ="https://fonts.gstatic.com " crossorigin >
11- < link href ="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&family=Noto+Sans+SC:wght@400;500;700;900&display=swap " rel ="stylesheet ">
11+ < link rel ="preload " as ="style " href ="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&family=Noto+Sans+SC:wght@400;500;700;900&display=swap " onload ="this.onload=null;this.rel='stylesheet' ">
12+ < noscript > < link rel ="stylesheet " href ="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&family=Noto+Sans+SC:wght@400;500;700;900&display=swap "> </ noscript >
1213< style >
1314 : root {
1415 --brand : # FD4E63 ; --brand-ink : # D5344E ; --brand2 : # FF3B3B ;
1516 --grad : linear-gradient (135deg , # FB7299 0% , # FF3B3B 100% );
1617 --brand-soft : rgba (253 , 78 , 99 , .12 );
17- --bili : # FB7299 ; --yt : # FF0000 ; --ok : # 1F9D55 ;
18- --ink : # 15181C ; --muted : # 6A7480 ; --faint : # 9AA4AD ;
18+ --bili : # FB7299 ; --yt : # FF0000 ; --ok : # 1F9D55 ; --ok-text : # 157A42 ;
19+ --ink : # 15181C ; --muted : # 5E6873 ; --faint : # 7C8893 ;
1920 --line : # EAEDF0 ; --card : # FFFFFF ; --bg : # F6F7F9 ;
2021 --nav-bg : rgba (255 , 255 , 255 , .82 );
2122 --input-bg : # FFFFFF ; --code-bg : # F0F2F4 ;
3031 --brand : # FF6E83 ; --brand-ink : # FF6E83 ; --brand2 : # FF5A5A ;
3132 --grad : linear-gradient (135deg , # FF8FB0 0% , # FF5A5A 100% );
3233 --brand-soft : rgba (255 , 110 , 131 , .16 );
33- --bili : # FF8FB0 ; --yt : # FF4D4D ; --ok : # 37C871 ;
34+ --bili : # FF8FB0 ; --yt : # FF4D4D ; --ok : # 37C871 ; --ok-text : # 37C871 ;
3435 --ink : # EDF0F3 ; --muted : # 9BA7B3 ; --faint : # 6C7783 ;
3536 --line : # 262C34 ; --card : # 181C22 ; --bg : # 0F1216 ;
3637 --nav-bg : rgba (18 , 22 , 26 , .80 );
9091 .hero .meaning {margin-top : 16px ;font-size : 17px ;font-weight : 600 ;color : var (--brand-ink )}
9192 .hero .desc {margin-top : 8px ;font-size : 15.5px ;color : var (--muted );max-width : 680px ;line-height : 1.7 }
9293 .hero .privacy {display : inline-flex;align-items : center;gap : 7px ;margin-top : 18px ;font-size : 12.5px ;
93- font-weight : 600 ;color : var (--ok );background : rgba (31 , 157 , 85 , .12 );padding : 5px 13px ;border-radius : 999px }
94+ font-weight : 600 ;color : var (--ok-text );background : rgba (31 , 157 , 85 , .12 );padding : 5px 13px ;border-radius : 999px }
9495 .hero .privacy svg {width : 13px ;height : 13px }
9596
9697 /* ——— layout ——— */
112113 /* ——— pills (status) ——— */
113114 .pill {display : inline-flex;align-items : center;gap : 6px ;font-size : 12px ;font-weight : 700 ;
114115 border-radius : 999px ;padding : 4px 11px ;margin-left : 8px }
115- .pill .on {background : rgba (31 , 157 , 85 , .13 );color : var (--ok )}
116+ .pill .on {background : rgba (31 , 157 , 85 , .13 );color : var (--ok-text )}
116117 .pill .off {background : rgba (140 , 150 , 160 , .16 );color : var (--muted )}
117118
118119 /* ——— platform chips ——— */
196197
197198 /* ——— creators 表的「数据」列 ——— */
198199 .dcell {font-variant-numeric : tabular-nums;font-weight : 600 ;font-size : 12.5px ;color : var (--ink );white-space : nowrap}
199- .dcell .ddate {color : var (--faint );font-weight : 500 }
200+ .dcell .ddate {color : var (--muted );font-weight : 500 }
200201
201202 /* ——— toolbar ——— */
202203 .toolbar {display : flex;gap : 10px ;flex-wrap : wrap;margin : 16px 0 6px ;align-items : center}
229230 font-variant-numeric : tabular-nums}
230231 .vstats span {display : inline-flex;align-items : center;gap : 4px }
231232 .vstats svg {width : 13px ;height : 13px ;opacity : .65 }
232- .empty {grid-column : 1 / -1 ;text-align : center;color : var (--faint );padding : 38px 0 ;font-size : 14px }
233+ .empty {grid-column : 1 / -1 ;text-align : center;color : var (--muted );padding : 38px 0 ;font-size : 14px }
233234 .more-wrap {text-align : center;margin-top : 22px }
234235 # vmore {display : none}
235236
247248 background : var (--ink );color : var (--bg );font-size : 13.5px ;font-weight : 600 ;padding : 12px 22px ;border-radius : 13px ;
248249 box-shadow : 0 16px 44px -16px rgba (0 , 0 , 0 , .55 );opacity : 0 ;pointer-events : none;transition : .25s ;z-index : 60 ;max-width : 80vw }
249250 # toast .show {opacity : 1 ;transform : translateX (-50% ) translateY (0 )}
250- .foot {text-align : center;color : var (--faint );font-size : 12.5px ;margin-top : 8px }
251+ .foot {text-align : center;color : var (--muted );font-size : 12.5px ;margin-top : 8px }
251252 .foot a {color : var (--brand-ink );font-weight : 600 }
252253 .foot a : hover {text-decoration : underline}
253254
255+ /* ——— 键盘焦点可见环(WCAG 2.4.7):统一所有可聚焦控件 ——— */
256+ : focus-visible {outline : 2px solid var (--brand );outline-offset : 2px }
257+ input : focus-visible , .sel : focus-visible {outline : none;border-color : var (--brand );
258+ box-shadow : 0 0 0 3px var (--brand-soft ), 0 0 0 1.5px var (--brand )}
259+ .chip : has (input : focus-visible ){border-color : var (--brand );box-shadow : 0 0 0 3px var (--brand-soft )}
260+ .vcard : focus-visible {border-color : transparent}
261+
254262 @media (max-width : 560px ){
255263 .hero h1 {font-size : 38px } .hero-inner {padding : 46px 22px 40px }
256264 .card {padding : 22px 18px } .head-actions {width : 100% ;margin-left : 0 }
257265 }
266+
267+ /* ——— 尊重系统「减少动态效果」偏好(prefers-reduced-motion)——— */
268+ @media (prefers-reduced-motion : reduce){
269+ html {scroll-behavior : auto}
270+ * , * ::before , * ::after {animation-duration : .001ms !important ;
271+ animation-iteration-count : 1 !important ;transition-duration : .001ms !important }
272+ body .lswitch .hero , body .lswitch .wrap {opacity : 1 }
273+ }
258274</ style >
259275</ head >
260276< body >
@@ -304,11 +320,11 @@ <h1>vira<span class="hl">lens</span></h1>
304320 </ div >
305321
306322 < div class ="key-row " id ="row-bili ">
307- < label class ="fld " data-i18n ="sessLabel "> </ label >
323+ < label class ="fld " for =" sessdata " data-i18n ="sessLabel "> </ label >
308324 < input id ="sessdata " type ="password " autocomplete ="off " data-i18n-ph ="sessPh ">
309325 </ div >
310326 < div class ="key-row " id ="row-yt ">
311- < label class ="fld " data-i18n ="ytLabel "> </ label >
327+ < label class ="fld " for =" ytkey " data-i18n ="ytLabel "> </ label >
312328 < input id ="ytkey " type ="password " autocomplete ="off " data-i18n-ph ="ytPh ">
313329 </ div >
314330
@@ -379,17 +395,17 @@ <h1>vira<span class="hl">lens</span></h1>
379395 < div class ="toolbar ">
380396 < div class ="search ">
381397 < svg viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 "> < circle cx ="11 " cy ="11 " r ="7 "/> < path d ="m20 20-3.5-3.5 "/> </ svg >
382- < input id ="vsearch " oninput ="resetPage();renderGrid() " data-i18n-ph ="searchPh ">
398+ < input id ="vsearch " type =" text " oninput ="resetPage();renderGrid() " data-i18n-ph ="searchPh " data-i18n-aria =" searchAria ">
383399 </ div >
384- < select class ="sel " id ="vsort " onchange ="resetPage();renderGrid() ">
400+ < select class ="sel " id ="vsort " onchange ="resetPage();renderGrid() " data-i18n-aria =" sortAria " >
385401 < option value ="play " data-i18n ="sortPlay "> </ option >
386402 < option value ="date " data-i18n ="sortDate "> </ option >
387403 < option value ="comment " data-i18n ="sortComment "> </ option >
388404 < option value ="like " data-i18n ="sortLike "> </ option >
389405 < option value ="danmaku " data-i18n ="sortDanmaku "> </ option >
390406 < option value ="duration_sec " data-i18n ="sortDur "> </ option >
391407 </ select >
392- < select class ="sel " id ="vplat " onchange ="resetPage();renderGrid() ">
408+ < select class ="sel " id ="vplat " onchange ="resetPage();renderGrid() " data-i18n-aria =" platAria " >
393409 < option value ="all " data-i18n ="platAll "> </ option >
394410 < option value ="bilibili "> Bilibili</ option >
395411 < option value ="youtube "> YouTube</ option >
@@ -454,6 +470,8 @@ <h1>vira<span class="hl">lens</span></h1>
454470 tDoneReport :"跑完了 ✓ 可以开报告" , tDoneData :"数据取好了 ✓" , tErr :"出错了,看日志" ,
455471 tNeedPath :"先填文件路径" , tImportOk :( c , v ) => `导入成功:${ c } 位 · ${ v } 条` , tImportErr :e => "导入失败:" + e ,
456472 tPickOne :"至少选一个平台" ,
473+ searchAria :"搜索视频" , sortAria :"排序方式" , platAria :"按平台筛选" ,
474+ themeToDark :"切换到深色" , themeToLight :"切换到浅色" , tNeedRun :"先点「抓取并分析」生成" ,
457475 } ,
458476 en :{
459477 brandChip :"video data lens" , langBtn :"中" ,
@@ -502,6 +520,8 @@ <h1>vira<span class="hl">lens</span></h1>
502520 tDoneReport :"Done ✓ report ready" , tDoneData :"Data ready ✓" , tErr :"Failed — check the log" ,
503521 tNeedPath :"Enter a file path first" , tImportOk :( c , v ) => `Imported: ${ c } creators · ${ v } videos` , tImportErr :e => "Import failed: " + e ,
504522 tPickOne :"Pick at least one platform" ,
523+ searchAria :"Search videos" , sortAria :"Sort by" , platAria :"Filter by platform" ,
524+ themeToDark :"Switch to dark" , themeToLight :"Switch to light" , tNeedRun :"Run “Fetch & Analyze” first" ,
505525 }
506526} ;
507527/* 分区名:中英双语(原始值来自 creators.py 的 zone 字段) */
@@ -519,6 +539,8 @@ <h1>vira<span class="hl">lens</span></h1>
519539 document . getElementById ( 'langLabel' ) . textContent = L ( ) . langBtn ;
520540 document . querySelectorAll ( '[data-i18n]' ) . forEach ( el => { const v = L ( ) [ el . dataset . i18n ] ; if ( typeof v === 'string' ) el . innerHTML = v ; } ) ;
521541 document . querySelectorAll ( '[data-i18n-ph]' ) . forEach ( el => { const v = L ( ) [ el . dataset . i18nPh ] ; if ( typeof v === 'string' ) el . placeholder = v ; } ) ;
542+ document . querySelectorAll ( '[data-i18n-aria]' ) . forEach ( el => { const v = L ( ) [ el . dataset . i18nAria ] ; if ( typeof v === 'string' ) el . setAttribute ( 'aria-label' , v ) ; } ) ;
543+ updateThemeBtn ( ) ;
522544}
523545function toggleLang ( ) {
524546 lang = lang === 'zh' ?'en' :'zh' ; localStorage . setItem ( 'lang' , lang ) ;
@@ -531,7 +553,7 @@ <h1>vira<span class="hl">lens</span></h1>
531553const ICON_MOON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>' ;
532554const ICON_SUN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>' ;
533555function curTheme ( ) { return document . documentElement . getAttribute ( 'data-theme' ) || 'light' ; }
534- function updateThemeBtn ( ) { document . getElementById ( 'themeBtn' ) . innerHTML = curTheme ( ) === 'dark' ? ICON_SUN : ICON_MOON ; }
556+ function updateThemeBtn ( ) { const b = document . getElementById ( 'themeBtn' ) ; b . innerHTML = curTheme ( ) === 'dark' ? ICON_SUN : ICON_MOON ; b . setAttribute ( 'aria-label' , curTheme ( ) === 'dark' ? L ( ) . themeToLight : L ( ) . themeToDark ) ; }
535557function toggleTheme ( ) {
536558 const next = curTheme ( ) === 'dark' ? 'light' : 'dark' ;
537559 const root = document . documentElement ;
@@ -623,13 +645,17 @@ <h1>vira<span class="hl">lens</span></h1>
623645 const no_fetch = document . getElementById ( 'nofetch' ) . checked , force = document . getElementById ( 'force' ) . checked ;
624646 const platforms = selectedPlatforms ( ) ;
625647 const log = document . getElementById ( 'log' ) ; log . style . display = 'block' ; log . textContent = '…' ;
626- const r = await fetch ( '/api/run' , { method :'POST' , headers :{ 'Content-Type' :'application/json' } ,
627- body :JSON . stringify ( { mode, no_fetch, force, platforms} ) } ) ;
648+ let r ;
649+ try { r = await fetch ( '/api/run' , { method :'POST' , headers :{ 'Content-Type' :'application/json' } ,
650+ body :JSON . stringify ( { mode, no_fetch, force, platforms} ) } ) ; }
651+ catch ( e ) { log . textContent = '' ; toast ( L ( ) . tErr ) ; return ; }
628652 if ( r . status === 409 ) { toast ( L ( ) . tRunningWait ) ; return ; }
629653 setRunning ( true ) ; if ( polling ) clearInterval ( polling ) ; polling = setInterval ( poll , 900 ) ;
630654}
631655async function poll ( ) {
632- const p = await ( await fetch ( '/api/progress' ) ) . json ( ) ;
656+ let p ;
657+ try { p = await ( await fetch ( '/api/progress' ) ) . json ( ) ; }
658+ catch ( e ) { clearInterval ( polling ) ; polling = null ; setRunning ( false ) ; toast ( L ( ) . tErr ) ; return ; }
633659 const log = document . getElementById ( 'log' ) ; log . textContent = p . lines . join ( '\n' ) ; log . scrollTop = log . scrollHeight ;
634660 if ( ! p . running ) { clearInterval ( polling ) ; polling = null ; setRunning ( false ) ;
635661 await refreshStatus ( ) ; await loadData ( ) ;
@@ -689,8 +715,14 @@ <h1>vira<span class="hl">lens</span></h1>
689715 else more . style . display = 'none' ;
690716}
691717
692- function openReport ( ) { window . open ( '/report' , '_blank' ) ; } /* 自包含 HTML 报告(跑过「抓取并分析」后生成) */
693- function openDiag ( ) { window . open ( '/diagnose' , '_blank' ) ; } /* 单视频体检页(选一条视频逐维度诊断) */
718+ async function openReport ( ) { /* 自包含 HTML 报告(跑过「抓取并分析」后生成) */
719+ try { const r = await fetch ( '/report' ) ; if ( r . status === 404 ) { toast ( L ( ) . tNeedRun ) ; return ; } } catch ( e ) { }
720+ window . open ( '/report' , '_blank' ) ;
721+ }
722+ function openDiag ( ) { /* 单视频体检页(选一条视频逐维度诊断) */
723+ if ( ! STATE . total ) { toast ( L ( ) . tNeedRun ) ; return ; }
724+ window . open ( '/diagnose' , '_blank' ) ;
725+ }
694726async function doImport ( ) {
695727 const path = document . getElementById ( 'imp' ) . value . trim ( ) ;
696728 if ( ! path ) { toast ( L ( ) . tNeedPath ) ; return ; }
@@ -701,7 +733,7 @@ <h1>vira<span class="hl">lens</span></h1>
701733}
702734
703735/* ——————————————————— boot ——————————————————— */
704- updateThemeBtn ( ) ; applyLang ( ) ; restorePlatforms ( ) ; refreshStatus ( ) ; loadData ( ) ;
736+ updateThemeBtn ( ) ; applyLang ( ) ; restorePlatforms ( ) ; refreshStatus ( ) . catch ( ( ) => { } ) ; loadData ( ) . catch ( ( ) => { } ) ;
705737</ script >
706738</ body >
707739</ html >
0 commit comments