|
54 | 54 | border-radius: var(--radius); |
55 | 55 | overflow:hidden; |
56 | 56 | box-shadow: 0 6px 18px rgba(0,0,0,.08); |
57 | | - transition: tr |
| 57 | + transition: transform .18s ease, box-shadow .18s ease; |
| 58 | + } |
| 59 | + @media (hover:hover){ |
| 60 | + .card:hover{ |
| 61 | + transform: translateY(-2px); |
| 62 | + box-shadow: 0 10px 28px rgba(0,0,0,.12); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + .player{ position:relative; aspect-ratio:16/9; background:#000; } |
| 67 | + video, img{ width:100%; height:100%; object-fit:cover; display:block; border:0; } |
| 68 | + </style> |
| 69 | +</head> |
| 70 | +<body> |
| 71 | + |
| 72 | + <!-- 新增的大标题 --> |
| 73 | + <header> |
| 74 | + <div class="wrap"> |
| 75 | + <h1>Expert Policy</h1> |
| 76 | + </div> |
| 77 | + </header> |
| 78 | + |
| 79 | + <main class="grid" id="grid"></main> |
| 80 | + |
| 81 | +<script> |
| 82 | +async function loadManifest(){ |
| 83 | + const res = await fetch('manifest.json', { cache: 'no-store' }); |
| 84 | + if(!res.ok) throw new Error('manifest.json 加载失败'); |
| 85 | + return res.json(); |
| 86 | +} |
| 87 | + |
| 88 | +/* 洗牌:Fisher–Yates */ |
| 89 | +function shuffle(arr){ |
| 90 | + for(let i = arr.length - 1; i > 0; i--){ |
| 91 | + const j = Math.floor(Math.random() * (i + 1)); |
| 92 | + [arr[i], arr[j]] = [arr[j], arr[i]]; |
| 93 | + } |
| 94 | + return arr; |
| 95 | +} |
| 96 | + |
| 97 | +function makeCard(item){ |
| 98 | + const card = document.createElement('article'); |
| 99 | + card.className = 'card'; |
| 100 | + |
| 101 | + const player = document.createElement('div'); |
| 102 | + player.className = 'player'; |
| 103 | + |
| 104 | + // 先放海报,进入视口再换成 <video> |
| 105 | + const poster = document.createElement('img'); |
| 106 | + poster.alt = item.title || item.file || ''; |
| 107 | + poster.loading = 'lazy'; |
| 108 | + poster.decoding = 'async'; |
| 109 | + poster.src = item.poster || ''; |
| 110 | + if(!item.poster){ poster.style.background = '#000'; } |
| 111 | + player.appendChild(poster); |
| 112 | + |
| 113 | + card.appendChild(player); |
| 114 | + |
| 115 | + // 懒加载:出现时才创建 <video> |
| 116 | + const io = new IntersectionObserver(entries => { |
| 117 | + for(const e of entries){ |
| 118 | + if(e.isIntersecting){ |
| 119 | + io.disconnect(); |
| 120 | + const v = document.createElement('video'); |
| 121 | + v.setAttribute('controls', ''); |
| 122 | + v.setAttribute('playsinline',''); |
| 123 | + v.setAttribute('preload','metadata'); |
| 124 | + if(item.poster) v.setAttribute('poster', item.poster); |
| 125 | + v.src = item.src; // 相对路径,如 videos/xxx.mp4 |
| 126 | + player.replaceChild(v, poster); |
| 127 | + } |
| 128 | + } |
| 129 | + }, { rootMargin: '160px' }); |
| 130 | + io.observe(card); |
| 131 | + |
| 132 | + return card; |
| 133 | +} |
| 134 | + |
| 135 | +function render(list){ |
| 136 | + const grid = document.getElementById('grid'); |
| 137 | + grid.innerHTML = ''; |
| 138 | + for(const item of list){ grid.appendChild(makeCard(item)); } |
| 139 | +} |
| 140 | + |
| 141 | +/* 加载清单 → 打乱 → 渲染一次 */ |
| 142 | +loadManifest() |
| 143 | + .then(list => { render(shuffle(list.slice())); }) |
| 144 | + .catch(err => { |
| 145 | + const grid = document.getElementById('grid'); |
| 146 | + grid.innerHTML = `<p style="opacity:.8;padding:16px">加载清单失败:${err.message}。请确认 manifest.json 已生成并在仓库根目录。</p>`; |
| 147 | + }); |
| 148 | +</script> |
| 149 | +</body> |
| 150 | +</html> |
0 commit comments