2026-06-21 15:29:36 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>播放页 · 走遍美国精听</title>
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
2026-06-21 17:21:14 +08:00
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=Public+Sans:wght@400;500;600&family=Noto+Sans+SC:wght@400;500&display=swap" rel="stylesheet">
|
2026-06-21 15:29:36 +08:00
|
|
|
|
<style>
|
|
|
|
|
|
:root{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
--paper:#f8f6f1; --surface:#ffffff;
|
|
|
|
|
|
--ink:#2a2724; --ink-soft:#56514a; --muted:#938b7e; --faint:#c2bbae;
|
|
|
|
|
|
--hair:#e9e3d8; --hair-soft:#f0ebe1;
|
|
|
|
|
|
--accent:#a4662b; --accent-2:#c0833f; --accent-soft:#f1e8d9;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
--serif:"Newsreader",Georgia,serif;
|
2026-06-21 17:21:14 +08:00
|
|
|
|
--sans:"Public Sans",system-ui,sans-serif;
|
|
|
|
|
|
--han:"Noto Sans SC",sans-serif;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
|
|
|
|
html,body{height:100%}
|
|
|
|
|
|
body{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
font-family:var(--sans); color:var(--ink); background:var(--paper);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
display:flex; justify-content:center; min-height:100%;
|
|
|
|
|
|
-webkit-font-smoothing:antialiased;
|
|
|
|
|
|
}
|
|
|
|
|
|
.phone{
|
|
|
|
|
|
width:100%; max-width:440px; min-height:100vh; position:relative;
|
2026-06-21 17:21:14 +08:00
|
|
|
|
display:flex; flex-direction:column; background:var(--paper);
|
|
|
|
|
|
border-left:1px solid var(--hair); border-right:1px solid var(--hair);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
:focus-visible{outline:2px solid var(--accent); outline-offset:3px; border-radius:6px}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---------- top bar ---------- */
|
|
|
|
|
|
.top{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
position:sticky; top:0; z-index:20; padding:16px 20px;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
display:flex; align-items:center; gap:14px;
|
2026-06-21 17:21:14 +08:00
|
|
|
|
background:rgba(248,246,241,.9); backdrop-filter:blur(8px);
|
|
|
|
|
|
border-bottom:1px solid var(--hair);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.icon-btn{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
width:40px;height:40px;border-radius:11px;border:1px solid var(--hair);
|
|
|
|
|
|
background:var(--surface); color:var(--ink-soft); display:grid;place-items:center;
|
|
|
|
|
|
cursor:pointer; transition:.2s; flex:none; font-size:18px;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.icon-btn:hover{border-color:var(--accent); color:var(--accent)}
|
|
|
|
|
|
.top-meta{flex:1; text-align:center; line-height:1.3}
|
|
|
|
|
|
.top-meta .ep{font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--accent); font-weight:600}
|
|
|
|
|
|
.top-meta .ti{font-family:var(--serif); font-size:18px; font-weight:500; color:var(--ink)}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
.top-meta .cn{font-family:var(--han); font-size:11.5px; color:var(--muted); margin-top:1px}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.tag{font-size:10px;letter-spacing:.1em;color:var(--ink-soft);background:var(--accent-soft);
|
|
|
|
|
|
padding:5px 9px;border-radius:20px;flex:none;font-weight:500}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---------- subtitle stream ---------- */
|
|
|
|
|
|
.stream{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
flex:1; overflow:hidden; position:relative; padding:9vh 28px 6vh;
|
|
|
|
|
|
-webkit-mask-image:linear-gradient(180deg,transparent,#000 15%,#000 84%,transparent);
|
|
|
|
|
|
mask-image:linear-gradient(180deg,transparent,#000 15%,#000 84%,transparent);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.line{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
font-family:var(--serif); font-size:20px; line-height:1.5; letter-spacing:.005em;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
color:var(--faint); padding:13px 0 13px 18px; position:relative; cursor:pointer;
|
2026-06-21 17:21:14 +08:00
|
|
|
|
transition:color .25s ease, font-size .25s ease;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.line:hover{color:var(--muted)}
|
|
|
|
|
|
.line .t{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
font-family:var(--sans); font-size:10.5px; color:var(--faint); letter-spacing:.06em;
|
|
|
|
|
|
opacity:0; transition:.2s; position:absolute; left:-1px; top:17px;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.line:hover .t{opacity:1}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
/* active line */
|
|
|
|
|
|
.line.active{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
color:var(--ink); font-size:24px; font-weight:500;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.line.active::before{
|
|
|
|
|
|
content:""; position:absolute; left:0; top:50%; transform:translateY(-50%);
|
2026-06-21 17:21:14 +08:00
|
|
|
|
width:3px; height:58%; border-radius:3px; background:var(--accent);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.line.done{color:var(--muted)}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---------- player bar ---------- */
|
|
|
|
|
|
.player{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
position:sticky; bottom:0; z-index:20; padding:16px 22px 24px;
|
|
|
|
|
|
background:var(--surface); border-top:1px solid var(--hair);
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.seek{display:flex;align-items:center;gap:10px;margin-bottom:16px}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.seek .time{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;letter-spacing:.03em}
|
|
|
|
|
|
.bar{flex:1;height:4px;border-radius:4px;background:var(--hair);position:relative;cursor:pointer}
|
|
|
|
|
|
.bar .fill{position:absolute;inset:0 60% 0 0;border-radius:4px;background:var(--accent)}
|
|
|
|
|
|
.bar .knob{position:absolute;left:40%;top:50%;width:12px;height:12px;border-radius:50%;
|
|
|
|
|
|
transform:translate(-50%,-50%);background:var(--accent);border:2px solid var(--surface);box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
.controls{display:flex;align-items:center;justify-content:center;gap:30px}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.ctl{background:none;border:none;color:var(--ink-soft);cursor:pointer;display:grid;place-items:center;
|
|
|
|
|
|
min-width:44px;min-height:44px;transition:.2s;font-size:18px}
|
|
|
|
|
|
.ctl:hover{color:var(--accent)}
|
|
|
|
|
|
.ctl small{font-family:var(--sans);font-size:9px;letter-spacing:.08em;color:var(--muted);
|
|
|
|
|
|
display:block;text-align:center;margin-top:2px}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
.play{
|
2026-06-21 17:21:14 +08:00
|
|
|
|
width:62px;height:62px;border-radius:50%;flex:none;
|
|
|
|
|
|
background:var(--accent); color:#fff;
|
|
|
|
|
|
display:grid;place-items:center;cursor:pointer;border:none;
|
|
|
|
|
|
box-shadow:0 4px 14px rgba(164,102,43,.28); transition:.15s;
|
2026-06-21 15:29:36 +08:00
|
|
|
|
}
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.play:hover{background:var(--accent-2)}
|
|
|
|
|
|
.play:active{transform:scale(.95)}
|
|
|
|
|
|
.hint{text-align:center;font-size:10.5px;color:var(--muted);letter-spacing:.05em;margin-top:15px}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
|
|
|
|
|
|
/* load-in */
|
2026-06-21 17:21:14 +08:00
|
|
|
|
.line{animation:rise .5s both}
|
|
|
|
|
|
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce){
|
|
|
|
|
|
*{animation:none!important; transition:none!important; scroll-behavior:auto!important}
|
|
|
|
|
|
}
|
2026-06-21 15:29:36 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="phone">
|
|
|
|
|
|
<header class="top">
|
2026-06-21 17:21:14 +08:00
|
|
|
|
<button class="icon-btn" aria-label="返回课程列表">‹</button>
|
2026-06-21 15:29:36 +08:00
|
|
|
|
<div class="top-meta">
|
|
|
|
|
|
<div class="ep">EP 1 · Act I</div>
|
|
|
|
|
|
<div class="ti">46 Linden Street</div>
|
|
|
|
|
|
<div class="cn">林登大街 46 号</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="tag">精听</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="stream" id="stream"><!-- lines injected --></main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="player">
|
|
|
|
|
|
<div class="seek">
|
|
|
|
|
|
<span class="time" id="cur">0:41</span>
|
2026-06-21 17:21:14 +08:00
|
|
|
|
<div class="bar" id="bar" role="slider" aria-label="播放进度"><div class="fill" id="fill"></div><div class="knob" id="knob"></div></div>
|
2026-06-21 15:29:36 +08:00
|
|
|
|
<span class="time">3:42</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="controls">
|
2026-06-21 17:21:14 +08:00
|
|
|
|
<button class="ctl" id="prev" aria-label="上一句">⏮<small>上一句</small></button>
|
|
|
|
|
|
<button class="play" id="play" aria-label="播放或暂停">
|
|
|
|
|
|
<svg id="ico" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
|
2026-06-21 15:29:36 +08:00
|
|
|
|
</button>
|
2026-06-21 17:21:14 +08:00
|
|
|
|
<button class="ctl" id="next" aria-label="下一句">⏭<small>下一句</small></button>
|
2026-06-21 15:29:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="hint">点击任意一句 · 音频跳到该句重听</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 真实 ep1 act1 数据(节选)
|
|
|
|
|
|
const LINES = [
|
|
|
|
|
|
{t:34.18, en:"Excuse me. My name is Richard Stewart."},
|
|
|
|
|
|
{t:37.43, en:"I'm a photographer."},
|
|
|
|
|
|
{t:38.86, en:"May I take a picture of you and your little boy?"},
|
|
|
|
|
|
{t:40.55, en:"What's it for?"},
|
|
|
|
|
|
{t:41.41, en:"It's for a book."},
|
|
|
|
|
|
{t:42.48, en:"You're writing a book?"},
|
|
|
|
|
|
{t:44.18, en:"It's a book of pictures."},
|
|
|
|
|
|
{t:45.54, en:"I call it Family Album, U.S.A."},
|
|
|
|
|
|
{t:47.23, en:"Oh, that's a nice idea."},
|
|
|
|
|
|
];
|
|
|
|
|
|
const stream = document.getElementById('stream');
|
|
|
|
|
|
const fmt = s => Math.floor(s/60)+":"+String(Math.floor(s%60)).padStart(2,'0');
|
|
|
|
|
|
let active = 2, playing = false, timer = null;
|
|
|
|
|
|
|
|
|
|
|
|
function render(){
|
|
|
|
|
|
stream.innerHTML = '';
|
|
|
|
|
|
LINES.forEach((l,i)=>{
|
|
|
|
|
|
const d = document.createElement('div');
|
|
|
|
|
|
d.className = 'line' + (i===active?' active':'') + (i<active?' done':'');
|
2026-06-21 17:21:14 +08:00
|
|
|
|
d.style.animationDelay = (i*0.04)+'s';
|
2026-06-21 15:29:36 +08:00
|
|
|
|
d.innerHTML = `<span class="t">${l.t.toFixed(1)}s</span>${l.en}`;
|
|
|
|
|
|
d.onclick = ()=>{ active=i; sync(); }; // 点句跳转
|
|
|
|
|
|
stream.appendChild(d);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function sync(){
|
|
|
|
|
|
render();
|
|
|
|
|
|
const cur = stream.querySelector('.active');
|
|
|
|
|
|
cur && cur.scrollIntoView({behavior:'smooth', block:'center'});
|
|
|
|
|
|
document.getElementById('cur').textContent = fmt(LINES[active].t);
|
|
|
|
|
|
const pct = (active/(LINES.length-1))*100;
|
|
|
|
|
|
document.getElementById('fill').style.right = (100-pct)+'%';
|
|
|
|
|
|
document.getElementById('knob').style.left = pct+'%';
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('play').onclick = ()=>{
|
|
|
|
|
|
playing = !playing;
|
|
|
|
|
|
document.getElementById('ico').innerHTML = playing
|
|
|
|
|
|
? '<path d="M6 5h4v14H6zM14 5h4v14h-4z"/>' // pause
|
|
|
|
|
|
: '<path d="M8 5v14l11-7z"/>'; // play
|
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
|
if(playing) timer = setInterval(()=>{
|
|
|
|
|
|
if(active < LINES.length-1){ active++; sync(); }
|
|
|
|
|
|
else { playing=false; document.getElementById('play').click(); }
|
|
|
|
|
|
}, 1700);
|
|
|
|
|
|
};
|
|
|
|
|
|
document.getElementById('prev').onclick = ()=>{ if(active>0){active--;sync();} };
|
|
|
|
|
|
document.getElementById('next').onclick = ()=>{ if(active<LINES.length-1){active++;sync();} };
|
|
|
|
|
|
sync();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|