2026-08-18 17:33:42 +08:00
2026-06-04 11:44:12 +08:00
2026-08-18 13:39:30 +08:00
2026-08-18 13:39:30 +08:00
2026-06-04 11:44:12 +08:00

易班 H5 开发模板

一个基于 React 19、TypeScript、Vite、React Router、Tailwind CSS 和 Zustand 的易班平台 H5 开发模板。

特性

  • React 19 + TypeScript
  • Vite 8 开发与构建
  • React Router 7 路由管理(支持懒加载)
  • Tailwind CSS 4 原子化样式
  • Zustand 轻量状态管理
  • Biome 代码格式化与检查
  • Husky + lint-staged 提交前检查
  • 1920x1080 整体缩放适配
  • 横屏访问提示
  • vconsole 移动端调试工具(仅开发环境)
  • 易班 videoajax.js 封装
  • 易班用户信息状态管理
  • @ 指向 src 的路径别名
  • 通过 @yuxiang/cli 安装并提交可直接修改的课程交互源码
  • 组件分层:Registry copied source、模板适配器、业务组件、页面组件

使用模板

通过 GitHub 页面创建

  1. 打开模板仓库页面
  2. 点击 Use this template
  3. 创建自己的新仓库
  4. 克隆新仓库并安装依赖
git clone https://github.com/your-name/your-app.git
cd your-app
bun install
bun dev

通过 GitHub CLI 创建

gh repo create my-react-app --template your-name/react-template --public
cd my-react-app
bun install
bun dev

创建项目后,建议修改 package.json 中的 name 字段,以及 src/app.config.ts 中的页面标题、描述和图标配置。

应用配置

页面标签页标题、描述、图标和 viewport 配置统一维护在 src/app.config.ts:

export const appConfig = {
  title: "React Vite App",
  description: "易班 H5 课程模板",
  favicon: "./vite.svg",
  design: {
    width: 1920,
    height: 1080,
  },
  scale: {
    max: 1,
  },
  viewport: {
    width: "device-width",
    initialScale: 1.0,
    minimumScale: 1.0,
    maximumScale: 1.0,
  },
};

修改该配置后需要重启开发服务。

开发命令

bun dev            # 启动开发服务器
bun run build      # 类型检查并构建生产版本
bun run preview    # 预览生产构建
bun run lint       # 代码检查
bun run lint:fix   # 自动修复代码问题

开发规范

完整的 Claude Code/自动化开发规范见 CLAUDE.md。日常开发遵守以下约定:

  • 使用 Bun 执行项目脚本,提交前确保 bun run lint 通过;涉及路由、构建配置、页面结构或类型系统的改动建议同时运行 bun run build。
  • 代码风格以 biome.json 为准:tab 缩进、双引号、Biome 检查和导入整理。
  • 组件、页面、store、API 的类型不要和实现代码写在同一个文件;组件类型放同目录 组件名.types.ts,store 类型放 src/stores/types/,跨模块共享类型才放入 src/types/。
  • 短的静态 Tailwind class 可直接写在 JSX 中;只有需要条件合并、外部 className、冲突合并或组合 CSS Module 时才使用 cn。
  • Tailwind class 过长、重复、条件组合复杂,或直接实现过于冗余时,再提取为局部常量、同目录文件、CSS Module 或通用组件。
  • 新增页面沿用 src/router/index.tsx 中的 lazyRoute 和 RouteFallback 模式,导入优先使用 @ 指向 src 的路径别名。
  • 新增通用组件时,按需维护 src/components/common/index.ts,并考虑在 src/pages/Components/index.tsx 补充示例。

课程交互源码

模板中的基础组件和课程交互不是外部 React 运行时包,而是由 @yuxiang/cli 复制并提交到当前项目:

src/components/ui/                  # Button、Portal、Overlay、Toast
src/components/course-interactions/ # GuideDialog、GuideSpotlight、DragDrop
src/course-runtime/                 # 固定课程画布运行时
src/lib/cn.ts                       # 本地 class 合并工具

项目根目录的 components.json 配置 copied source 的目录与 alias。需要新增能力时运行:

bunx @yuxiang/cli add <item>

CLI 默认不会覆盖不同内容的本地文件。安装后请把源码作为模板代码维护,不要重新添加 @yuxiang/react 或 @yuxiang/core 依赖。

真实课程案例

本地打开以下路由可体验连贯的课程流程:

http://localhost:5173/#/course-demo

案例包含课程导入与分支、页面 Spotlight、课程资源分类拖拽、错误/正确 Toast、完成 Overlay,以及易班或 session fallback 的进度保存与刷新恢复。

组件开发 Smoke 页

本地开发时可以打开组件示例页查看和测试基础组件:

