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>
This commit is contained in:
ila
2026-06-21 15:29:36 +08:00
co-authored by Claude Opus 4.8
parent 673b3a323a
commit d37b83011a
4 changed files with 398 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
<!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>