/** * [INPUT]: 依赖 react、components/v4/CoverImage;props: ci/si/li(下标)、a(课节位置数组)、plan(usePlanV4 视图)、done(布尔)、catName?、onClick * [OUTPUT]: 对外提供 LessonCard 组件——16:9 暗底课节卡(含试看/付费/拓展角标、时长角标、S/E 编号); * 根节点走 controls.css 的 zzm-cardlink(白卡承托+悬停高亮描边/上浮/柔影,与综合首页课程卡同手感); * 完成态=容器级 zzm-cardlink--done(整卡朱染+朱描边+标题行「✓ 已完成」chip,v20.0.0 取代右上角小✓圆徽); * 有 a[8].cover_url 时经 CoverImage 三态渲染封面(加载暗底/淡入+蒙层/失败回退兜底面)—— * ★ v23.5.0:卡面中央短标题+朱线【迁入 fallback 槽】,只在无封面/封面 404 时出现; * 有封面则整层不渲染(封面图自带课程名,再叠一遍即重复书写),蒙层中段同步收薄让画面亮出来; * 未解锁(canWatchLesson=false)时叠暗底锁头遮罩 * [POS]: screens/v4/course 的共享课节卡,NodePage 与 SeriesPage 共用;tagOf/shortOf/codeOf 为模块私有 * (react-refresh/only-export-components 禁止组件文件混导出函数) * 角标(v21):付费(2)/拓展(3) 用逐课节 canWatchLesson(会员/买父课/单买任一命中即隐形); * 免费档用板块级 canWatchCourse 显朱「试看」;点击放行与否由屏侧 canWatchLesson 逐课节判定 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md */ import { CoverImage } from '../../../components/v4/CoverImage' import { Skel } from '../../../components/v4/Skeleton' import { hoverPrefetchProps } from '../../../lib/api/playUrlCache' import { TierPill } from './TierPill' const ACCENT = 'var(--zzm-accent,#A6402F)' // 缩略图外壳单一真相源:真实态(spread + position:relative)与骨架态【同时】引用——改这里两态同变 const LC_THUMB = { aspectRatio: '16/9', borderRadius: 12, background: '#131316', overflow: 'hidden' } // ── 课节编号(DC 行 2871):S{系列} · E{集数,两位} ── const codeOf = (si, li) => 'S' + (si + 1) + ' · E' + String(li + 1).padStart(2, '0') // ── 兜底短标题(DC 行 2948):按':'切首段,>12 字截断加 … // 【只在无封面/封面加载失败】时出现——后台封面自带课程名,再叠一遍就是重复书写 ── const shortOf = (title) => { const s = (title || '').split(':')[0] return s.length > 12 ? s.slice(0, 12) + '…' : s } // ── 角标判定(v21):B站公开片→免费;付费(2)/拓展(3)→需购买(会员/买课/单买任一解锁,用逐课节真闸);免费档→试看 ── // ⚠️ B 站分支必须【最先】判且【无视 a[4]】:canAccessVideo 对 bilibili 源直接放行(公开引流片, // 免登录直接看),而 a[4] 只看 required_level<2、完全 source-blind。角标须与真闸同源。 // v21:付费与拓展同权(is_premium 不再参与鉴权),标签只随课时属性走——2=「付费」/3=「拓展」; // 用逐课节 canWatchLesson(会员/买父课/单买任一命中即可看→隐形),不再有「围墙花园」的独立样式。 function tagOf(plan, ci, a) { // B 站片对【未购者】显朱色「免费」(免登录、点开即看);对【已购者】隐形(已购不打扰)。 if (a[8]?.source === 'bilibili') { return plan.canWatchCourse(ci) ? { tag: '免费', tagOp: 0, tagBg: 'transparent', tagC: 'transparent' } : { tag: '免费', tagOp: 1, tagBg: ACCENT, tagC: '#FFFFFF' } } const lvl = a[8]?.required_level ?? 0 // 会员(2)/拓展(3):需购买课程。逐课节真闸——可看则隐形,否则显灰标。 if (lvl >= 2) { const name = lvl >= 3 ? '拓展' : '会员' return plan.canWatchLesson(a) ? { tag: name, tagOp: 0, tagBg: 'transparent', tagC: 'transparent' } : { tag: name, tagOp: 1, tagBg: 'rgba(0,0,0,.55)', tagC: 'rgba(255,255,255,.92)' } } // 免费(0/1):登录即看;非会员板块显朱「试看」引流,可看则隐形。 return plan.canWatchCourse(ci) ? { tag: '试看', tagOp: 0, tagBg: 'transparent', tagC: 'transparent' } : { tag: '试看', tagOp: 1, tagBg: ACCENT, tagC: '#FFFFFF' } } // 16:9 暗底课节卡。catName 缺省时脚注只显简介(总览变体,DC 行 421-422)。 // tierTag:传入时标题前显「免费/付费/拓展」档位胶囊(课程详情用;学习地图 NodePage 不传,保持原样)。 export function LessonCard({ ci = 0, si, li, a, plan, done, catName, onClick, tierTag, loading = false }) { // ── 骨架态:同一 zzm-cardlink 外壳 + 16:9 暗底缩略 + 标题/脚注两行占位(无 hooks,可顶部早退) ── if (loading) { return (