/* ============================================================ ROOY · 03c 滑行中 · 上课模式 ─────────────────────────────────────────────────────────── 需求:教学中 —— ROOY 主动给反馈,当前任务上下文永久可见, 客观数据弱化(不强调"成绩")。 跟 03b 的差别: 1. 顶部悬浮教学 banner(任务名 + 进度 N/M),点击展开看任务详情 2. peek hero 是教学指标(完成度 = 逐弯计数 + tally + gold 达标 线,节奏为副指标),客观三数据下沉到 expanded 3. **没有** lesson tab —— 教学态由顶部 banner 承担 italic anchor:完成度弯数(peek hero)。 accent:ROOY 绿(banner kicker "ROOY · 当前任务" + peek 完成度 计数与 tally 填充);gold 1(tally 达标 tick = 本趟计入任务进 度的门槛)。 交互细节:banner 跟 sheet 都占大屏空间,同时展开会顶人 —— useEffect 做互斥:谁后展开谁让另一个收回。 ============================================================ */ function SkiScreenC({ onNav }) { const [expanded, setExpanded] = React.useState(false); const [bannerOpen, setBannerOpen] = React.useState(false); React.useEffect(() => { if (bannerOpen) setExpanded(false); }, [bannerOpen]); React.useEffect(() => { if (expanded) setBannerOpen(false); }, [expanded]); return (
{/* basemap — 与 03b 一致(冷调雪面浅灰蓝,色值见 ski-b 注释); 上课模式仍然有真实滑行场景. */}
{Array.from({ length: 19 }).map((_, i) => { const y = 18 + i * 30; const amp = 24 + (i % 4) * 9; return ( ); })} {Array.from({ length: 5 }).map((_, i) => { const y = 65 + i * 120; return ( ); })} 2400 M 2200 M 2000 M 1800 M 1600 M Furi 缆车站 {/* Trail group — 03c 整体下移 80,让起点贴 banner collapsed 底部 (banner top 60 + maxHeight 80 = bottom 140;trail 起点 78+80=158, 露出 banner 下方 ~18pt,保持"从 banner 自然延伸"的紧邻感). */}
{/* 教学 banner — 在 status bar 下方,向下展开 (collapsed → 任务上下文 + progress;expanded 加 callout subtitle + 关键点 list + 继续 button). 底色用 R.bg 跟下方 sheet 一致, 靠 shadow.lift + 圆角表示"脱离 basemap 的功能层". */}
setBannerOpen(!bannerOpen)} style={{ position: 'absolute', /* 默认 (design canvas) top:60 — mock StatusBar 54pt + 6pt 间距. PWA prototype (__ROOY_HIDE_MOCK_STATUSBAR) 下 mock 不渲染, 改用 safe-area-inset-top 自适应:iOS PWA = notch 47 + 6 = 53, Android PWA = 0 + 6 = 6(贴在系统状态栏下方). */ top: (typeof window !== 'undefined' && window.__ROOY_HIDE_MOCK_STATUSBAR) ? 'calc(env(safe-area-inset-top, 0px) + 6px)' : 60, left: 12, right: 12, zIndex: 4, background: R.bg, borderRadius: B.cardL, boxShadow: Shadow.lift, overflow: 'hidden', cursor: 'pointer', maxHeight: bannerOpen ? 260 : 80, transition: 'max-height 0.36s cubic-bezier(0.4,0,0.2,1)', }}>
ROOY · 当前任务
{/* Headline 行 inline 进度 metadata —— progress bar 删了 (4pt ROOY 绿粗条让 banner 读作 "hero tracking 卡",跟 "轻量教学任 务上下文" 定位冲突;同屏 ROOY 绿配额也超标)。"2 / 5" 退成右 侧 footnote, 让标题主导这条 banner. */}

转换时轻跳一下

2 / 5
{/* Expanded 内容 —— 仅 bannerOpen 时渲染. 不再靠外层 overflow:hidden 裁切,避免 collapsed 状态下 video thumb 顶部圆角 4pt 漏出 banner 底部的灰色边缘. */} {bannerOpen && ( <>
e.stopPropagation()} style={{ marginTop: 14, display: 'flex', gap: 14, alignItems: 'flex-start' }}>
{[ '弯之间轻轻跳一下', '落地用脚趾·入弯', '上身保持朝前', ].map((t, i) => (

{t}

))}
{/* Footer —— 仅 退出上课 胶囊按钮, 右下角. hairline 分隔 banner 内容. */}
)}
{/* Stop button —— 跟 03b 同款 56×56 戴手套友好白圆 + 红方块. tap → 暂停 push 03d(跟 03b 一致)。bottom 固定到 peek (252) 顶 14px 上方, expanded 时被 sheet 盖住 (intended). */} {/* Sheet — peek (scope + hero + ROOY), expanded 加 4 段. */}
onNav && onNav(t)}/>
); } /* DataContentC — 03c sheet 内容(peek + expanded) Peek (252): scope 行 + 完成度 hero(逐弯计数 + tally + gold 达 标线)+ 节奏副指标 + ROOY 反馈 2 行 Expanded 加 4 段(同 03b): 本趟 3 数据 → 速度曲线 → 左右脚 → 今日累计。任务名靠顶部 banner 表达,sheet 不重复。 */ function DataContentC({ expanded }) { return ( <> {/* PEEK · 0. Scope label — 跟 03b 同款 space-between layout. */}
刚刚那趟 Furi 蓝道
{/* PEEK · 1. Hero — 不对称布局: 左: 完成度主 hero = 逐弯计数(italic 5 + "/ 8 弯"); 右: "节奏" label 在上 + 65 数字在下 (column, 副 hero). 85% 抽象百分比换成逐弯计数——上课模式的完成度本来就按弯 计(哪几个弯做到轻跳),计数 + 下方 tally 比百分比可扫 读,且直接给出"差几个弯"的下一步差距;节奏数字非 italic, 让副 hero 视觉权重弱于主 hero. */}
5 / 8 弯 完成度

节奏

65
{/* PEEK · 1b. 逐弯 tally — 8 弯做到 5 个,gold tick = 6 弯门 槛:本趟够 6 个弯才计入 banner 的任务进度(差 1 个,所以 banner 停在 2/5)。差距不配文字 caption——tick 前的空槽 自己说话. */}
{/* PEEK · 2. ROOY 反馈 — 03b 同款单行段: dot inline + 一段话. peek 不再放 3 stats (已下沉到 expanded "本趟" section), peek 只承担"教学焦点 hero + ROOY 评价"两件事, 减少视觉压力. */}

节奏稳了 ↑3,立刃也跟上 ↑11。下一弯收尾再晚一点试试。

{/* EXPANDED · 5 段: 本趟 (3 stats) → 能力 → 速度曲线 → 左右脚 → 今日累计. 3 stats 从 peek 下沉到 expanded 顶, peek 留给 hero + ROOY. */} {expanded && ( <>
52 48
)} ); } /* ── 子组件 (03b 同款设计语言, 加 C 后缀避免跟 ski-b.jsx 的 global 冲突) */ /* TallyC — 逐弯记分板(教学态):total 弯 / done 做到(R.rooy 填充 = 教学指标色,同老 85% 的 hero 色角色)/ goal 后的 gold 达标 tick。段是 skewX(-14°) 直角平行四边形——跟上方 N.italic 计数 同一倾角(SF italic ≈14°),棱角和运动感来自形态不是装饰. */ function TallyC({ total = 8, done = 0, goal = 6 }) { return (
{Array.from({ length: total }).map((_, i) => (
{i === goal - 1 && (
)} ))}
); } function SectionC({ label, sub, first, last, gap, children }) { const top = first ? 32 : gap ? 40 : 22; return (
{label} {sub && {sub}}
{children}
); } function StatColC({ label, value, unit }) { return (

{label}

); } function DayStatC({ l, v, u }) { return ( {l} {v} {u && {u}} ); } /* 速度曲线 sparkline — 与 03b 同款 (8 段 turns + 实线 vertical 分隔 + dot-grid 底). 复制而非共享, 避免跨 script global 依赖. */ function SpeedSparklineC() { const turns = [ [18, 28, 36, 30, 20, 14, 12], [22, 36, 50, 55, 46, 30, 20, 16, 18], [28, 46, 62, 68, 58, 40, 28, 22, 30], [38, 48, 50, 40, 22, 14, 12], [26, 46, 64, 72, 60, 40, 28, 24, 30], [36, 42, 45, 36, 22, 16, 14], [28, 44, 58, 60, 50, 32, 22, 18], [24, 28, 32, 26, 22, 20], ]; const speeds = turns.flat(); const boundaries = turns.reduce( (acc, t) => [...acc, acc[acc.length - 1] + t.length], [0], ).slice(1, -1); const min = Math.min(...speeds), max = Math.max(...speeds); const range = max - min || 1; const w = 326, h = 80, padTop = 14, padBot = 10, padX = 6; const yRange = h - padTop - padBot; const xRange = w - padX * 2; const xAt = (i) => padX + (i / (speeds.length - 1)) * xRange; const pts = speeds.map((v, i) => ({ x: xAt(i), y: padTop + (1 - (v - min) / range) * yRange, })); const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' '); const last = pts[pts.length - 1]; return ( {boundaries.map((b, k) => { const x = xAt(b - 0.5); return ( ); })} ); } Object.assign(window, { SkiScreenC });