Files
food-add-edu/README.md
T

314 lines
13 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 平台交付,按 1920×1080 设计稿开发,整体等比缩放适配 PC 与移动端横屏。
## 课程概要
### 学习路径
学生首次学习沿剧情主线推进,完成每个阶段的调查任务即可积累经验、提升侦探等级:
```text
实习侦探(序幕)
↓
助理侦探(第一幕)
↓
初级侦探(第二幕)
↓
中级侦探(第三幕)
↓
高级侦探(第四幕)
↓
食安神探(终章 · 零食风暴)
```
### 四条调查线
序幕从前情舆论视频切入,学生从求助信中梳理出四条调查线索,分别对应四幕核心案件:
| 案件 | 调查线 | 待核验说法 | 核心问题 |
| --- | --- | --- | --- |
| 🌈 色彩迷局 | 着色剂身份线 / 来源辨析线 | 「颜色这么鲜艳,色素肯定没少加」 | 食品里的「色素」是什么?天然与人工合成能直接代表安全吗? |
| 🍊 味觉密码 | 香精与风味线 | 「水果味都是香精调出来的」 | 食品的香气从哪里来?香精的使用是否有据可依? |
| 🍮 Q 弹真相 | 增稠剂线 | 「果冻 Q 弹是因为加了很多胶」 | 「胶」是什么?增稠剂在食品中起什么作用? |
| ⏳ 保鲜疑云 | 防腐剂线 | 「面包放得久是因为防腐剂多」 | 防腐剂为什么被使用?如何判断其安全性? |
每条调查线统一遵循五步调查轨迹:**待核验说法 → 调查问题 → 探查方式 → 关键证据 → 调查判定**。
### 核心教学要点
- **着色剂**:赋予或改善食品色泽的一类食品添加剂;食品有颜色不等于额外加入了着色剂,颜色鲜艳也不代表用量多。
- **身份核验**:能染色 ≠ 可作为食品着色剂。苏丹红是工业染料、非法添加物,不属于食品着色剂;胭脂红、栀子黄等才具有食品着色剂身份。
- **天然 vs 人工合成**:两者首先反映的是来源与获得方式不同,不能直接等同于「安全」与「有害」;无论哪种来源,都要结合具体物质、食品类别及相应标准判断。
- **铝色淀**:把水溶性着色剂转变为水不溶性着色形态,满足彩色糖衣、蛋糕装饰等不掉色、不迁移的加工需要。
- **国标意识**:GB 2760-2024《食品安全国家标准 食品添加剂使用标准》对每种允许使用的添加剂的适用食品类别、最大使用量、使用范围均有严格规定。
### 案件档案与学习资源
课程把调查过程中产生和解锁的内容沉淀为「零食侦探事务所」资源清单,首次学习沿剧情推进,完成后可脱离剧情顺序回看复习:
| 分类 | 功能定位 |
| --- | --- |
| 01 物证材料 | 案件调查的原始物证,调查的事实起点 |
| 02 收件箱 | 市民求助、补充线索及后续来信 |
| 03 调查资料 | 科普动画、微课、案例短片、旧案资料,承担主要知识讲解 |
| 04 调查成果 | 学生实际完成过的调查操作及判断记录 |
| 05 添加剂档案 | 持续积累的食品添加剂知识库,学一个解锁一个 |
| 06 国标档案 | 培养「遇到添加剂问题查标准」的意识,提供标准依据 |
| 07 案件线索板 | 剧情导航与学习进度导航,首页常驻入口 |
| 08 调查报告 | 对一幕中多条调查线的最终汇总与消费建议 |
| 09 侦探等级 | 学习者身份与整体课程成长记录 |
## 技术栈
- React 19 + TypeScript
- Vite 8 开发与构建
- React Router 7 路由管理(Hash 路由 + 懒加载)
- Tailwind CSS 4 原子化样式
- Zustand 轻量状态管理
- GSAP + @gsap/react 动效
- dnd-kit 拖拽交互
- Biome 代码格式化与检查
- Husky + lint-staged 提交前检查
- 1920×1080 整体缩放适配
- 横屏访问提示
- vconsole 移动端调试(仅开发环境)
- 易班 videoajax.js 封装与用户信息状态管理
## 开发命令
```bash
bun install # 安装依赖
bun dev # 启动开发服务器
bun run build # 类型检查并构建生产版本
bun run preview # 预览生产构建
bun run lint # 代码检查
bun run lint:fix # 自动修复代码问题
```
修改代码后至少运行 `bun run lint`;涉及路由、构建配置、页面结构或类型系统的改动,同时运行 `bun run build`。
## 应用配置
页面标题、描述、图标、课程标识和 viewport 统一维护在 [src/app.config.ts](src/app.config.ts)。修改后需重启开发服务。
## 目录结构
```text
src/
├── api/ # API 请求封装
├── assets/ # 静态资源
│ └── images/ # 课程切图(按页面分子目录)
├── components/
│ ├── business/ # 业务组合组件
│ ├── common/ # 通用组件(Button、Overlay、Portal、Toast、ImageButton 等)
│ ├── course-interactions/ # 课程交互(GuideDialog、GuideSpotlight、DragDrop)
│ └── ui/ # 基础 UI 组件
├── course-runtime/ # 固定课程画布运行时(缩放、横屏守护)
├── lib/cn.ts # class 合并工具
├── pages/ # 页面组件
├── router/ # 路由配置
├── stores/ # Zustand 状态管理
├── styles/ # 全局样式
├── types/ # 跨模块共享类型
└── utils/ # 工具函数(assets、motion、yiban、lazyRoute 等)
```
完整开发规范见 [CLAUDE.md](CLAUDE.md)。
## 课程交互能力
项目内置一套课程交互能力,用于支撑探案式学习中的对话、引导、拖拽、弹层等场景。这些源码直接维护在当前仓库,可按需修改。
### 引导聚光灯 SpotlightGuide
适合课程首次学习时突出关键操作区域。在元素上标注 `data-guide`,在页面初始化时启动引导:
```tsx
import { useEffect } from "react";
import { GuideSpotlight } from "@/components/common";
import { useSpotlightStore } from "@/stores";
export default function LessonPage() {
useEffect(() => {
useSpotlightStore.getState().startSpotlight({
courseId: "color-mystery",
spotlightId: "intro",
steps: [
{
id: "start",
target: { dataGuide: "start-button" },
title: "开始调查",
description: "点击这里开始第一幕调查。",
placement: "bottom",
allowTargetClick: true,
},
],
});
}, []);
return (
<>
<button data-guide="start-button">立即立案</button>
<GuideSpotlight />
</>
);
}
```
- `target` 支持 CSS selector 或 `{ dataGuide: "xxx" }`。
- `placement`:`top` / `bottom` / `left` / `right` / `auto`。
- `allowTargetClick`:让高亮区域下方的真实元素可被点击,但不会自动切换步骤,需在业务点击逻辑中手动调用 `nextStep` / `finishSpotlight` / `closeSpotlight`。
- 同一 `courseId + spotlightId` 默认只显示一次(完成或跳过即标记已完成),可用 `resetSpotlight(courseId, spotlightId)` 重新显示。
- 组件内置遮罩淡入、高亮呼吸和提示框入场动画,并兼容系统 `prefers-reduced-motion`。
### 引导对话 GuideDialog
适合在剧情节点弹出侦探助手对话、问题选择等。通过 `defineDialog` 定义、`GuideDialog` 渲染:
```tsx
import { defineDialog, GuideDialog } from "@/components/common";
const dialog = defineDialog({ /* steps, characters, ... */ });
dialog.start();
```
### 拖拽交互 DragDrop
基于 dnd-kit,适合证据分类、调查轨迹排序、选项匹配等交互:
```tsx
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="tartrazine">柠檬黄</Draggable>
<Droppable id="natural-zone">天然来源</Droppable>
</DragDropProvider>
```
由于画布整体使用 `transform: scale(...)` 缩放,拖拽内容在画布内时建议给 `DragDropProvider` 传入当前 `scale`,避免拖拽元素与手指/鼠标移动不同步。
### 动效 GSAP
项目通过 [src/utils/motion.ts](src/utils/motion.ts) 统一封装 GSAP 入口:`gsap` / `useGSAP` 已注册导出,提供统一缓动常量 `EASE_OUT` / `EASE_IN_OUT`,以及 `useReducedMotion()` 响应 `prefers-reduced-motion` 做动画降级。后续课程页面需要 ScrollTrigger 等插件时,在该文件按需 `registerPlugin`。
```ts
import { gsap, useGSAP, EASE_OUT, useReducedMotion } from "@/utils";
```
### 弹层 Overlay / Portal
`Overlay` 已内置 `Portal`,默认挂载到 App 缩放画布内部,适合课程弹窗跟随画布缩放:
```tsx
import { Overlay } from "@/components/common";
<Overlay
open={open}
onClose={() => setOpen(false)}
backgroundImage="url(./images/dialog-bg.png)"
backgroundClassName="bg-cover bg-center"
>
{/* 课程弹层内容 */}
</Overlay>
```
`Portal` 默认挂载到 `document.body`,`Overlay` 默认 `placement="app"`(跟随画布缩放);也可用 `placement="app"` 或 `container` 自定义挂载位置。挂载优先级:`container` > `placement` > 组件默认值。
### Toast 轻提示
`App` 已默认接入 `ToastProvider` 和 `ToastViewport`,页面中可直接使用:
```tsx
import { useToast } from "@/components/common";
const toast = useToast();
toast.success("证据已收入档案");
toast.error("选择有误,请重新核验");
```
支持 `info` / `success` / `warning` / `error` / `show`。
## 易班平台接入
项目面向易班 H5 / 静态资源相对路径部署,Vite `base` 固定为 `"./"`,确保构建产物在未知子路径或压缩包上传场景下仍能引用资源。
### 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),需与构建产物同源、同相对路径部署。若易班平台提供新版本,确认来源、版本和分发权限后再替换,并重新验证用户数据、课程进度、断点和答题记录能力。
### 环境判断
启动时从 URL 读取 `courseId` / `courseid`、`uid`。生产环境中只要存在以上任意参数即视为易班环境。
本地开发即使 URL 带这些参数也默认使用 `sessionStorage`,避免误请求易班接口:
```text
http://localhost:5173/?courseId=123&uid=456
```
本地确需强制走易班接口时追加 `yiban=1`:
```text
http://localhost:5173/?courseId=123&uid=456&yiban=1
```
### 易班能力
```ts
import {
setUserStorage,
getUserStorage,
reportCourseProgress,
getCourseProgress,
saveBreakPoint,
getBreakPoint,
saveUserAnswer,
} from "@/utils/yiban";
import { useUserStore } from "@/stores";
```
- **存取用户数据**:`setUserStorage(lessonId, key, value)` / `getUserStorage(lessonId, key)`,自动根据易班连接状态选择存储方式。
- **课程进度**:`reportCourseProgress(order)` / `getCourseProgress(order)`。
- **播放断点**:`saveBreakPoint({ userId, courseId, lessonId, breakPoint })` / `getBreakPoint(...)`。
- **答题记录**:`saveUserAnswer({ userId, lessonId, courseId, questionOrder, answer })`。
- **用户状态**:`useUserStore()` 提供 `userId`、`userName`、`setUserInfo`、`setYibanConnectionStatus` 等。
### 移动端调试
开发环境自动启用 vconsole,可在移动端查看 Console、Network、Element、Storage。
## 课程画布适配
按 1920×1080 设计稿开发,由 [src/course-runtime/canvas](src/course-runtime/canvas) 的 `CourseCanvas` 组件承担整体缩放,默认 `width=1920`、`height=1080`、`maxScale=1`。
- PC 端:按可视区域等比缩放并居中。
- 大屏:`maxScale` 默认为 1,不超过设计稿原始尺寸,周围留白。
- 移动端:优先横屏访问,竖屏时由 `OrientationGuard` 显示横屏提示。
- 视口优先读取 `visualViewport`,移动端地址栏显示/隐藏时更稳定。
- 页面内容按设计稿像素开发,不使用 rem 适配。
## 路由
路由集中在 [src/router/index.tsx](src/router/index.tsx),使用 Hash 路由 + `lazyRoute` 懒加载,新增页面遵循该模式并配 `HydrateFallback: RouteFallback`:
```tsx
{
path: "about",
lazy: lazyRoute(() => import("../pages/About")),
HydrateFallback: RouteFallback,
}
```
页面组件使用默认导出。
## License
MIT