/** * [INPUT]: 依赖 react 的 useMemo、components/v4/CoverImage、components/v4/Skeleton 的 Skel; * props: cat{name,sub,cover,ls[]}、hist(useV4Data().hist)、selected、onClick、loading(骨架态) * [OUTPUT]: 对外提供 SeriesCard 组件——课程总览的系列卡片(16:9 头图 + 简介 + 总时长 + 进度热力图) * [POS]: screens/v4/course 的系列卡,OverviewPage 每系列一张;视觉母版对齐综合首页 CourseCard(白卡+16:9头图) * 进度热力图:每课一格,完成态查 hist.doneIds.has(a[7]) 打勾;头图 cat.cover 优先,回退首课封面/星座线稿 * 封面经 CoverImage 三态渲染(加载期暗底、就绪淡入、失败回退线稿),不再先画线稿再跳变。 * ★ 头图【不叠任何蒙层】:本卡文案全在下方白卡区、图上零叠字,压暗只会让封面偏离上传原图的颜色 * ★ 骨架同源(loading):共用同一外壳(SC_CARD/SC_HEADER/SC_BODY),文案位换 Skel 块——改布局骨架自动跟随 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md */ import { useMemo } from 'react' import { CoverImage } from '../../../components/v4/CoverImage' import { Skel } from '../../../components/v4/Skeleton' const ACCENT = 'var(--zzm-accent,#A6402F)' const MONO = 'var(--zzm-mono)' // ── 布局单一真相源:真实态与骨架态共用 ── const SC_CARD = { borderRadius: 16, background: '#FFFFFF', overflow: 'hidden' } const SC_HEADER = { aspectRatio: '16/9', background: '#131316', position: 'relative', overflow: 'hidden' } const SC_BODY = { padding: '15px 17px 16px' } // 分钟 → 「N 分钟 / H 小时 M 分」。总时长由课节 duration_minutes 求和派生。 function fmtDur(min) { if (!(min > 0)) return '—' if (min < 60) return `${min} 分钟` const h = Math.floor(min / 60) const m = min % 60 return m ? `${h} 小时 ${m} 分` : `${h} 小时` } export function SeriesCard({ cat, hist, selected, onClick, loading = false }) { // ls 用 useMemo 稳定引用(cat.ls||[] 的逻辑表达式否则每帧新建、令下游 useMemo 依赖恒变) const ls = useMemo(() => cat?.ls || [], [cat]) // 每课一格的完成态(a[7]=课节ID);doneIds 每次渲染新建,用 useMemo 缓存派生数组 // (hooks 必须无条件先跑——骨架态 ls 为空、hist 可能缺省,用可选链兜底) const done = useMemo(() => ls.map((a) => hist?.doneIds?.has(a[7])), [ls, hist]) const doneCount = done.filter(Boolean).length const totalMin = useMemo( () => ls.reduce((t, a) => t + (Number(a[8]?.duration_minutes) || 0), 0), [ls], ) // 头图:系列封面优先 → 回退系列内首个有封面的课节 → 再回退星座线稿 const cover = cat?.cover || ls.find((a) => a[8]?.cover_url)?.[8]?.cover_url || null // ── 骨架态:同一外壳 + 16:9 暗底头图 + 文案/热力图位换 Skel(热力图占位取记忆或默认 8 格) ── if (loading) { return (