一、背景
Playedu的前端是基于React18 + TypeScript开发。主要包括:PC学员界面、H5学员界面、后台管理界面三个界面程序。它们都是相同的技术架构,因此只要您懂得其中一个项目的开发,那么另外2个项目可以无缝上手。
二、开发运行快速上手
2.1环境要求
- 操作系统:Window,Mac,Linux均可以
- nodejs ≥ v18
- pnpm ≥ v 8.15.4
2.2 运行系统
下面我们将以后台管理界面为例,给您演示如何将系统跑起来。
2.2.1 安装依赖
首先,进入到后台管理界面程序的根目录,然后执下面命令安装依赖:
pnpm install
2.2.2 配置文件
依赖安装完成之后,执行下面命令创建本地开发配置文件:
cp .env.example .env.local注意,上述命令仅适用于mac或者linux系统,如果您是window系统的话,这一步请手动复制.env.example文件并重命名为.env.local
修改
.env.local中的VITE_APP_URL的值为您的PlayEdu API服务的地址,示例如下:VITE_APP_URL=http://127.0.0.1:9898
2.2.3 程序跑起来
执行下面命令将程序跑起来:
pnpm dev跑起来之后可以看到:
VITE v4.4.9 ready in 515 ms ➜ Local: http://localhost:4000/ ➜ Network: http://192.168.3.14:4000/ ➜ Network: http://192.168.64.1:4000/ ➜ press h to show help
我们在浏览器就可以打开上述地址访问。
三、生产部署快速上手
当我们程序二开完成之后,需要将程序部署更新到生产服务器,在部署之前,我们需要编译程序。下面将是编译的流程。
3.1 生产环境配置文件
执行下面命令创建生产环境配置文件:
cp .env.example .env.production注意,上述命令仅适用于mac或者linux系统,如果您是window系统的话,这一步请手动复制.env.example文件并重命名为.env.production
修改
.env.production中的VITE_APP_URL的值为您的PlayEdu API服务生产环境的地址,示例如下:VITE_APP_URL=https://demo-api.playedu.xyz
3.2 编译程序
执行下面命令编译程序:
pnpm build上述命令执行成功之后,可以看到类似下面的输出信息:
dist/assets/index-c61b50c7.js 100.60 kB │ gzip: 30.06 kB dist/assets/xlsx-4f9172c7.js 424.92 kB │ gzip: 140.79 kB dist/assets/index-e0333ef0.js 1,029.09 kB │ gzip: 341.34 kB dist/assets/index-3c2cf1a6.js 1,347.42 kB │ gzip: 442.01 kB (!) Some chunks are larger than 500 kBs after minification. Consider: - Using dynamic import() to code-split the application - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit. ✓ built in 7.46s ✨ Done in 14.22s.
这就是编译完成了。编译后的程序文件在
dist 目录。到这里,我们就可以将 dist 目录的程序文件部署到生产环境服务器了。四、项目介绍
4.1后台管理界面
4.1.1文件目录结构
playedu ├── public // 公共资源 │ └── js // 公共JS │ └── template // 公共下载、导入模板 │ └── favicon.ico // 网站ICO │ ├── scripts // 脚本 ├── src // 资源 │ └── api // 各页面API │ └── internal // 全局API配置 │ └──... // 各模块API配置 │ └── compenents // 全局组件 │ └── assets // 全局外部资源 │ └── i18n // 国际化配置 │ └── cn.json // 简中语言JSON │ └── config.ts // 国际化配置 │ └── tc.json // 繁中语言JSON │ └── js // 通用JS │ └── pages // 页面资源 │ └── cert // 证书模块 │ └── change-password // 修改密码模块 │ └── course // 线上课模块 │ └── dashboard // 主页模块 │ └── department // 部门模块 │ └── error // 错误模块 │ └── exam // 考试模块 │ └── papers // 试卷模块 │ └── questions // 试题模块 │ └── task // 考试任务模块 │ └── init // 初始化 │ └── layouts // 页面布局 │ └── loading // 公用loading模块 │ └── login // 登录模块 │ └── member // 学员模块 │ └── paper-show // 试卷阅览模块 │ └── resource // 资源模块 │ └── attachment // 课件模块 │ └── audio // 音频模块 │ └── courseware // 文档模块 │ └── images // 图片模块 │ └── resource-category // 分类模块 │ └── videos // 视频模块 │ └── study // 学习任务模块 │ └── system // 系统模块 │ └── administrator // 管理人员模块 │ └── adminlog // 管理日志模块 │ └── adminroles // 管理角色模块 │ └── config // 系统配置模块 │ └── teacher // 讲师模块 │ └── test // 测试页面(仅用于测试环境) │ └── routes // 路由 │ └── store // 中间件及store │ └── system // 系统store │ └── user // 用户store │ └── index.ts // store配置 │ └── types // 全局静态类型 │ └── utils // 工具类 │ └── playedu.d.ts // 全局类型 │ └── AutoTop.ts // 全局页面自动滚动到最上方 │ └── custom.d.ts // 全局css、less、scss配置 │ └── index.less // 全局样式 │ └── main.tsx // 主要模块 │ └── App.module.less // 系统样式 │ └── App.tsx // 系统模块 ├── .env.example // Env配置模版 ├── .env.local // Env配置 ├── .env(可用) // Env配置 ├── vite.config.ts // VITE配置 ├── ...
4.1.2 依赖库
| 依赖库 | 地址 | 说明 |
antd | https://ant.design | UI框架 |
axios | https://axios-http.com/docs/intro | HTTP请求库 |
echarts | https://echarts.apache.org/zh/index.html | 图表渲染库 |
i18next | https://www.i18next.com | 国际化 |
moment | https://momentjs.cn | 时间解析库 |
ahooks | https://ahooks.js.org/zh-CN/ | React Hooks库 |
localforage | https://localforage.docschina.org | LocalStorage的封装 |
match-sorter | https://github.com/kentcdodds/match-sorter | 数组处理库 |
react-router-dom | https://reactrouter.com/en/main | 路由库 |
redux | https://cn.redux.js.org | 全局状态 |
sort-by | https://github.com/kvnneff/sort-by | 数组加强库 |
xlsx | https://github.com/SheetJS/sheetjs | Excel操作 |
4.1.3 页面架构图

