Files
lingo/design_mockups/play.html
T
ilaandClaude Opus 4.8 d37b83011a design: 新增播放页与课程列表 UI 效果稿(HTML/CSS)
- design_mockups/play.html:播放页,含逐句字幕高亮 + 点句跳转 + 播放器,
  用 ep1 真实台词;CSS 变量化,可迁移进 go-app
- design_mockups/list.html:26 集课程列表,含进度环、Act 音频状态、
  ep18+ 无音频降级态;用真实集标题
- design_mockups/README.md:设计语言与文档对应说明
- 06-tasks:新增 Phase D / T-200 设计任务

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:29:36 +08:00

210 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Hanken+Grotesk:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ink:#15120d; --ink-2:#1d1812; --card:#221c14;
--paper:#ece1cd; --muted:#9b8d77; --faint:#5c513f;
--amber:#e6ad5c; --amber-2:#f0c074; --amber-soft:rgba(230,173,92,.13);
--line:rgba(236,225,205,.08);
--serif:"Newsreader",Georgia,serif;
--sans:"Hanken Grotesk",system-ui,sans-serif;
--han:"Noto Serif SC",serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
font-family:var(--sans); color:var(--paper);
background:
radial-gradient(120% 80% at 50% -10%, rgba(230,173,92,.10), transparent 55%),
radial-gradient(140% 120% at 50% 120%, rgba(0,0,0,.5), transparent 60%),
var(--ink);
display:flex; justify-content:center; min-height:100%;
-webkit-font-smoothing:antialiased;
}
/* film grain */
body::after{
content:""; position:fixed; inset:0; pointer-events:none; z-index:50; opacity:.5; mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.4'/%3E%3C/svg%3E");
}
.phone{
width:100%; max-width:440px; min-height:100vh; position:relative;
display:flex; flex-direction:column;
background:linear-gradient(180deg, rgba(255,255,255,.015), transparent 30%);
border-left:1px solid var(--line); border-right:1px solid var(--line);
}
/* ---------- top bar ---------- */
.top{
position:sticky; top:0; z-index:20; padding:18px 20px 14px;
display:flex; align-items:center; gap:14px;
background:linear-gradient(180deg, var(--ink) 60%, transparent);
}
.icon-btn{
width:38px;height:38px;border-radius:50%;border:1px solid var(--line);
background:var(--ink-2); color:var(--paper); display:grid;place-items:center;
cursor:pointer; transition:.2s; flex:none;
}
.icon-btn:hover{border-color:var(--amber); color:var(--amber)}
.top-meta{flex:1; text-align:center; line-height:1.25}
.top-meta .ep{font-size:11px; letter-spacing:.22em; text-transform:uppercase; color:var(--amber); font-weight:600}
.top-meta .ti{font-family:var(--serif); font-size:18px; font-weight:500}
.top-meta .cn{font-family:var(--han); font-size:11.5px; color:var(--muted); margin-top:1px}
.tag{font-size:10px;letter-spacing:.14em;color:var(--muted);border:1px solid var(--line);
padding:5px 8px;border-radius:20px;flex:none}
/* ---------- subtitle stream ---------- */
.stream{
flex:1; overflow:hidden; position:relative; padding:8vh 26px 6vh;
-webkit-mask-image:linear-gradient(180deg,transparent,#000 16%,#000 82%,transparent);
mask-image:linear-gradient(180deg,transparent,#000 16%,#000 82%,transparent);
}
.line{
font-family:var(--serif); font-size:21px; line-height:1.45; letter-spacing:.005em;
color:var(--faint); padding:13px 0 13px 18px; position:relative; cursor:pointer;
transition:color .35s ease, opacity .35s, transform .35s; opacity:.8;
}
.line:hover{color:var(--muted)}
.line .t{
font-family:var(--sans); font-size:10.5px; color:var(--faint); letter-spacing:.08em;
opacity:0; transition:.25s; position:absolute; left:-2px; top:18px;
}
.line:hover .t{opacity:.7}
/* active line */
.line.active{
color:var(--paper); font-size:25px; opacity:1; font-weight:500;
text-shadow:0 0 30px rgba(230,173,92,.18);
}
.line.active::before{
content:""; position:absolute; left:0; top:50%; transform:translateY(-50%);
width:3px; height:62%; border-radius:3px;
background:linear-gradient(var(--amber),var(--amber-2));
box-shadow:0 0 14px rgba(230,173,92,.6);
}
.line.done{color:var(--muted); opacity:.55}
/* ---------- player bar ---------- */
.player{
position:sticky; bottom:0; z-index:20; padding:16px 22px 26px;
background:linear-gradient(0deg, var(--ink) 62%, transparent);
}
.seek{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.seek .time{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;letter-spacing:.04em}
.bar{flex:1;height:4px;border-radius:4px;background:var(--line);position:relative;cursor:pointer}
.bar .fill{position:absolute;inset:0 60% 0 0;border-radius:4px;
background:linear-gradient(90deg,var(--amber),var(--amber-2))}
.bar .knob{position:absolute;left:40%;top:50%;width:11px;height:11px;border-radius:50%;
transform:translate(-50%,-50%);background:var(--amber-2);box-shadow:0 0 10px rgba(230,173,92,.7)}
.controls{display:flex;align-items:center;justify-content:center;gap:30px}
.ctl{background:none;border:none;color:var(--paper);cursor:pointer;display:grid;place-items:center;
opacity:.85;transition:.2s}
.ctl:hover{opacity:1;color:var(--amber-2)}
.ctl small{font-family:var(--sans);font-size:9px;letter-spacing:.12em;color:var(--muted);
display:block;text-align:center;margin-top:3px}
.play{
width:64px;height:64px;border-radius:50%;flex:none;
background:radial-gradient(120% 120% at 30% 20%,var(--amber-2),var(--amber));
color:#1a1206;display:grid;place-items:center;cursor:pointer;border:none;
box-shadow:0 8px 26px rgba(230,173,92,.34), inset 0 1px 1px rgba(255,255,255,.4);
transition:transform .15s;
}
.play:active{transform:scale(.94)}
.hint{text-align:center;font-size:10.5px;color:var(--faint);letter-spacing:.08em;margin-top:16px}
/* load-in */
.line{animation:rise .6s both}
@keyframes rise{from{opacity:0;transform:translateY(8px)}}
</style>
</head>
<body>
<div class="phone">
<header class="top">
<button class="icon-btn" aria-label="返回">‹</button>
<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>
<div class="bar" id="bar"><div class="fill" id="fill"></div><div class="knob" id="knob"></div></div>
<span class="time">3:42</span>
</div>
<div class="controls">
<button class="ctl" id="prev" title="上一句">⏮<small>上一句</small></button>
<button class="play" id="play" aria-label="播放/暂停">
<svg id="ico" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
</button>
<button class="ctl" id="next" title="下一句">⏭<small>下一句</small></button>
</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':'');
d.style.animationDelay = (i*0.05)+'s';
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>