http://localhost:5173/#/components

该路由仅用于开发时检查 copied source 的基础状态,不作为产品级组件展示墙。真实组合用法以 #/course-demo 为准。

当前 smoke 页包含:

  • Button: variant 和 size 示例
  • Toast: info / success / warning / error 轻提示示例
  • Portal: 挂载到 document.body 或自定义容器的浮层示例
  • Overlay: 默认背景、自定义 Tailwind 背景、背景图片、禁止点击背景关闭等交互示例
  • SpotlightGuide: 课程引导和高亮目标区域示例
  • Drag and Drop: 基于 dnd-kit 的拖拽到目标区域示例

后续新增通用组件时,可以在 src/pages/Components/index.tsx 中补充对应示例。

目录结构

src/
├── api/          # API 请求相关代码
├── assets/       # 项目资源
├── components/   # 组件
│   ├── business/ # 业务组件
│   └── common/   # 通用组件
├── pages/        # 页面组件
├── router/       # 路由配置
├── stores/       # Zustand 状态管理
│   └── modules/  # 状态模块
│       ├── app.ts   # 应用状态
│       └── user.ts  # 易班用户状态
├── styles/       # 全局样式
├── types/        # TypeScript 类型
└── utils/        # 工具函数
    ├── vconsole.ts       # 调试工具
    └── yiban.ts          # 易班 API 封装

易班开发说明

模板面向易班 H5 / 静态资源相对路径部署,Vite base 固定为 "./",确保构建产物在未知子路径或压缩包上传场景下仍能引用资源。除非部署环境明确要求根路径资源,否则不要轻易改成 "/"。

videoajax.js 使用

模板已集成易班 videoajax.js,提供以下功能:

这些文件需要和构建后的页面同源、同相对路径部署。若易班平台提供了新的 videoajax.js,请同步确认来源、版本和分发权限后再替换,并重新验证用户数据、课程进度、断点和答题记录相关能力。

易班环境判断

模板启动时会从 URL 中读取:

  • courseId / courseid
  • uid

生产环境中,只要存在以上任意参数,就会认为当前处于易班环境。

本地开发环境中,即使 URL 带有 courseId / uid,也默认使用 sessionStorage,避免误请求易班接口:

http://localhost:5173/?courseId=123&uid=456

如果本地确实需要强制走易班接口,可追加:

http://localhost:5173/?courseId=123&uid=456&yiban=1

模板已集成以下易班能力:

1. 存取用户数据

import { setUserStorage, getUserStorage } from '@/utils/yiban';

// 保存数据(自动根据易班连接状态选择存储方式)
await setUserStorage(lessonId, key, value);

// 读取数据
const result = await getUserStorage(lessonId, key);
if (result.status === 'success') {
  console.log(result.content);
}

// 强制指定存储方式
await setUserStorage(lessonId, key, value, { backend: 'session' });

2. 课程进度管理

import { reportCourseProgress, getCourseProgress } from '@/utils/yiban';

// 上报课程进度(自动读取 store 中的 courseId 和连接状态)
await reportCourseProgress(order);

// 获取课程进度
const progress = await getCourseProgress(order);
if (progress) {
  console.log('当前进度:', progress.data.process);
}

3. 播放断点管理

import { saveBreakPoint, getBreakPoint } from '@/utils/yiban';

// 保存播放断点
await saveBreakPoint({
  userId: '123456',
  courseId: '789',
  lessonId: '001',
  breakPoint: 120, // 秒
});

// 获取播放断点
const breakpoint = await getBreakPoint({
  userId: '123456',
  courseId: '789',
  lessonId: '001',
});

4. 用户答题记录

import { saveUserAnswer } from '@/utils/yiban';

// 保存答题记录
await saveUserAnswer({
  userId: '123456',
  lessonId: '001',
  courseId: '789',
  questionOrder: 1,
  answer: 'A',
});

5. 用户状态管理

import { useUserStore } from '@/stores';

function MyComponent() {
  const { userId, userName, setUserInfo, setYibanConnectionStatus } = useUserStore();
  
  // 设置易班连接状态
  setYibanConnectionStatus('connected');
  
  // 设置用户信息
  setUserInfo({
    userId: '123456',
    userName: '张三',
    userAvatar: 'https://...'
  });
}

移动端调试

开发环境自动启用 vconsole,可在移动端查看:

  • Console 日志
  • Network 请求
  • Element 结构
  • Storage 存储

课程画布适配

模板按 1920 x 1080 设计稿开发,由 src/course-runtime/canvas 的 CourseCanvas 组件承担整体缩放。CourseCanvas 默认 width=1920、height=1080、maxScale=1,如需其他设计稿尺寸或放大比例,显式传入对应 props 即可。

