/* ============================================================ ROOY · 06 课程 ─────────────────────────────────────────────────────────── 需求:关卡式学习路径。主干一条 + 少量分支。给用户的感受是 "这棵树是 ROOY 给我画的学习地图"。 结构:NavBar + Segmented (复盘/课程/成就) → 路径卡 horizontal swipe (5 条 path) → 当前选中 path 的 node tree 纵向展开 → 节点 点击进入该阶段详情(06b course-detail),locked 节点不可点; current 节点「试一试」→ 04 ROOY 对话。 path 4 态: done → ink-filled rail + check current → ROOY 绿左边 + "ROOY 推荐" badge + 当前节点 next → muted outline + "下一个" locked → dashed faded accent budget:ROOY 绿(current path 标识 + 推荐 pill)+ gold (下一步动作建议)。 ============================================================ */ function CourseScreen({ onNav }) { const [tab, setTab] = React.useState('course'); const [active, setActive] = React.useState(1); // index of carving (current) const scrollerRef = React.useRef(null); const paths = [ { id: 'parallel', name: '平行式', st: 'done', nodes: 3, doneCount: 3, desc: '基础站位与平行下滑' }, { id: 'carving', name: '刃滑路径', st: 'current', nodes: 5, doneCount: 2, desc: '把立刃从想着做变成本能', currentNode: '立刃 · 40°' }, { id: 'short', name: '短弯节奏', st: 'next', nodes: 4, doneCount: 0, desc: '高频小幅转弯' }, { id: 'mogul', name: '雪沟地形', st: 'locked', nodes: 6, doneCount: 0, desc: '不规则地形适应' }, { id: 'powder', name: '粉雪适应', st: 'locked', nodes: 4, doneCount: 0, desc: '在松软雪面上的浮感' }, ]; const treesByPath = { parallel: [ { st: 'done', name: '平地站位', desc: '建立中立站位与触感', boosts: ['平衡'] }, { st: 'done', name: '平行下滑', desc: '直线下滑保持平行', boosts: ['平衡', '压力'] }, { st: 'done', name: '左右弯转换', desc: '把平行的感觉串起来', boosts: ['节奏'] }, ], carving: [ { st: 'done', name: '平行式', desc: '基础站位与平行下滑', boosts: ['平衡', '压力'] }, { st: 'done', name: '转弯衔接', desc: '左右弯之间的过渡', boosts: ['节奏', '协调'] }, { st: 'current', name: '立刃 · 40°', desc: '把刃角变成本能', boosts: ['立刃', '压力'], tasks: [ { l: '转换时轻跳一下', done: true }, { l: '右弯入弯踩脚趾', done: false, hi: true }, { l: '完整连接 5 趟', done: false }, ], }, { st: 'next', name: '短弯节奏', desc: '高频小幅转弯', boosts: ['节奏', '旋转'] }, { st: 'locked', name: '雪沟地形', desc: '不规则地形适应', boosts: ['平衡', '协调'] }, ], short: [ { st: 'current', name: '上半身预转', desc: '上体领转的基础动作', boosts: ['旋转'] }, { st: 'next', name: '高频短弯', desc: '把节奏压到 1.2s 内', boosts: ['节奏', '旋转'] }, { st: 'locked', name: '原地短弯', desc: '小坡度高密度练习', boosts: ['节奏'] }, { st: 'locked', name: '随心切换', desc: '长短弯自由切换', boosts: ['节奏', '协调'] }, ], mogul: [ { st: 'locked', name: '雪沟入门', desc: '在雪沟里站稳', boosts: ['平衡'] }, { st: 'locked', name: '雪沟节奏', desc: '随雪沟起伏', boosts: ['节奏', '协调'] }, { st: 'locked', name: '吸吐控制', desc: '主动吸收冲击', boosts: ['压力'] }, { st: 'locked', name: '雪沟刃滑', desc: '不规则地形划弧', boosts: ['立刃', '协调'] }, { st: 'locked', name: '全雪道穿越', desc: '在杂乱地形稳定', boosts: ['协调'] }, { st: 'locked', name: '随心而行', desc: '不再思考雪沟', boosts: ['协调'] }, ], powder: [ { st: 'locked', name: '粉雪站位', desc: '更靠后的重心', boosts: ['平衡'] }, { st: 'locked', name: '浮起来', desc: '保持板尖朝上', boosts: ['压力'] }, { st: 'locked', name: '粉雪转弯', desc: '在浮感里转动', boosts: ['旋转', '压力'] }, { st: 'locked', name: '深粉适应', desc: '过膝雪深里行进', boosts: ['平衡', '协调'] }, ], }; const activePath = paths[active]; const tree = treesByPath[activePath.id] || []; const onScroll = () => { const sc = scrollerRef.current; if (!sc) return; const cards = sc.querySelectorAll('[data-path-card]'); if (!cards.length) return; const center = sc.scrollLeft + sc.clientWidth / 2; let best = 0, bestDist = Infinity; cards.forEach((c, i) => { const cardCenter = c.offsetLeft + c.offsetWidth / 2; const d = Math.abs(cardCenter - center); if (d < bestDist) { bestDist = d; best = i; } }); if (best !== active) setActive(best); }; return (
onNav && onNav('home')} title="滑雪学院"/>
{ setTab(v); if (v === 'review') onNav && onNav('review'); if (v === 'medals') onNav && onNav('achievement'); }}/>
{/* ── Horizontal path pager — full-width, no card, snap-center ── */}
{paths.map((p, i) => (
setActive(i)}/>
))}
{/* ── pager removed entirely (the strip-with-count read too tech) Path switching is the swipe gesture; the active path's title appears in the row above as its own anchor. */}
{/* ── Node tree ── */}
{tree.map((n, i) => ( onNav && onNav('course-detail')} onTry={() => onNav && onNav('rooy')} /> ))}
); } /* PathRow — flat on paper, state-differentiated。 ⚠ Do not add a left-edge ROOY green accent bar —— that's the tropey ai-slop "accent stripe" pattern. State 由 badge fill + 正在练/下一步 pill + progress-strip 颜色承担。 */ function PathRow({ p, active, onTap }) { const isLocked = p.st === 'locked'; const isCurrent = p.st === 'current'; const isDone = p.st === 'done'; const isNext = p.st === 'next'; return (
{p.name} {isCurrent && 正在练} {isNext && 下一步}

{isCurrent && p.currentNode ? <>当前 · {p.currentNode} : p.desc}

{p.doneCount} / {p.nodes}
); } function ProgressStrip({ p }) { if (p.st === 'locked') { // no bars, just a faint hairline to suggest structure return (
); } return (
{Array.from({ length: p.nodes }).map((_, i) => { const done = i < p.doneCount; const cur = i === p.doneCount && p.st === 'current'; let bg = R.hair; let h = 4; if (done) bg = R.ink; if (cur) { bg = R.rooy; h = 6; } return (
); })}
); } /* ── Path node ─────────────────────────────────────────── */ function PathNode({ node, isLast, onOpen, onTry }) { const meta = { done: { color: R.ink, ring: R.ink, fill: R.ink, label: '已掌握', faded: false }, current: { color: R.rooy, ring: R.rooy, fill: R.rooy, label: 'ROOY 推荐', faded: false }, next: { color: R.ink2, ring: R.ink2, fill: 'transparent', label: '下一步', faded: false }, locked: { color: R.ink3, ring: R.hair2, fill: 'transparent', label: '未解锁', faded: true }, }[node.st]; return (
{node.st === 'done' && ( )} {node.st === 'current' && ( )} {node.st === 'locked' && ( )}
{!isLast && (
)}
{meta.label} {node.st === 'current' && 进行中}

{node.name}

{node.desc}

{node.boosts && (
+ {node.boosts.join(' · ')}
)} {node.st === 'current' && node.tasks && (

任务 · {node.tasks.length} 项

{node.tasks.map((tk, i) => (
{tk.done && } {tk.hi && !tk.done && } {tk.l}
))}
{ e.stopPropagation(); onTry && onTry(); }}>试一试
)}
); } Object.assign(window, { CourseScreen });