lesson-card.md
组件 · COMPONENT

课节卡 LessonCard

一节课:16:9 暗底缩略图,四角是编号、档位角标、时长;未解锁时整卡压暗并居中一把锁;看完后整卡朱染。

已完成整卡朱染 + 标题行「✓ 已完成」
import { LessonCard } from './screens/v4/course/LessonCard.jsx'

// 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据]
const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }]
const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true }
const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 }

export function Done() {
  return (
    <div style={{ width: 280 }}>
      <LessonCard si={0} li={3} a={lesson('l-4', '双链与反向链接', 0)} plan={OPEN} done tierTag />
    </div>
  )
}
试看免费课节对未购者显朱色「试看」角标
import { LessonCard } from './screens/v4/course/LessonCard.jsx'

// 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据]
const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }]
const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true }
const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 }

export function Trial() {
  return (
    <div style={{ width: 280 }}>
      <LessonCard si={0} li={0} a={lesson('l-1', '为什么是 Obsidian', 0)} plan={GUEST} done={false} tierTag />
    </div>
  )
}
会员 · 未解锁暗底锁罩;锁孔是这张卡唯一的朱色
import { LessonCard } from './screens/v4/course/LessonCard.jsx'

// 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据]
const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }]
const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true }
const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 }

export function Locked() {
  return (
    <div style={{ width: 280 }}>
      <LessonCard si={1} li={1} a={lesson('l-7', 'Dataview:笔记变数据库', 2)} plan={GUEST} done={false} tierTag />
    </div>
  )
}
拓展 · 未解锁传 catName 时脚注带系列名
import { LessonCard } from './screens/v4/course/LessonCard.jsx'

// 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据]
const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }]
const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true }
const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 }

export function Extension() {
  return (
    <div style={{ width: 280 }}>
      <LessonCard si={4} li={0} a={lesson('l-x', '用笔记管理人脉', 3)} plan={GUEST} done={false} catName="人脉管理" />
    </div>
  )
}
骨架态
import { LessonCard } from './screens/v4/course/LessonCard.jsx'

export function Loading() {
  return (
    <div style={{ width: 280 }}>
      <LessonCard loading />
    </div>
  )
}

属性

属性取值默认说明
si / linumber系列下标 / 课节下标,生成左上 S1 · E04 编号
a课节位置数组见下表
plan{ canWatchCourse(ci), canWatchLesson(a) }权限视图:决定角标显隐与是否上锁
doneboolean已完成 → 整卡朱染
tierTagbooleanfalse标题前显档位胶囊(带反光)
catNamestring脚注前缀系列名
cinumber0课程下标(传给 canWatchCourse)
loadingbooleanfalse骨架态

课节位置数组 a

下标含义
0标题;无封面时截首段(「:」前,≤ 12 字)作卡面短标题
1时长文字,右下角显示
2简介,作脚注
3Markdown 笔记(卡片不用)
4是否免费
5 / 6视频源标签 / 播放器 uid(卡片不用)
7课节 ID
8行数据:cover_urlrequired_levelsourceduration_minutes

在 edu 里,悬停可看的课节会预取播放凭证(hoverPrefetchProps)。设计系统站用 src/stubs/playUrlCache.js 的空实现顶替,源文件一字未改。

铁律

✓ 要四角元素(编号、时长、角标)各自带深色小底
✗ 不要给整张封面加暗色蒙层
✓ 要有封面就不叠短标题——封面图自己写着课名
✗ 不要锁定用 backdrop 模糊(同屏几十张卡,GPU 吃不消)