/* ============================================================ ROOY · 06b 阶段详情(CourseDetailScreenB) ─────────────────────────────────────────────────────────── 需求:从 06 课程的节点点进来的单个阶段详情。示例 = carving 路径 current 节点「立刃 · 40°」(进行中)。 结构:NavBar → 阶段头部(进行中 + boosts 并排 / 描述)→ 「阶段详情」(intro + 3 要点白卡 InsetGroup,编号中性 ink)→ 「练习视频」(featured hero 海报 + 紧凑列表白卡)→ ROOY 提示 + 试一试(InlineCTA,随内容滚动)。点 featured / 列表行 → 应用内全屏播放器(zIndex fullscreen)。 accent budget:ROOY 绿单一主导(进行中 badge + featured 「先看这个」pill + RooyLine dot)。无 gold / data。 跨屏:prototype 路由 'course-detail'(home tab push)。由 06 course.jsx 的 PathNode 点击进入;试一试 → 'rooy'。 非显然决定:3 条练习要点是真·3 项相关列表 → 用 InsetGroup 白卡(符合「3+ 相关项才用白卡」)。featured 海报走 rooy 偏色 暗渐变(推荐那条带品牌色),列表行缩略走中性 ink 暗渐变。 ============================================================ */ function CourseDetailScreenB({ onNav }) { /* active: null | video —— 当前全屏播放的视频。null = 不在全屏。 */ const [active, setActive] = React.useState(null); const stage = { name: '立刃 · 40°', path: '刃滑路径 · 第 3 关', desc: '把刃角从"想着做"变成本能 —— 入弯就吃住雪,靠身体记住 40° 的体感。', boosts: ['立刃', '压力'], taskDone: 1, taskTotal: 3, points: [ '入弯前先把重心压到外脚,让板刃自然咬住雪面', '膝盖随弯倾向山下,上半身保持朝向前进方向', '出弯不急着收,让刃走完整条弧线再换刃', ], }; const videos = [ { id: 'v1', t: '立刃的体感是什么', sub: '入门 · 概念', dur: '2:48' }, { id: 'v2', t: '40° 刃角分解动作', sub: '核心 · 分解', dur: '4:12' }, { id: 'v3', t: '常见错误:内倾过度', sub: '纠错 · 对比', dur: '3:05' }, { id: 'v4', t: '完整连接 5 趟示范', sub: '进阶 · 实战', dur: '5:30' }, ]; /* featured = v2:RooyLine 点名"先看「40° 刃角分解动作」",给推荐 那条一个独立的 hero 落点。 */ const featured = videos[1]; const rest = videos.filter(v => v.id !== featured.id); return (
onNav && onNav('back')} kicker={stage.path} title={stage.name} />
{/* ── 阶段头部 ── */}
{/* 状态 + boosts 并排:进行中 在前 */}
进行中 + {stage.boosts.join(' · ')}

{stage.desc}

{/* ── 阶段详情 ── */} 阶段详情

这一关把前两关学会的转弯衔接收进刃滑。目标不是更快,而是让板刃在每个弯里 都吃住雪 —— 当 40° 的刃角变成不用想的本能,你的弧线会自己变干净。

{/* 3 条要点 = 真·3 项相关列表 → InsetGroup 白卡 */}
{stage.points.map((p, i) => (
{i + 1}

{p}

))}
{/* ── 练习视频:featured + 紧凑列表 ── */} 练习视频
setActive(featured)}/> {rest.map(v => ( setActive(v)}/> ))}
{/* ROOY 提示 + 试一试(InlineCTA,随内容滚动) */}
onNav && onNav('rooy')}>试一试
{active && ( setActive(null)}/> )}
); } /* ── FeaturedVideo — ROOY 推荐那条的 hero 海报卡 ────────────── rooy 偏色暗渐变 + "先看这个" pill + 大播放键 + 时长角标。 副标题不放时长(角标已承担),每条时长只出现一次。 */ function FeaturedVideo({ v, onOpen }) { const [pressed, setPressed] = React.useState(false); return (
setPressed(true)} onMouseUp={() => setPressed(false)} onMouseLeave={() => setPressed(false)} style={{ cursor: 'pointer', transform: pressed ? 'scale(0.99)' : 'none', transition: 'transform 0.15s ease' }} > 先看这个

{v.t}

{v.sub}

); } /* CompactVideoRow — 列表行:小缩略 + 标题/副标题 + chevron。 时长进副标题(缩略图无角标),每条时长只出现一次。 */ function CompactVideoRow({ v, onOpen }) { return (

{v.t}

{v.sub} · {v.dur}

); } /* Poster — 深色渐变 + 等高线 + 居中播放键;dur 给则右下时长角标; children 浮在海报上(featured 的"先看这个"pill)。fill=true 用于 全屏铺底(inset 0、无圆角)。tone 只 rooy(品牌偏色) / ink(中性)。 */ function Poster({ tone = 'ink', dur, height, fill = false, playSize = 56, playing = false, radius = B.cardL, children, style }) { const grad = tone === 'rooy' ? 'linear-gradient(135deg, #0b3b44 0%, #0E1116 78%)' : 'linear-gradient(135deg, #2a2e36 0%, #0E1116 82%)'; return (
{children} {dur && ( {dur} )}
); } /* PlayGlyph — 半透明圆底 + 白色三角。playing=true 画暂停双竖条。 */ function PlayGlyph({ size = 56, playing = false }) { const tri = size * 0.34; return (
{playing ? (
) : ( )}
); } /* ── VideoPlayer — 应用内全屏覆盖层(zIndex fullscreen)───────── 点中央切换播放/暂停,playing 时假进度条推进;✕ 关闭。 */ function VideoPlayer({ video, onClose }) { const [playing, setPlaying] = React.useState(true); const [progress, setProgress] = React.useState(0); const total = React.useMemo(() => { const [m, s] = video.dur.split(':').map(Number); return m * 60 + s; }, [video.dur]); React.useEffect(() => { if (!playing) return; const id = setInterval(() => { setProgress(p => { const next = p + 1 / (total * 2); // 2 ticks/sec → 双速演示 if (next >= 1) { return 0; } return next; }); }, 500); return () => clearInterval(id); }, [playing, total]); const cur = Math.round(progress * total); const fmt = (sec) => `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`; return (
{/* 视频帧铺底 + 中央播放/暂停命中区 */}
setPlaying(p => !p)} style={{ position: 'absolute', inset: 0, cursor: 'pointer' }}>
{/* 顶部条:✕ + 标题 */}

{video.t}

{video.sub}

{/* 底部:进度条 + 时间 */}
{fmt(cur)}
{fmt(total)}

{playing ? '点击画面暂停' : '点击画面播放'}

); } Object.assign(window, { CourseDetailScreenB });