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:
@@ -0,0 +1,22 @@
|
||||
# design_mockups — UI 效果稿(HTML/CSS)
|
||||
|
||||
可在浏览器直接打开的中保真效果稿,**CSS 可直接迁移进 go-app**(技术栈选的就是纯 CSS)。
|
||||
|
||||
| 文件 | 页面 | 看点 |
|
||||
| --- | --- | --- |
|
||||
| `play.html` | 播放页 | **逐句字幕同步**:当前句放大+琥珀高亮+左侧光条,已读句变暗;**点任意句跳转**;底部播放器(播/停、上/下一句、进度)。点 ▶ 可看自动逐句推进的效果 |
|
||||
| `list.html` | 课程列表 | 26 集卡片:英文/中文标题、Act 幕状态(有音频=琥珀点 / 无音频=虚线)、进度环;顶部"继续学习"卡;ep18+ 显示"暂无音频"降级态 |
|
||||
|
||||
## 设计语言(深夜电影感精听)
|
||||
|
||||
- **配色**:暖墨黑底 `#15120d` + 纸感米色 `#ece1cd` + **琥珀金单点强调** `#e6ad5c`;细微胶片颗粒 + 顶部暖光。
|
||||
- **字体**:英文句子用文学衬线 **Newsreader**(精听的主角);UI 用 **Hanken Grotesk**;中文用 **Noto Serif SC**。
|
||||
- **原则**:单一强调色、移动优先、内容(英文句子)为视觉主体;克制、不镀金。
|
||||
|
||||
> 颜色 / 字体 / 间距都用 CSS 变量(`:root`)集中定义,迁移进 go-app 时整段 `:root` + 组件样式可直接复用。
|
||||
|
||||
## 与文档的对应
|
||||
|
||||
- 形态对应 `docs/04-architecture.md`(列表 + 播放页 + 字幕同步)与 `docs/02-requirements.md` 第五节验收(逐句高亮、点句跳转、进度、无音频降级)。
|
||||
- 用的是 `family-album-usa/episodes.json` 的**真实数据**(ep1《46 Linden Street》真实台词、各集真实标题与音频覆盖)。
|
||||
- 落地任务见 `docs/06-tasks.md` 的 T-200(设计)→ T-201/T-203(实现)。
|
||||
@@ -0,0 +1,159 @@
|
||||
<!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;0,6..72,600&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; --card-2:#271f16;
|
||||
--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}
|
||||
body{
|
||||
font-family:var(--sans); color:var(--paper);
|
||||
background:
|
||||
radial-gradient(120% 70% at 50% -5%, rgba(230,173,92,.10), transparent 50%),
|
||||
var(--ink);
|
||||
display:flex; justify-content:center; min-height:100vh;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
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;border-left:1px solid var(--line);border-right:1px solid var(--line);padding-bottom:30px}
|
||||
|
||||
/* header */
|
||||
.head{padding:30px 24px 18px}
|
||||
.brand{display:flex;align-items:baseline;justify-content:space-between}
|
||||
.brand h1{font-family:var(--han);font-size:25px;font-weight:600;letter-spacing:.04em}
|
||||
.brand .en{font-family:var(--serif);font-style:italic;font-size:13px;color:var(--amber);letter-spacing:.02em}
|
||||
.sub{font-size:11.5px;color:var(--muted);letter-spacing:.16em;text-transform:uppercase;margin-top:6px}
|
||||
|
||||
/* continue hero */
|
||||
.hero{
|
||||
margin:18px 20px 6px;padding:18px 20px;border-radius:18px;position:relative;overflow:hidden;
|
||||
background:linear-gradient(135deg,var(--card-2),var(--card));border:1px solid var(--line);
|
||||
}
|
||||
.hero::before{content:"";position:absolute;right:-40px;top:-40px;width:160px;height:160px;border-radius:50%;
|
||||
background:radial-gradient(closest-side,rgba(230,173,92,.18),transparent)}
|
||||
.hero .lab{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--amber);font-weight:600}
|
||||
.hero .ti{font-family:var(--serif);font-size:22px;font-weight:600;margin-top:7px}
|
||||
.hero .cn{font-family:var(--han);font-size:12.5px;color:var(--muted);margin-top:2px}
|
||||
.hero .row{display:flex;align-items:center;gap:14px;margin-top:16px}
|
||||
.progress{flex:1;height:5px;border-radius:5px;background:rgba(0,0,0,.35);overflow:hidden}
|
||||
.progress i{display:block;height:100%;width:38%;border-radius:5px;background:linear-gradient(90deg,var(--amber),var(--amber-2))}
|
||||
.resume{flex:none;display:flex;align-items:center;gap:7px;background:var(--amber);color:#1a1206;
|
||||
font-weight:700;font-size:13px;border:none;border-radius:30px;padding:9px 16px;cursor:pointer}
|
||||
.resume:hover{background:var(--amber-2)}
|
||||
|
||||
/* section label */
|
||||
.seclab{display:flex;align-items:center;gap:10px;padding:22px 24px 8px;color:var(--muted)}
|
||||
.seclab span{font-size:11px;letter-spacing:.18em;text-transform:uppercase}
|
||||
.seclab .ln{flex:1;height:1px;background:var(--line)}
|
||||
|
||||
/* episode list */
|
||||
.list{padding:4px 14px}
|
||||
.ep{display:flex;gap:14px;align-items:center;padding:14px 12px;border-radius:14px;cursor:pointer;
|
||||
transition:background .2s;position:relative}
|
||||
.ep:hover{background:var(--ink-2)}
|
||||
.ep + .ep{border-top:1px solid var(--line)}
|
||||
.num{font-family:var(--serif);font-size:20px;font-style:italic;color:var(--faint);width:30px;flex:none;text-align:center}
|
||||
.ep.cur .num{color:var(--amber)}
|
||||
.body{flex:1;min-width:0}
|
||||
.body .ti{font-family:var(--serif);font-size:18px;font-weight:500;line-height:1.2}
|
||||
.body .cn{font-family:var(--han);font-size:11.5px;color:var(--muted);margin-top:2px}
|
||||
.acts{display:flex;gap:6px;margin-top:9px}
|
||||
.chip{font-size:10px;letter-spacing:.06em;color:var(--muted);border:1px solid var(--line);
|
||||
border-radius:6px;padding:3px 8px;display:flex;align-items:center;gap:5px}
|
||||
.chip.done{color:var(--amber);border-color:rgba(230,173,92,.4)}
|
||||
.chip.done::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--amber)}
|
||||
.chip.noaudio{color:var(--faint);border-style:dashed}
|
||||
.ring{flex:none;width:34px;height:34px}
|
||||
.ring svg{transform:rotate(-90deg)}
|
||||
.ring .bg{stroke:var(--line)} .ring .fg{stroke:var(--amber);stroke-linecap:round}
|
||||
.ep.locked{opacity:.5}
|
||||
.ep.locked .body .ti{font-style:normal}
|
||||
.badge{font-size:9.5px;letter-spacing:.1em;color:var(--faint);border:1px dashed var(--line);
|
||||
border-radius:20px;padding:4px 9px;flex:none}
|
||||
|
||||
.list .ep{animation:rise .5s both}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(8px)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone">
|
||||
<header class="head">
|
||||
<div class="brand">
|
||||
<h1>走遍美国</h1>
|
||||
<span class="en">Family Album, U.S.A.</span>
|
||||
</div>
|
||||
<div class="sub">逐句精听 · Close Listening</div>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="lab">继续学习</div>
|
||||
<div class="ti">46 Linden Street · Act I</div>
|
||||
<div class="cn">林登大街 46 号 · 第一幕</div>
|
||||
<div class="row">
|
||||
<div class="progress"><i></i></div>
|
||||
<button class="resume">▶ 续听</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="seclab"><span>全 26 集</span><div class="ln"></div></div>
|
||||
|
||||
<div class="list" id="list"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 真实数据:title / titleCn / 幕数 / 有音频幕数 / 进度(%)
|
||||
const EPS = [
|
||||
{n:1, ti:"46 Linden Street", cn:"林登大街46号", acts:3, audio:2, prog:38, cur:true},
|
||||
{n:2, ti:"The Blind Date", cn:"相亲", acts:3, audio:3, prog:100},
|
||||
{n:3, ti:"Grandpa's Trunk", cn:"祖父的行李箱", acts:3, audio:3, prog:64},
|
||||
{n:4, ti:"A Piece of Cake", cn:"小菜一碟", acts:3, audio:3, prog:0},
|
||||
{n:5, ti:"The Right Magic", cn:"灵丹妙药", acts:3, audio:3, prog:0},
|
||||
{n:6, ti:"Thanksgiving", cn:"感恩节", acts:3, audio:3, prog:0},
|
||||
{n:7, ti:"Man's Best Friend", cn:"人类最好的朋友",acts:3, audio:3, prog:0},
|
||||
{n:8, ti:"You're Going to Be Fine",cn:"你会好起来的",acts:3, audio:3, prog:0},
|
||||
{n:18, ti:"Making a Difference", cn:"与众不同", acts:3, audio:0, prog:0, locked:true},
|
||||
{n:19, ti:"I Do", cn:"我愿意", acts:3, audio:0, prog:0, locked:true},
|
||||
];
|
||||
const roman = ["Ⅰ","Ⅱ","Ⅲ"];
|
||||
const list = document.getElementById('list');
|
||||
EPS.forEach((e,idx)=>{
|
||||
const el = document.createElement('div');
|
||||
el.className = 'ep' + (e.cur?' cur':'') + (e.locked?' locked':'');
|
||||
el.style.animationDelay = (idx*0.04)+'s';
|
||||
let chips = '';
|
||||
for(let a=0;a<e.acts;a++){
|
||||
if(e.locked) chips += `<span class="chip noaudio">Act ${roman[a]}</span>`;
|
||||
else chips += `<span class="chip ${a<e.audio?'done':''}">Act ${roman[a]}</span>`;
|
||||
}
|
||||
const r=15, c=2*Math.PI*r, off=c*(1-e.prog/100);
|
||||
const right = e.locked
|
||||
? `<span class="badge">暂无音频</span>`
|
||||
: `<div class="ring"><svg width="34" height="34"><circle class="bg" cx="17" cy="17" r="${r}" fill="none" stroke-width="3"/><circle class="fg" cx="17" cy="17" r="${r}" fill="none" stroke-width="3" stroke-dasharray="${c}" stroke-dashoffset="${off}"/></svg></div>`;
|
||||
el.innerHTML = `
|
||||
<div class="num">${e.n}</div>
|
||||
<div class="body">
|
||||
<div class="ti">${e.ti}</div>
|
||||
<div class="cn">${e.cn}</div>
|
||||
<div class="acts">${chips}</div>
|
||||
</div>
|
||||
${right}`;
|
||||
list.appendChild(el);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -43,6 +43,14 @@
|
||||
|
||||
> 对应需求验收:02-requirements 第五节"逐句同步"。这是 P0 核心,务必先单独跑通。
|
||||
|
||||
## Phase D · 设计(先于 Phase 2 的 UI)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-200 | UI 效果稿:播放页 + 课程列表(HTML/CSS) | — | 中保真稿覆盖逐句高亮/点句跳转/进度/无音频降级;用真实数据;CSS 可迁移 go-app。见 `design_mockups/` | DOING |
|
||||
|
||||
> 设计 spike,不是生产代码。产出在 `design_mockups/play.html`、`list.html`,作为 T-201/T-203 的视觉依据。
|
||||
|
||||
## Phase 2 · 课程列表 + 播放页
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
|
||||
Reference in New Issue
Block a user