当前默认缩放策略:

  • PC 端:按可视区域等比缩放并居中显示
  • 大屏:maxScale 默认为 1,不会超过设计稿原始尺寸,周围留白
  • 移动端:优先横屏访问,竖屏时由 OrientationGuard 显示横屏提示
  • 视口:缩放逻辑优先读取 visualViewport,移动端地址栏显示/隐藏时更稳定
  • 页面内容:按 1920 x 1080 设计稿进行开发,不使用 rem 适配

本地运行 bun dev 后,可以访问标准课程页示例:

http://localhost:5173/#/course-demo

CourseDemo 用于展示标准课程页面结构、易班 session fallback、Toast、Overlay 和 SpotlightGuide 的基础接入方式。

弹层基础组件

模板内置 Portal 和 Overlay,用于课程页面组合自定义弹窗、提示层、章节面板、全屏 loading 等弹层内容。

Overlay 已经内置 Portal,默认会挂载到 App 缩放画布内部,适合课程弹层跟随 1920 x 1080 画布一起缩放,并支持直接放入课程自己的切图和内容:

import { Overlay } from "@/components/common";

<Overlay
  open={open}
  onClose={() => setOpen(false)}
  backgroundImage="dialog/overlay-bg.png"
  backgroundClassName="bg-cover bg-center"
  contentClassName="flex items-center justify-center"
>
  <img src="/images/dialog.png" alt="课程提示" />
</Overlay>;

说明:

  • backgroundImage: 传入 src/assets/images 下的图片路径,组件内部会通过 getBackgroundImage 处理。
  • backgroundClassName: 使用 Tailwind class 控制背景层样式,例如 bg-cover、bg-center、渐变、透明度等。
  • contentClassName: 控制内容层布局,弹窗视觉样式由具体课程页面自行决定。
  • closeOnBackgroundClick: 是否允许点击背景关闭,默认开启。
  • closeOnEscape: 是否允许按 Esc 关闭,默认开启。
  • lockScroll: 弹层打开时是否锁定页面滚动,默认开启。
  • variant: 提供 dim / light / transparent 三种基础背景。

Portal 挂载位置

Portal 默认挂载到 document.body,Overlay 默认挂载到 App 缩放画布内部。如果 Portal 也需要跟随模板的 1920 x 1080 课程画布一起缩放,可以使用 placement="app":

<Overlay placement="app" open={open} onClose={() => setOpen(false)}>
  {/* 和课程画布一起缩放的弹层 */}
</Overlay>

如果需要挂载到业务自己的容器,可以传入 container。container 支持 DOM 元素、React ref 或返回元素的函数:

import { useRef } from "react";
import { Overlay } from "@/components/common";

const rootRef = useRef<HTMLDivElement>(null);

<div ref={rootRef} className="relative" />;

<Overlay
  container={rootRef}
  className="absolute"
  open={open}
  onClose={() => setOpen(false)}
>
  {/* 挂到自定义容器里的弹层 */}
</Overlay>;

挂载优先级:container > placement > 组件默认值。Portal 默认 placement="body",Overlay 默认 placement="app"。如只需要改变渲染位置,也可以单独使用 Portal。

Toast 轻提示

模板内置 ToastProvider、ToastViewport 和 useToast,用于保存成功、提交失败、网络异常等轻量反馈。

App 已默认接入 ToastProvider 和 ToastViewport,页面组件中可以直接使用:

import { useToast } from "@/components/common";

const toast = useToast();

toast.success("保存成功");
toast.error("网络异常,请稍后重试");

支持:

  • toast.info(message)
  • toast.success(message)
  • toast.warning(message)
  • toast.error(message)
  • toast.show({ message, type, title, duration, className, data })

如果课程视觉风格不同,可以自定义 ToastViewport:

<ToastViewport
  classNames={{
    item: "rounded-full border-0 bg-black/80 px-8 py-4 text-white",
    title: "hidden",
    message: "text-white",
    close: "text-white/70",
  }}
/>

也可以完全接管单条 Toast 渲染:

<ToastViewport
  renderToast={(toast, { remove }) => (
    <div className="pointer-events-auto rounded-full bg-black/80 px-8 py-4 text-white">
      <span>{toast.message}</span>
      <button onClick={() => remove(toast.id)}>关闭</button>
    </div>
  )}
/>

本地运行 bun dev 后,可以访问 /#/components 查看 Toast 示例。

拖拽交互

模板引入 dnd-kit 作为拖拽基础能力,适合网络课程中常见的拖物品到目标区、分类、匹配、排序等交互。

已封装基础组件:

  • DragDropProvider: 拖拽上下文,默认启用 Pointer / Touch / Keyboard sensors。
  • Draggable: 可拖拽元素。
  • Droppable: 可投放区域。