4.1.4 全局状态
4.1.4.1 全局状态一、登录用户
- 路径
src/store/user/loginUserSlice.ts- 存储变量结构
type UserInterface = { id: number; name: string; email: string; }; type UserStoreInterface = { user: UserInterface | null; isLogin: boolean; permissions: string[]; uploadStatus: boolean; uploadCateIds: number[]; };
- demo一、读取信息
import { useSelector } from "react-redux"; // 读取登录用户信息 const user = useSelector((state: any) => state.loginUser.value.user); // 读取登录用户的关联权限 const user = useSelector((state: any) => state.loginUser.value.permissions);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { loginAction } from "./store/user/loginUserSlice"; const dispatch = useDispatch(); // 获取用户数据 let res: any = await loginApi.getUser(); // 将用户数据存储到全局状态 dispatch(loginAction(res.data));
4.1.4.2 全局状态二、全局配置
- 路径
src/store/system/systemConfigSlice.ts- 存储变量结构
type SystemConfigStoreInterface = { synchEnabled?: boolean; synchScene?: number; systemLanguage?: string; systemApiUrl?: string; systemPcUrl?: string; systemH5Url?: string; systemLogo?: string; systemName?: string; memberDefaultAvatar?: string; resourceUrl?: { [key: number]: string; }; };
- demo一、读取信息
import { useSelector } from "react-redux"; const systemConfig = useSelector((state: any) => state.systemConfig.value);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { SystemConfigStoreInterface, saveConfigAction, } from "../../store/system/systemConfigSlice"; let config: SystemConfigStoreInterface = {...}; dispatch(saveConfigAction(config));
4.1.5 后台权限概述
4.1.5.1按钮权限组件
文件:
src/components/permission-button使用权限按钮组件(demo)
import { PerButton } from "@/components/permission-button"; import { PlusOutlined } from "@ant-design/icons"; return ( <> <PerButton type="primary" text={"测试按钮"} class="demo-button" icon={<PlusOutlined />}//或者null p="upload-word" //该按钮权限 onClick={() => void()} disabled={true|false}//或者null /> </> )
4.1.5.2 左侧菜单组件
文件:
src/components/left-menuimport React, { useEffect, useState } from "react"; import { Menu } from "antd"; import { useSelector } from "react-redux"; import { useNavigate, useLocation } from "react-router-dom"; export const LeftMenu: React.FC = () => { const location = useLocation(); // 全局权限 const permissions = useSelector( (state: any) => state.loginUser.value.permissions ); // 选中的菜单 const [selectedKeys, setSelectedKeys] = useState<string[]>([ location.pathname, ]); // 展开菜单 const hit = (pathname: string): string[] => { for (let p in children2Parent) { if (pathname.search(p) >= 0) { return children2Parent[p]; } } return []; }; const [openKeys, setOpenKeys] = useState<string[]>(hit(location.pathname)); const items = [ getItem( "主页", "/", <i className={`iconfont icon-icon-home`} />, null, null, "menu-index" //该菜单权限 ), getItem( "分类", "/resource-category", <i className="iconfont icon-icon-category" />, null, null, "menu-category" //该菜单权限 ) // ... ] // ...省略代码 return ( <div className={styles["menu-box"]}> <Menu onClick={onClick} style={{ width: 200, background: "#ffffff", }} selectedKeys={selectedKeys} openKeys={openKeys} mode="inline" items={activeMenus} onSelect={(data: any) => { setSelectedKeys(data.selectedKeys); }} onOpenChange={(keys: any) => { setOpenKeys(keys); }} /> </div> ) }
4.2 PC学员界面
4.2.1文件目录结构
playedu ├── public // 公共资源 │ └── js // 公共JS │ └── favicon.ico // 网站ICO │ ├── src // 资源 │ └── api // 各页面API │ └── internal // 全局API配置 │ └──... // 各模块API配置 │ └── compenents // 全局组件 │ └── assets // 全局外部资源 │ └── i18n // 国际化配置 │ └── cn.json // 简中语言JSON │ └── config.ts // 国际化配置 │ └── tc.json // 繁中语言JSON │ └── js // 通用JS │ └── pages // 页面资源 │ └── cert // 证书模块 │ └── course // 线上课模块 │ └── index // 首页模块 │ └── error // 错误模块 │ └── init // 初始化 │ └── layouts // 页面布局 │ └── loading // 公用loading模块 │ └── login // 登录模块 │ └── task // 任务模块 │ └── index.tsx // 学习任务模块 │ └── exam.tsx // 考试任务模块 │ └── ... // 任务相关模块 │ └── latest-learn // 最近学习模块 │ └── routes // 路由 │ └── store // 中间件及store │ └── system // 系统store │ └── user // 用户store │ └── index.ts // store配置 │ └── utils // 工具类 │ └── global.d.ts // 全局变量 │ └── playedu.d.ts // 全局类型 │ └── AutoTop.ts // 全局页面自动滚动到最上方 │ └── custom.d.ts // 全局css、less、scss配置 │ └── index.scss // 全局样式 │ └── main.tsx // 主要模块 │ └── App.scss // 系统样式 │ └── App.tsx // 系统模块 ├── .env.example // Env配置模版 ├── .env.local // Env配置 ├── .env(可用) // Env配置 ├── vite.config.ts // VITE配置 ├── ...
4.2.2 依赖库
| 依赖库 | 地址 | 说明 |
antd | https://ant.design | UI框架 |
axios | https://axios-http.com/docs/intro | HTTP请求库 |
jspdf | https://github.com/parallax/jsPDF | 生成PDF文档 |
html2canvas | https://html2canvas.hertzen.com/ | html内容写入canvas生成图片 |
lodash | https://www.lodashjs.com/ | Lodash库 |
i18next | https://www.i18next.com | 国际化 |
moment | https://momentjs.cn | 时间解析库 |
localforage | https://localforage.docschina.org | LocalStorage的封装 |
match-sorter | https://github.com/kentcdodds/match-sorter | 数组处理库 |
react-router-dom | https://reactrouter.com/en/main | 路由库 |
redux | https://cn.redux.js.org | 全局状态 |
sort-by | https://github.com/kvnneff/sort-by | 数组加强库 |
react-ga | https://github.com/react-ga/react-ga | 反应谷歌分析模块 |
4.2.3 页面架构图

4.2.4 全局状态
4.2.4.1 全局状态一、登录用户
- 路径
src/store/user/loginUserSlice.ts- 存储变量结构
type ResourceUrlModel = { [key: number]: string; }; type UserModel = { id: number; name: string; avatar: string; credit1: number; email: string; create_city: string; create_ip: string; id_card: string; is_active: number; is_lock: number; from_scene: number; is_set_password: number; is_verify: number; created_at: string; updated_at: string; login_at?: string; verify_at?: string; }; type UserStoreInterface = { user: UserModel | null; departments: string[]; currentDepId: number; resourceUrl: ResourceUrlModel; isLogin: boolean; };
- demo一、读取信息
import { useSelector } from "react-redux"; // 读取登录用户信息 const user = useSelector((state: any) => state.loginUser.value.user); // 读取登录用户的部门 const departments = useSelector((state: any) => state.loginUser.value.departments); // 读取登录用户的资源地址 const resourceUrl = useSelector((state: any) => state.loginUser.value.resourceUrl); // 读取登录用户的登录状态 const isLogin = useSelector((state: any) => state.loginUser.value.isLogin); // 读取登录用户的默认当前部门ID const currentDepId = useSelector((state: any) => state.loginUser.value.currentDepId);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { loginAction } from "./store/user/loginUserSlice"; const dispatch = useDispatch(); // 获取用户数据 let userRes: any = await user.detail(); // 将用户数据存储到全局状态 dispatch(loginAction(userRes));
4.2.4.2 全局状态二、全局配置
- 路径
src/store/system/systemConfigSlice.ts- 存储变量结构
type SystemConfigStoreInterface = { synchEnabled: boolean; synchScene: number; synchWorkAgentId: string; synchWorkCorpId: string; synchWorkCorpSecret: string; synchWorkUrl: string; systemApiUrl: string; systemLanguage?: string; systemPcUrl: string; systemH5Url: string; systemLogo: string; systemName: string; pcIndexFooterMsg: string; playerPoster: string; playerIsEnabledBulletSecret: boolean; playerIsDisabledDrag: boolean; playerBulletSecretText: string; playerBulletSecretColor: string; playerBulletSecretOpacity: string; resourceUrl?: { [key: number]: string; }; };
- demo一、读取信息
import { useSelector } from "react-redux"; const systemConfig = useSelector((state: any) => state.systemConfig.value);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { SystemConfigStoreInterface, saveConfigAction, } from "../../store/system/systemConfigSlice"; let configRes: any = await system.config(); let config: SystemConfigStoreInterface = { //系统配置 synchEnabled: configRes["synch.enabled"], synchScene: configRes["synch.scene"], synchWorkAgentId: configRes["synch.agentid"], synchWorkCorpId: configRes["synch.corpid"], synchWorkCorpSecret: configRes["synch.corpsecret"], synchWorkUrl: configRes["synch.url"], systemApiUrl: configRes["system-api-url"], systemLanguage: configRes["system.language"], systemH5Url: configRes["system-h5-url"], systemLogo: configRes["resource_url"][ Number(configRes["system-logo"]) ] ? configRe["resource_url"][ Number(configRes["system-logo"]) ] : "", systemName: configRes["system-name"], systemPcUrl: configRes["system-pc-url"], pcIndexFooterMsg: configRes["system-pc-index-footer-msg"], //播放器配置 playerPoster: configRes["resource_url"][ Number(configRes["player-poster"]) ] ? configRes["resource_url"][ Number(configRes["player-poster"]) ] : "", playerIsEnabledBulletSecret: configRes["player-is-enabled-bullet-secret"] && configRes["player-is-enabled-bullet-secret"] === "1" ? true : false, playerIsDisabledDrag: configRes["player-disabled-drag"] && configRes["player-disabled-drag"] === "1" ? true : false, playerBulletSecretText: configRes["player-bullet-secret-text"], playerBulletSecretColor: configRes["player-bullet-secret-color"], playerBulletSecretOpacity: configRes["player-bullet-secret-opacity"], resourceUrl: configRes["resource_url"], }; i18n.changeLanguage(configRes["system.language"]); // 将系统数据存储到全局状态 dispatch(saveConfigAction(config));
4.3 H5学员界面
4.3.1文件目录结构
playedu ├── public // 公共资源 │ └── js // 公共JS │ └── favicon.ico // 网站ICO │ ├── src // 资源 │ └── api // 各页面API │ └── internal // 全局API配置 │ └──... // 各模块API配置 │ └── compenents // 全局组件 │ └── assets // 全局外部资源 │ └── i18n // 国际化配置 │ └── cn.json // 简中语言JSON │ └── config.ts // 国际化配置 │ └── tc.json // 繁中语言JSON │ └── js // 通用JS │ └── pages // 页面资源 │ └── cert // 证书模块 │ └── change-department // 切换部门模块 │ └── change-password // 修改密码模块 │ └── course // 线上课模块 │ └── index // 首页模块 │ └── error // 错误模块 │ └── init // 初始化 │ └── layouts // 页面布局 │ └── loading // 公用loading模块 │ └── login // 登录模块 │ └── member // 学员中心模块 │ └── task // 任务模块 │ └── index.tsx // 学习任务模块 │ └── exam.tsx // 考试任务模块 │ └── ... // 任务相关模块 │ └── study // 最近学习模块 │ └── upload // PC扫码、H5的考试上传模块 │ └── routes // 路由 │ └── store // 中间件及store │ └── system // 系统store │ └── user // 用户store │ └── index.ts // store配置 │ └── utils // 工具类 │ └── global.d.ts // 全局变量 │ └── playedu.d.ts // 全局类型 │ └── AutoTop.ts // 全局页面自动滚动到最上方 │ └── custom.d.ts // 全局css、less、scss配置 │ └── main.scss // 全局样式 │ └── main.tsx // 主要模块 │ └── App.scss // 系统样式 │ └── App.tsx // 系统模块 ├── .env.example // Env配置模版 ├── .env.local // Env配置 ├── .env(可用) // Env配置 ├── vite.config.ts // VITE配置 ├── ...
4.3.2 依赖库
| 依赖库 | 地址 | 说明 |
antd-mobile | https://ant-design-mobile.antgroup.com/zh | UI框架 |
axios | https://axios-http.com/docs/intro | HTTP请求库 |
jspdf | https://github.com/parallax/jsPDF | 生成PDF文档 |
html2canvas | https://html2canvas.hertzen.com/ | html内容写入canvas生成图片 |
@types/lodash | https://www.lodashjs.com/ | Lodash库 |
i18next | https://www.i18next.com | 国际化 |
moment | https://momentjs.cn | 时间解析库 |
xgplayer | https://h5player.bytedance.com | 播放器 |
xgplayer-hls | - | 播放器HLS库 |
localforage | https://localforage.docschina.org | LocalStorage的封装 |
match-sorter | https://github.com/developit/mitt | 数组处理库 |
mitt | https://github.com/kentcdodds/match-sorter | Mitt事件库 |
react-router-dom | https://reactrouter.com/en/main | 路由库 |
redux | https://cn.redux.js.org | 全局状态 |
sort-by | https://github.com/kvnneff/sort-by | 数组加强库 |
react-ga | https://github.com/react-ga/react-ga | 反应谷歌分析模块 |
4.3.3 页面架构图

4.3.4 全局状态
4.3.4.1 全局状态一、登录用户
- 路径
src/store/user/loginUserSlice.ts- 存储变量结构
type ResourceUrlModel = { [key: number]: string; }; type UserModel = { id: number; name: string; avatar: string; credit1: number; email: string; create_city: string; create_ip: string; id_card: string; is_active: number; is_lock: number; from_scene: number; is_set_password: number; is_verify: number; created_at: string; updated_at: string; login_at?: string; verify_at?: string; }; type UserStoreInterface = { user: UserModel | null; departments: string[]; currentDepId: number; resourceUrl: ResourceUrlModel; isLogin: boolean; };
- demo一、读取信息
import { useSelector } from "react-redux"; // 读取登录用户信息 const user = useSelector((state: any) => state.loginUser.value.user); // 读取登录用户的部门 const departments = useSelector((state: any) => state.loginUser.value.departments); // 读取登录用户的资源地址 const resourceUrl = useSelector((state: any) => state.loginUser.value.resourceUrl); // 读取登录用户的登录状态 const isLogin = useSelector((state: any) => state.loginUser.value.isLogin); // 读取登录用户的默认当前部门ID const currentDepId = useSelector((state: any) => state.loginUser.value.currentDepId);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { loginAction } from "./store/user/loginUserSlice"; const dispatch = useDispatch(); // 获取用户数据 let userRes: any = await user.detail(); // 将用户数据存储到全局状态 dispatch(loginAction(userRes));
4.3.4.2 全局状态二、全局配置
- 路径
src/store/system/systemConfigSlice.ts- 存储变量结构
type SystemConfigStoreInterface = { synchEnabled: boolean; synchScene: number; synchWorkAgentId: string; synchWorkCorpId: string; synchWorkCorpSecret: string; synchWorkUrl: string; systemApiUrl: string; systemLanguage?: string; systemPcUrl: string; systemH5Url: string; systemLogo: string; systemName: string; pcIndexFooterMsg: string; playerPoster: string; playerIsEnabledBulletSecret: boolean; playerIsDisabledDrag: boolean; playerBulletSecretText: string; playerBulletSecretColor: string; playerBulletSecretOpacity: string; resourceUrl?: { [key: number]: string; }; };
- demo一、读取信息
import { useSelector } from "react-redux"; const systemConfig = useSelector((state: any) => state.systemConfig.value);
- demo二、存储信息
import { useDispatch } from "react-redux"; import { SystemConfigStoreInterface, saveConfigAction, } from "../../store/system/systemConfigSlice"; let configRes: any = await system.config(); let config: SystemConfigStoreInterface = { //系统配置 synchEnabled: configRes["synch.enabled"], synchScene: configRes["synch.scene"], synchWorkAgentId: configRes["synch.agentid"], synchWorkCorpId: configRes["synch.corpid"], synchWorkCorpSecret: configRes["synch.corpsecret"], synchWorkUrl: configRes["synch.url"], systemApiUrl: configRes["system-api-url"], systemLanguage: configRes["system.language"], systemH5Url: configRes["system-h5-url"], systemLogo: configRes["resource_url"][ Number(configRes["system-logo"]) ] ? configRe["resource_url"][ Number(configRes["system-logo"]) ] : "", systemName: configRes["system-name"], systemPcUrl: configRes["system-pc-url"], pcIndexFooterMsg: configRes["system-pc-index-footer-msg"], //播放器配置 playerPoster: configRes["resource_url"][ Number(configRes["player-poster"]) ] ? configRes["resource_url"][ Number(configRes["player-poster"]) ] : "", playerIsEnabledBulletSecret: configRes["player-is-enabled-bullet-secret"] && configRes["player-is-enabled-bullet-secret"] === "1" ? true : false, playerIsDisabledDrag: configRes["player-disabled-drag"] && configRes["player-disabled-drag"] === "1" ? true : false, playerBulletSecretText: configRes["player-bullet-secret-text"], playerBulletSecretColor: configRes["player-bullet-secret-color"], playerBulletSecretOpacity: configRes["player-bullet-secret-opacity"], resourceUrl: configRes["resource_url"], }; i18n.changeLanguage(configRes["system.language"]); // 将系统数据存储到全局状态 dispatch(saveConfigAction(config));