6161 <!-- SW 版本检测和强制更新(必须在最开头) -->
6262 < script data-cfasync ="false ">
6363 ( function ( ) {
64- var REQUIRED_SW_VERSION = 'v23 ' ;
64+ var REQUIRED_SW_VERSION = 'v24 ' ;
6565 var STORAGE_KEY = 'sw_cleared_' + REQUIRED_SW_VERSION ;
6666
6767 // 如果已经为这个版本清理过,不再重复
117117 < link rel ="icon " href ="icon.png " type ="image/png ">
118118 < meta name ="theme-color " content ="#141414 ">
119119
120+ <!-- iOS PWA 支持(iOS 不读 manifest,需要这些 meta 才能添加到主屏并全屏) -->
121+ < meta name ="apple-mobile-web-app-capable " content ="yes ">
122+ < meta name ="mobile-web-app-capable " content ="yes ">
123+ < meta name ="apple-mobile-web-app-status-bar-style " content ="black-translucent ">
124+ < meta name ="apple-mobile-web-app-title " content ="E视界 ">
125+ < link rel ="apple-touch-icon " href ="icon.png ">
126+
120127 <!-- 关键 CSS (优先加载) -->
121128 < link href ="libs/css/bootstrap.min.css " rel ="stylesheet ">
122129 < link rel ="stylesheet " href ="libs/css/fontawesome.min.css ">
134141 < script data-cfasync ="false " src ="libs/js/bootstrap.bundle.min.js " defer > </ script >
135142
136143 < style >
144+ /* ♿ 尊重用户的"减少动态效果"系统偏好:大幅削弱动画/过渡,
145+ 缓解前庭功能障碍/晕动症用户的不适(保留极短时长以不破坏交互反馈) */
146+ @media (prefers-reduced-motion : reduce) {
147+ * , * ::before , * ::after {
148+ animation-duration : 0.01ms !important ;
149+ animation-iteration-count : 1 !important ;
150+ transition-duration : 0.01ms !important ;
151+ scroll-behavior : auto !important ;
152+ }
153+ }
137154 : root {
138155 --bg-color : # 141414 ;
139156 --panel-bg : # 1f1f1f ;
19641981
19651982 .source-pill {
19661983 padding : 8px 16px ;
1984+ min-height : 44px ; /* ♿ 移动端触控目标 ≥44px */
1985+ box-sizing : border-box;
19671986 border-radius : 6px ;
19681987 background : # 2a2a2a ;
19691988 border : 1px solid # 333 ;
20032022 border : 1px solid transparent;
20042023 color : # ddd ;
20052024 padding : 12px 5px ;
2025+ min-height : 44px ; /* ♿ 移动端触控目标 ≥44px */
2026+ box-sizing : border-box;
2027+ display : flex;
2028+ align-items : center;
2029+ justify-content : center;
20062030 text-align : center;
20072031 border-radius : 6px ;
20082032 font-size : 14px ;
@@ -2968,8 +2992,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
29682992
29692993 <!-- 横向滚动 -->
29702994 < div class ="scroll-container ">
2971- < button class ="scroll-btn left " v-show ="historyCanScrollLeft " @click ="scrollHistoryRow(-1) "> < i
2972- class ="fas fa-chevron-left "> </ i > </ button >
2995+ < button class ="scroll-btn left " v-show ="historyCanScrollLeft " @click ="scrollHistoryRow(-1) " aria-label =" 向左滚动观看历史 " > < i
2996+ class ="fas fa-chevron-left " aria-hidden =" true " > </ i > </ button >
29732997 < div class ="hot-row " ref ="watchHistoryRow " @scroll ="onHistoryScroll ">
29742998 < div class ="history-card " v-for ="(item, index) in watchHistory " :key ="item.name + index ">
29752999 <!-- 删除按钮 (放在 card 外层避免随缩放移动) -->
@@ -3002,8 +3026,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30023026 < div class ="movie-date " style ="font-size: 11px; "> {{ formatWatchTime(item.watchedAt) }}</ div >
30033027 </ div >
30043028 </ div >
3005- < button class ="scroll-btn right " v-show ="historyCanScrollRight " @click ="scrollHistoryRow(1) "> < i
3006- class ="fas fa-chevron-right "> </ i > </ button >
3029+ < button class ="scroll-btn right " v-show ="historyCanScrollRight " @click ="scrollHistoryRow(1) " aria-label =" 向右滚动观看历史 " > < i
3030+ class ="fas fa-chevron-right " aria-hidden =" true " > </ i > </ button >
30073031 </ div >
30083032 </ div >
30093033
@@ -3049,8 +3073,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30493073 < template v-if ="rowLists[key] && rowLists[key].length > 0 ">
30503074 <!-- 收起状态:横向滚动 -->
30513075 < div class ="scroll-container " v-show ="!expandedRows[key] ">
3052- < button class ="scroll-btn left " @click ="scrollRow(key, -1) " v-show ="rowLeftButtons[key] "> < i
3053- class ="fas fa-chevron-left "> </ i > </ button >
3076+ < button class ="scroll-btn left " @click ="scrollRow(key, -1) " v-show ="rowLeftButtons[key] " aria-label =" 向左滚动 " > < i
3077+ class ="fas fa-chevron-left " aria-hidden =" true " > </ i > </ button >
30543078 < div class ="hot-row " :ref ="key " @scroll ="onRowScroll(key) ">
30553079 < div class ="hot-card " v-for ="item in rowLists[key] " :key ="item.id "
30563080 @click ="autoSearch(item.title || item.name, item.original_title || item.original_name) ">
@@ -3067,8 +3091,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30673091 </ div >
30683092 </ div >
30693093 </ div >
3070- < button class ="scroll-btn right " @click ="scrollRow(key, 1) "> < i
3071- class ="fas fa-chevron-right "> </ i > </ button >
3094+ < button class ="scroll-btn right " @click ="scrollRow(key, 1) " aria-label =" 向右滚动 " > < i
3095+ class ="fas fa-chevron-right " aria-hidden =" true " > </ i > </ button >
30723096 </ div >
30733097 <!-- 展开状态:分页网格布局 -->
30743098 < div class ="grid-expanded " v-show ="expandedRows[key] ">
@@ -3277,7 +3301,7 @@ <h2>{{ infoModalData.title || infoModalData.name }}</h2>
32773301
32783302 <!-- ★★★ 播放详情页 (影院布局) ★★★ -->
32793303 < div class ="detail-overlay " :class ="{ active: showDetail } ">
3280- < div class ="close-btn " tabindex ="1 " @click ="closeDetail " @keydown.enter ="closeDetail "> < i
3304+ < div class ="close-btn " tabindex ="1 " role =" button " aria-label =" 关闭 " @click ="closeDetail " @keydown.enter ="closeDetail "> < i
32813305 class ="fas fa-times "> </ i > </ div >
32823306
32833307 < div class ="player-layout " v-if ="currentGroup ">
@@ -3494,7 +3518,7 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
34943518 function showPasswordError ( message ) {
34953519 const errorDiv = document . getElementById ( 'password-error' ) ;
34963520 const errorText = document . getElementById ( 'password-error-text' ) ;
3497- errorText . textContent = message ;
3521+ errorText . textContent = message ; // 用 textContent 防止服务器返回的错误信息触发 XSS
34983522 errorDiv . style . display = 'flex' ;
34993523 }
35003524
@@ -4071,7 +4095,8 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
40714095 if ( ! groups [ name ] ) groups [ name ] = { name : name , pic : item . vod_pic , sources : [ ] } ;
40724096 const isDefault = ( url ) => ! url || url . includes ( 'mac_default' ) || url . includes ( 'nopic' ) ;
40734097 if ( isDefault ( groups [ name ] . pic ) && ! isDefault ( item . vod_pic ) ) groups [ name ] . pic = item . vod_pic ;
4074- if ( isDefault ( groups [ name ] . pic ) ) this . handleImgError ( name ) ;
4098+ // 注:图片回退(handleImgError)已在搜索 onmessage 中按新到达的项处理,
4099+ // computed 内不再调用,保持纯函数、避免渲染期副作用引发的重渲染抖动。
40754100
40764101 groups [ name ] . sources . push ( {
40774102 ...item ,
@@ -4670,7 +4695,7 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
46704695 box-shadow: 0 10px 40px rgba(0,0,0,0.5);
46714696 border: 1px solid rgba(229, 9, 20, 0.3);
46724697 ` ;
4673- toast . innerHTML = message ;
4698+ toast . textContent = message ; // textContent 防 XSS
46744699 document . body . appendChild ( toast ) ;
46754700
46764701 // 延迟刷新以便用户看到提示
@@ -7393,13 +7418,31 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
73937418
73947419 let progressRestored = false ;
73957420
7396- // 注:DPlayer.switchVideo 不会销毁旧 Hls 实例(库层泄漏,待后续更稳妥地处理)。
7397- // 此处不主动 destroy,因为 hls.destroy() 会 detach media 触发 <video> 短暂报错,
7398- // 可能被错误处理误判为"播放失败"而误跳过实际可播的线路。
7421+ // 🧹 修复 Hls 实例泄漏:DPlayer.switchVideo 会新建 Hls 但不销毁旧的。
7422+ // 安全时序——先捕获旧实例,switchVideo 创建新实例后,等"新流真正 playing"
7423+ // 再销毁旧的:此时 <video> 已挂上可用的新源,detach 旧实例不会触发"无源"报错,
7424+ // 从而既回收泄漏又不会误跳过线路(旧实现是在 switchVideo 前 destroy 才出的问题)。
7425+ const _oldHls = ( dp . plugins && dp . plugins . hls ) || null ;
73997426
74007427 dp . switchVideo ( { url : finalUrl , type : 'hls' } ) ;
74017428 console . log ( '[进度记忆-切换] switchVideo 已调用' + ( finalUrl !== url ? ' (代理模式)' : '' ) ) ;
74027429
7430+ if ( _oldHls ) {
7431+ let _destroyed = false ; // 防止 'playing' 与 5s 兜底重复销毁
7432+ const destroyOldHls = ( ) => {
7433+ if ( _destroyed ) return ;
7434+ _destroyed = true ;
7435+ if ( dp . video ) dp . video . removeEventListener ( 'playing' , destroyOldHls ) ;
7436+ // 仅当它确实是被替换掉的旧实例时才销毁,避免误杀当前实例
7437+ if ( _oldHls !== ( dp . plugins && dp . plugins . hls ) ) {
7438+ try { _oldHls . destroy ( ) ; } catch ( e ) { }
7439+ }
7440+ } ;
7441+ if ( dp . video ) dp . video . addEventListener ( 'playing' , destroyOldHls ) ;
7442+ // 兜底:5秒后无论是否 playing 都回收(仍只在它非当前实例时)
7443+ setTimeout ( destroyOldHls , 5000 ) ;
7444+ }
7445+
74037446 // ▶ 修复"切换资源/集数后不自动播放 + 中央大按钮图标反向":
74047447 // switchVideo 会复用同一个 <video> 元素但重置 src,HLS 尚未就绪时立刻 dp.play()
74057448 // 既无法真正播放,又会让 dp.on('play') 乐观地把中央按钮设成⏸而与实际(暂停)相反。
0 commit comments