基础示例:

import { DragDropProvider, Draggable, Droppable } from "@/components/course-interactions/drag-drop";
import { useCourseCanvas } from "@/course-runtime/canvas";

const { scale } = useCourseCanvas();

<DragDropProvider
  scale={scale}
  onDragEnd={(event) => console.log(event.active.id, event.over?.id)}
>
  <Draggable id="apple">苹果</Draggable>
  <Droppable id="fruit-zone">水果区域</Droppable>
</DragDropProvider>;

由于模板整体使用 transform: scale(...) 缩放课程画布,拖拽内容在画布内时建议给 DragDropProvider 传入当前 scale,避免拖拽元素和鼠标/手指移动不同步。

DragDropProvider 默认会根据 scale 修正拖拽启动距离和拖拽位移,并使用 pointerWithin 优先、closestCenter 兜底的碰撞检测策略,让“拖到目标区域”类课程交互更符合手指/鼠标位置直觉。

本地运行 bun dev 后,可以访问 /#/components 查看拖拽示例。

当前基础封装优先覆盖多端拖拽和投放区域判断。排序、分类、拼图等更上层课程交互可以继续基于这些组件封装。

引导聚光灯

模板内置 SpotlightGuide 引导组件,适合课程首次学习时突出关键操作区域。

基础使用

在页面元素上添加 data-guide:

<Button data-guide="start-button">开始学习</Button>

在页面初始化时启动引导:

import { useEffect } from "react";
import { SpotlightGuide } from "@/components/common";
import { useGuideStore } from "@/stores";

export default function LessonPage() {
  useEffect(() => {
    useGuideStore.getState().startGuide({
      courseId: "lesson-1",
      guideId: "intro",
      steps: [
        {
          id: "start",
          target: { dataGuide: "start-button" },
          title: "开始学习",
          description: "点击这里开始本节课程。",
          placement: "bottom",
        },
      ],
    });
  }, []);

  return <SpotlightGuide />;
}

持久化说明

引导状态接入 Zustand,并通过 Zustand persist 只持久化已完成记录:

completedGuideMap: {
  "lesson-1:intro": true,
}

默认同一个 courseId + guideId 只显示一次。点击“完成”或“跳过”都会标记为已完成,后续不会自动显示。

如需重新显示:

useGuideStore.getState().resetGuide("lesson-1", "intro");

Step 配置

{
  id: "start",
  target: { dataGuide: "start-button" },
  title: "开始学习",
  description: "点击这里开始本节课程。",
  placement: "bottom",
  allowTargetClick: true,
}

支持:

  • target: CSS selector 或 { dataGuide: "xxx" }
  • placement: top / bottom / left / right / auto
  • allowTargetClick: 是否允许点击高亮目标
  • padding / radius: 调整高亮区域
  • panelClassName: 针对单步自定义提示框样式

建议 data-guide 只使用字母、数字、横线和下划线,例如 start-button、question_1。

高级交互说明

allowTargetClick 只负责让高亮区域下方的真实元素可以被点击,不会自动切换引导步骤。

如果目标元素点击后需要进入下一步,需要在业务点击逻辑中手动调用 nextStep、finishGuide 或 closeGuide:

const { nextStep } = useGuideStore();

<Button
  data-guide="play-button"
  onClick={() => {
    playVideo();
    nextStep();
  }}
>
  播放
</Button>

样式与层级

SpotlightGuide 默认通过 Portal 挂载到 document.body,可以正确适配整体缩放后的坐标。

如遇到课程弹窗、横屏提示等全局层级冲突,可以通过 theme.zIndex 调整:

<SpotlightGuide theme={{ zIndex: 1200 }} />

开发环境中,如果目标元素不存在,页面会显示开发提示面板,方便检查 data-guide 或 selector;生产环境默认会自动跳过找不到目标的步骤。

组件内置了遮罩淡入、高亮呼吸和提示框入场动画,并兼容系统的 prefers-reduced-motion 设置。

路由懒加载

模板内置了 lazyRoute 工具函数,新页面可以这样配置:

{
  path: "about",
  lazy: lazyRoute(() => import("../pages/About")),
}

页面组件使用默认导出:

export default function About() {
  return <div>About</div>;
}

发布为 GitHub 模板仓库

如果你要把这个项目作为模板发布:

  1. 推送到 GitHub
  2. 进入仓库 Settings
  3. 勾选 Template repository

之后其他人就可以通过 Use this template 使用它。

License

MIT

S
Description
网络课程模版(易班)
Readme MIT
259 KiB
0 Stars 1 Watchers 1 Forks
Languages
TypeScript 89.1%
JavaScript 5.4%
CSS 5.3%
HTML 0.2%