/* ============================================================
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) => (
))}
{/* ── 练习视频: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' }}
>
先看这个
);
}
/* CompactVideoRow — 列表行:小缩略 + 标题/副标题 + chevron。
时长进副标题(缩略图无角标),每条时长只出现一次。 */
function CompactVideoRow({ v, onOpen }) {
return (
);
}
/* 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 (
);
}
/* ── 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' }}>
{/* 顶部条:✕ + 标题 */}
{/* 底部:进度条 + 时间 */}
{playing ? '点击画面暂停' : '点击画面播放'}
);
}
Object.assign(window, { CourseDetailScreenB });