Files
food-add-edu/README.md
T
zhongyidianandClaude Opus 4.8 a98b1d383e docs: 更新模板使用文档
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 13:40:07 +08:00

623 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 易班 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. 克隆新仓库并安装依赖
```bash
git clone https://github.com/your-name/your-app.git
cd your-app
bun install
bun dev
```
### 通过 GitHub CLI 创建
```bash
gh repo create my-react-app --template your-name/react-template --public
cd my-react-app
bun install
bun dev
```
创建项目后,建议修改 [package.json](package.json) 中的 `name` 字段,以及 [src/app.config.ts](src/app.config.ts) 中的页面标题、描述和图标配置。
## 应用配置
页面标签页标题、描述、图标和 viewport 配置统一维护在 [src/app.config.ts](src/app.config.ts):
```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,
},
};
```
修改该配置后需要重启开发服务。
## 开发命令
```bash
bun dev # 启动开发服务器
bun run build # 类型检查并构建生产版本
bun run preview # 预览生产构建
bun run lint # 代码检查
bun run lint:fix # 自动修复代码问题
```
## 开发规范
完整的 Claude Code/自动化开发规范见 [CLAUDE.md](CLAUDE.md)。日常开发遵守以下约定:
- 使用 Bun 执行项目脚本,提交前确保 `bun run lint` 通过;涉及路由、构建配置、页面结构或类型系统的改动建议同时运行 `bun run build`。
- 代码风格以 [biome.json](biome.json) 为准:tab 缩进、双引号、Biome 检查和导入整理。
- 组件、页面、store、API 的类型不要和实现代码写在同一个文件;组件类型放同目录 `组件名.types.ts`,store 类型放 [src/stores/types/](src/stores/types/),跨模块共享类型才放入 [src/types/](src/types/)。
- 短的静态 Tailwind class 可直接写在 JSX 中;只有需要条件合并、外部 `className`、冲突合并或组合 CSS Module 时才使用 `cn`。
- Tailwind class 过长、重复、条件组合复杂,或直接实现过于冗余时,再提取为局部常量、同目录文件、CSS Module 或通用组件。
- 新增页面沿用 [src/router/index.tsx](src/router/index.tsx) 中的 `lazyRoute` 和 `RouteFallback` 模式,导入优先使用 `@` 指向 `src` 的路径别名。
- 新增通用组件时,按需维护 [src/components/common/index.ts](src/components/common/index.ts),并考虑在 [src/pages/Components/index.tsx](src/pages/Components/index.tsx) 补充示例。
## 课程交互源码
模板中的基础组件和课程交互不是外部 React 运行时包,而是由 `@yuxiang/cli` 复制并提交到当前项目:
```text
src/components/ui/ # Button、Portal、Overlay、Toast
src/components/course-interactions/ # GuideDialog、GuideSpotlight、DragDrop
src/course-runtime/ # 固定课程画布运行时
src/lib/cn.ts # 本地 class 合并工具
```
项目根目录的 [components.json](components.json) 配置 copied source 的目录与 alias。需要新增能力时运行:
```bash
bunx @yuxiang/cli add <item>
```
CLI 默认不会覆盖不同内容的本地文件。安装后请把源码作为模板代码维护,不要重新添加 `@yuxiang/react` 或 `@yuxiang/core` 依赖。
## 真实课程案例
本地打开以下路由可体验连贯的课程流程:
```text
http://localhost:5173/#/course-demo
```
案例包含课程导入与分支、页面 Spotlight、课程资源分类拖拽、错误/正确 Toast、完成 Overlay,以及易班或 session fallback 的进度保存与刷新恢复。
## 组件开发 Smoke 页
本地开发时可以打开组件示例页查看和测试基础组件:
```text
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/pages/Components/index.tsx) 中补充对应示例。
## 目录结构
```text
src/
├── api/ # API 请求相关代码
├── assets/ # 项目资源
├── components/ # 组件
│ ├── business/ # 业务组件
│ └── common/ # 通用组件
├── pages/ # 页面组件
├── router/ # 路由配置
├── stores/ # Zustand 状态管理
│ └── modules/ # 状态模块
│ ├── app.ts # 应用状态
│ └── user.ts # 易班用户状态
├── styles/ # 全局样式
├── types/ # TypeScript 类型
└── utils/ # 工具函数
├── useOrientation.ts # 横竖屏检测
├── useScale.ts # 1920x1080 整体缩放
├── vconsole.ts # 调试工具
└── yiban.ts # 易班 API 封装
```
## 易班开发说明
模板面向易班 H5 / 静态资源相对路径部署,Vite `base` 固定为 `"./"`,确保构建产物在未知子路径或压缩包上传场景下仍能引用资源。除非部署环境明确要求根路径资源,否则不要轻易改成 `"/"`。
### videoajax.js 使用
模板已集成易班 videoajax.js,提供以下功能:
- [public/videoajax/jquery-3.7.1.min.js](public/videoajax/jquery-3.7.1.min.js)
- [public/videoajax/videoajax.js](public/videoajax/videoajax.js)
这些文件需要和构建后的页面同源、同相对路径部署。若易班平台提供了新的 `videoajax.js`,请同步确认来源、版本和分发权限后再替换,并重新验证用户数据、课程进度、断点和答题记录相关能力。
### 易班环境判断
模板启动时会从 URL 中读取:
- `courseId` / `courseid`
- `uid`
生产环境中,只要存在以上任意参数,就会认为当前处于易班环境。
本地开发环境中,即使 URL 带有 `courseId` / `uid`,也默认使用 `sessionStorage`,避免误请求易班接口:
```text
http://localhost:5173/?courseId=123&uid=456
```
如果本地确实需要强制走易班接口,可追加:
```text
http://localhost:5173/?courseId=123&uid=456&yiban=1
```
模板已集成以下易班能力:
#### 1. 存取用户数据
```ts
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. 课程进度管理
```ts
import { reportCourseProgress, getCourseProgress } from '@/utils/yiban';
// 上报课程进度(自动读取 store 中的 courseId 和连接状态)
await reportCourseProgress(order);
// 获取课程进度
const progress = await getCourseProgress(order);
if (progress) {
console.log('当前进度:', progress.data.process);
}
```
#### 3. 播放断点管理
```ts
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. 用户答题记录
```ts
import { saveUserAnswer } from '@/utils/yiban';
// 保存答题记录
await saveUserAnswer({
userId: '123456',
lessonId: '001',
courseId: '789',
questionOrder: 1,
answer: 'A',
});
```
#### 5. 用户状态管理
```ts
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 存储
### 课程画布适配
模板采用 [src/app.config.ts](src/app.config.ts) 中的 `appConfig.design` 作为课程设计稿尺寸,默认是 `1920 x 1080`。页面内容按该设计稿开发,App 外层会通过整体缩放适配不同屏幕。
当前默认缩放策略:
- PC 端:按可视区域等比缩放并居中显示
- 大屏:`appConfig.scale.max` 默认为 `1`,不会超过设计稿原始尺寸,周围留白
- 移动端:优先横屏访问,竖屏时显示横屏提示
- 视口:缩放逻辑优先读取 `visualViewport`,移动端地址栏显示/隐藏时更稳定
- 页面内容:按 `1920 x 1080` 设计稿进行开发,不使用 rem 适配
整体缩放逻辑维护在 [src/utils/useScale.ts](src/utils/useScale.ts)。如需修改设计稿尺寸或最大放大比例,优先修改 [src/app.config.ts](src/app.config.ts)。
本地运行 `bun dev` 后,可以访问标准课程页示例:
```text
http://localhost:5173/#/course-demo
```
`CourseDemo` 用于展示标准课程页面结构、易班 session fallback、Toast、Overlay 和 SpotlightGuide 的基础接入方式。
## 弹层基础组件
模板内置 `Portal` 和 `Overlay`,用于课程页面组合自定义弹窗、提示层、章节面板、全屏 loading 等弹层内容。
`Overlay` 已经内置 `Portal`,默认会挂载到 App 缩放画布内部,适合课程弹层跟随 `1920 x 1080` 画布一起缩放,并支持直接放入课程自己的切图和内容:
```tsx
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"`:
```tsx
<Overlay placement="app" open={open} onClose={() => setOpen(false)}>
{/* 和课程画布一起缩放的弹层 */}
</Overlay>
```
如果需要挂载到业务自己的容器,可以传入 `container`。`container` 支持 DOM 元素、React ref 或返回元素的函数:
```tsx
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`,页面组件中可以直接使用:
```tsx
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`:
```tsx
<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 渲染:
```tsx
<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`: 可投放区域。
基础示例:
```tsx
import { DragDropProvider, Draggable, Droppable } from "@/components/business";
import { useScale } from "@/utils";
const scale = useScale();
<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`:
```tsx
<Button data-guide="start-button">开始学习</Button>
```
在页面初始化时启动引导:
```tsx
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` 只持久化已完成记录:
```ts
completedGuideMap: {
"lesson-1:intro": true,
}
```
默认同一个 `courseId + guideId` 只显示一次。点击“完成”或“跳过”都会标记为已完成,后续不会自动显示。
如需重新显示:
```ts
useGuideStore.getState().resetGuide("lesson-1", "intro");
```
### Step 配置
```ts
{
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`:
```tsx
const { nextStep } = useGuideStore();
<Button
data-guide="play-button"
onClick={() => {
playVideo();
nextStep();
}}
>
播放
</Button>
```
### 样式与层级
`SpotlightGuide` 默认通过 Portal 挂载到 `document.body`,可以正确适配整体缩放后的坐标。
如遇到课程弹窗、横屏提示等全局层级冲突,可以通过 `theme.zIndex` 调整:
```tsx
<SpotlightGuide theme={{ zIndex: 1200 }} />
```
开发环境中,如果目标元素不存在,页面会显示开发提示面板,方便检查 `data-guide` 或 selector;生产环境默认会自动跳过找不到目标的步骤。
组件内置了遮罩淡入、高亮呼吸和提示框入场动画,并兼容系统的 `prefers-reduced-motion` 设置。
## 路由懒加载
模板内置了 `lazyRoute` 工具函数,新页面可以这样配置:
```tsx
{
path: "about",
lazy: lazyRoute(() => import("../pages/About")),
}
```
页面组件使用默认导出:
```tsx
export default function About() {
return <div>About</div>;
}
```
## 发布为 GitHub 模板仓库
如果你要把这个项目作为模板发布:
1. 推送到 GitHub
2. 进入仓库 **Settings**
3. 勾选 **Template repository**
之后其他人就可以通过 **Use this template** 使用它。
## License
MIT