Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 KiB
易班 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 页面创建
- 打开模板仓库页面
- 点击 Use this template
- 创建自己的新仓库
- 克隆新仓库并安装依赖
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/yx-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/courseiduid
生产环境中,只要存在以上任意参数,就会认为当前处于易班环境。
本地开发环境中,即使 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/autoallowTargetClick: 是否允许点击高亮目标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 模板仓库
如果你要把这个项目作为模板发布:
- 推送到 GitHub
- 进入仓库 Settings
- 勾选 Template repository
之后其他人就可以通过 Use this template 使用它。
License
MIT