/* ============================================================ ROOY · 09 滑行历史 · 雪场地图 hero(history-b,原 09b 转正) ─────────────────────────────────────────────────────────── 把原 09 的「生涯黑卡」hero 换成「生涯打卡雪场分布地图」,参考 08b(season-c)的地图 hero 语言。原 09(history.jsx)已删,本屏 转正为唯一 09——prototype 'history' 路由 → HistoryScreenB。黑卡 点击跳转(旧 09 → 08)随之废弃(本屏无黑卡)。 两态 hero: · 折叠态 = 地图(生涯打卡雪场世界底图)+ 3 个生涯数字(天/里程/雪场) 浮在地图底部,底部渐变托底。 · 展开态 = 点「展开」地图向下加高(顶部间距不变),浮层数字(第一行 天/里程/雪场)原位不动仍浮在渐变上,下面铺开生涯详情——第二行 stats (雪季/垂降/心流)/ 滑行模式 / 雪况 / 高光。结构搬 08b(season-c)的 地图下段,数据换全生涯口径(非单雪季)。展开后点 pin 更新最常去读出。 · 展开/收起键 = sticky 浮在地图右上角(参考 08b season-c),静止贴角、 展开后下滚也钉在视口同一位置,保证随时一键收起。 复用:SeasonMapCanvas / frameOf / LONLAT + BStatRow / BRecordRow / CModeSplit / CSnowMix / SectionTitle / Trophy·Flag·MedalGlyph(均 window 导出);DayRow 本屏自带(原 history.jsx 已删,DayRow 随本屏保留)。 地图 pin = 生涯打卡雪场坐标。 accent budget:data 蓝主(地图 pin + 折叠数字 + 最常去名);gold 仅 心流 stat + ROOY 见证(成长语义,沿用 08b);rooy 仅 ROOY 指数最高分。 list 里 lesson 绿 pip 沿用原 09。 ============================================================ */ /* 生涯打卡雪场(经纬度真实,用于投影 + 自适应取景 + 聚类都吃这份坐标)。 跨阿尔卑斯 / 日本 / 中国 → frameOf 自动框到欧亚带。hi = 生涯最常去。 本季列表只展开近段,Hakuba 是更早雪季的日本行——生涯图比列表更全, 属正常。 */ const CAREER_B_RESORTS = [ { id: 'zermatt', name: 'Zermatt', area: '阿尔卑斯', country: '瑞士', lon: 7.75, lat: 46.02, visits: 20, hours: 96, hi: true }, { id: 'verbier', name: 'Verbier', area: '阿尔卑斯', country: '瑞士', lon: 7.23, lat: 46.10, visits: 4, hours: 18 }, { id: 'hakuba', name: '白马 Hakuba', area: '日本', country: '日本', lon: 137.86, lat: 36.70, visits: 6, hours: 28 }, { id: 'wanlong', name: '万龙', area: '中国', country: '中国', lon: 115.47, lat: 40.93, visits: 12, hours: 45 }, ]; function HistoryScreenB({ onNav }) { const [openSeasons, setOpenSeasons] = React.useState({ '25-26': true }); const [selectedId, setSelectedId] = React.useState('zermatt'); // 最常去读出(展开后随 pin 更新) const [expanded, setExpanded] = React.useState(false); // 展开 = 地图加高 + 生涯详情(搬 08b) const seasons = [ { id: '25-26', year: '2025–26', summary: '12 天 | 64.8 km | 最爱 Zermatt', days: [ { date: '4.30', place: 'Zermatt', runs: 13, km: 8.4, time: '2:18 h', lesson: true }, { date: '4.22', place: 'Verbier', runs: 9, km: 6.2, time: '1:42 h', lesson: false }, { date: '4.18', place: 'Zermatt', runs: 11, km: 7.1, time: '2:04 h', lesson: true }, { date: '4.10', place: 'Saas-Fee', runs: 7, km: 4.6, time: '1:18 h', lesson: false }, { date: '3.16', place: 'Zermatt', runs: 14, km: 9.8, time: '2:42 h', lesson: true }, ], }, { id: '24-25', year: '2024–25', summary: '8 天 | 42.8 km | 最爱 万龙', days: [ { date: '3.04', place: '万龙雪场', runs: 12, km: 7.4, time: '3:12 h', lesson: true }, { date: '2.18', place: '万龙雪场', runs: 9, km: 5.8, time: '2:38 h', lesson: false }, { date: '1.25', place: '南山雪场', runs: 7, km: 4.2, time: '1:46 h', lesson: false }, ], }, { id: '23-24', year: '2023–24', summary: '4 天 | 18.2 km | 最爱 南山', days: [], }, ]; const sel = CAREER_B_RESORTS.find(r => r.id === selectedId) || CAREER_B_RESORTS[0]; // 地图 hero:full-bleed。pad 0.35 = 原始取景比例(不放大地图内容,展示完整 // 世界带;放大只作用于 pin 的 markerScale)。展开时地图向下加高(顶部间距不 // 变),并按新宽高比重算取景。biasY 把 pin 顶到上部。 const HERO_W = 390; const mapH = expanded ? 460 : 340; const heroView = (() => { const v = frameOf(CAREER_B_RESORTS, HERO_W / mapH, { pad: 0.35, minW: 150 }); return { ...v, y: v.y + v.h * 0.20 }; })(); /* 展开/收起键:收起态固定贴地图右上角(随地图一起滚走,不浮空于列表 之上);展开态 sticky 浮在视口右上角(下滚也钉住、随时一键收起)。 按钮样式两态共用,定位策略由 btnStyle 注入。 */ const renderToggle = (btnStyle) => ( ); return (
onNav && onNav('home')} title="滑行历史"/>
{/* 展开态:展开/收起键 sticky 浮在视口右上角(展开后下滚也钉住、随时 一键收起),z-index 10 压在地图之上。收起态的键不浮空——它固定贴 地图右上角、随地图一起滚走(见下方地图 div 内)。 */} {expanded && (
{renderToggle({ position: 'absolute', top: 0, right: 12, pointerEvents: 'auto' })}
)} {/* ── HERO · 生涯打卡地图(替换原黑卡)─────────────── 折叠/展开态:地图 + 3 个生涯数字(天/里程/雪场)浮在底部渐变上。 展开态地图向下加高(顶部间距不变),浮层数字原位不动(展开态 第一行即此),下方铺开生涯详情(第二行 stats 起步)。 */}
{ if (!expanded) setExpanded(true); }} style={{ position: 'relative', height: mapH, overflow: 'hidden', cursor: expanded ? 'default' : 'pointer', background: 'oklch(96.8% 0.007 240)', borderTop: `0.5px solid ${R.hair}`, transition: 'height 340ms cubic-bezier(0.32, 0.72, 0, 1)', }}> { if (!expanded) setExpanded(true); setSelectedId(c.members.find(m => m.hi)?.id || c.members[0].id); }} /> {/* eyebrow:地图左上角「生涯总览」kicker —— 标注本图是全生涯口径 (区别于 08b season-c 的单雪季 hero)。中文 eyebrow 不 uppercase / 不 tracking(仅 season-b 那块英文 eyebrow 才上 tracking);绝对定位 浮在地图上,折叠/展开两态都随地图一起走。 */}
生涯总览
{/* 底部渐变托底(始终存在,展开/折叠都保留)*/}
{/* 3 个生涯数字浮层(折叠/展开都浮在底部渐变上;展开态第一行即此)*/}
{[ { v: '24', u: '天', l: '生涯滑行', a: 'left', g: 5 }, { v: '187', u: 'km', l: '累计里程', a: 'center', g: 4 }, { v: String(CAREER_B_RESORTS.length), u: '个', l: '打卡雪场', a: 'right', g: 4 }, ].map((s, i) => (
{s.v} {s.u}

{s.l}

))}
{/* 收起态:展开键固定贴地图右上角,随地图一起滚走(不浮空于列表之上)*/} {!expanded && renderToggle({ position: 'absolute', top: 12, right: 12, zIndex: 10 })}
{/* ── 展开态:生涯详情(结构搬 08b,数据换生涯口径)── 第一行(24天/187km/4个)已浮在地图渐变上(折叠/展开共用),不在此 重复;这里从第二行起步。雪季 ↔ 累计垂降 对调:雪季置左作锚,垂降 居中。展开态保留地图底部渐变。 */} {expanded && (
{/* ── 第二行:3-up(雪季 / 累计垂降 / 心流)── grid 与第一行同列严格 对齐;除心流(gold)外数字用 data 蓝(同第一行色系)。top padding 0, 与浮层第一行紧贴成一组。 */}
{[ { v: String(seasons.length), u: '季', l: '雪季', a: 'left', g: 4 }, { v: '56.9', u: 'km', l: '累计垂降', a: 'center', g: 4 }, { v: '98.4', u: 'km', l: '心流', a: 'right', g: 4, color: R.gold }, ].map((s, i) => (
{s.v} {s.u}

{s.l}

))}
{/* 滑行模式(生涯)*/} 滑行模式
{/* 雪况(生涯)*/} 雪况
{/* 高光时刻(生涯纪录,保持列表)*/} 高光时刻
)} {/* Seasons — 仅折叠态显示(展开态隐藏)*/} {!expanded && seasons.map((s, si) => { const open = openSeasons[s.id]; return (
setOpenSeasons({ ...openSeasons, [s.id]: !open })} style={{ padding: '0 22px', marginTop: si === 0 ? 24 : 0, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 12, }}>
{s.year}

{s.summary.split(' | ').map((part, i, arr) => ( {part} {i < arr.length - 1 && |} ))}

{open && s.days.length > 0 && (
{s.days.map((d, i) => ( onNav && onNav('daily')}/> ))}
)}
); })}
); } /* DayRow — 单日记录行:左 date pill (月/日) + 中 place + meta (趟 数 / 里程 / 时长 vertical-bar 分隔). lesson flag 是 pill 角上的 ROOY 绿小点,不放文字标签。原定义于 history.jsx,旧 09 删除后随 本屏保留(本屏 seasons 列表依赖它)。 */ function DayRow({ day, onClick }) { const [month, dayNum] = day.date.split('.'); return (
{month} 月 {dayNum} {day.lesson && ( )}

{day.place}

{day.runs} 趟 | {day.km} km | {day.time}

); } Object.assign(window, { HistoryScreenB, DayRow });