观前提示:
😀文章的内容大都为我学习过程中的笔记记录,不一定代表权威的做法,如果您有更好的想法,欢迎评论留言。
👍👍完整的项目实战经历,文章非常非常非常非常长,不过相信你完整的看完后会有所收获。
🧨🧨🧨项目分为三端
① Web服务器 ② 客户端 ③ 后台管理系统
这里主要做了后台管理系统和服务器
🎈🎈🎈🎈为了练习SQL语句,使用的命令创建表、字段等,也可以使用navicat图形化界面创建。
💎💎💎💎💎基于服务器开发 && 前后端分离开发的区别。
基于服务端开发和前后端分离开发区别
前后端分离的项目中,后端Web服务器的作用是:给客户端提供接口,和数据库进行连接。
🐮🐮🐮🐮🐮🐮Web开发其实整体过程并不是很难,主要就是以下几步:
① 前端界面搭建
② 后端服务器搭建
③ 前后交互,接口先行。先把后端接口准备好。
④ 前端调用接口,进行界面渲染即可。
(个人感觉..)
🐾🐾🐾🐾🐾🐾🐾完整代码:关注一下公众号「代码行间」,回复「后台管理」可以获取代码~
一、数据库表设计
Ⅰ、安装MySql
参考博客MySQL8.0.22安装教程
Ⅱ、新建数据库
㈠、使用Navicat连接本地MySQL
㈡、新建数据库
Ⅲ、建立数据表(数据表设计)
㈠ 、幼教资源
1、分类数据表
-
对应内容
-
建表SQL
DROP TABLE IF EXISTS t_resource_category; CREATE TABLE t_resource_category ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', category_name VARCHAR ( 255 ) NOT NULL COMMENT '分类的名称' ) COMMENT '幼教资源分类数据表'
-
暂时添加默认分类到数据表中
insert into t_resource_category(category_name) values ('教学活动小助手'),('亲自小学堂'),('培训教室'),('GT课程')
-
效果
2、班级数据表
-
对应内容
-
建表语句
DROP TABLE IF EXISTS t_resource_classes; CREATE TABLE t_resource_classes ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', classes_name VARCHAR ( 255 ) NOT NULL COMMENT '班级的名称' ) COMMENT '幼教资源班级数据表'
-
暂时添加默认班级到数据表中
insert into t_resource_classes(classes_name) values ('托班'),('小班'),('中班'),('大班')
-
效果
3、领域数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_resource_area; CREATE TABLE t_resource_area ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', area_name VARCHAR ( 255 ) NOT NULL COMMENT '领域的名称' ) COMMENT '幼教资源领域数据表'
- 暂时添加默认领域到数据表中
insert into t_resource_area(area_name) values ('健康'),('语言'),('社会'),('科学'),('艺术')
- 效果
4、素材数据表
-
对应内容
-
建表语句
DROP TABLE IF EXISTS t_resource_material; CREATE TABLE t_resource_material ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', material_name VARCHAR ( 255 ) NOT NULL COMMENT '素材的名称' ) COMMENT '幼教资源素材数据表'
-
暂时添加默认素材到数据表中
insert into t_resource_material(material_name ) values ('教学设计'),('教学课件')
-
效果
5、格式数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_resource_format; CREATE TABLE t_resource_format( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', format_name VARCHAR(255) NOT NULL COMMENT '格式的名称' )COMMENT '幼教资源格式数据表'
- 暂时添加默认格式到数据表中
insert t_resource_format(format_name) values ('图片'),('文档'),('音频'),('视频'),('课件'),('综合多媒体')
- 效果
6、幼教资源主表
- 建表语句
DROP TABLE IF EXISTS t_resource; CREATE TABLE t_resource( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', resource_name VARCHAR ( 255 ) NOT NULL COMMENT '资源名称', resource_auther VARCHAR ( 255 ) NOT NULL COMMENT '资源作者', resource_views INT ( 11 ) COMMENT '资源观看数量', resource_publish_time VARCHAR ( 255 ) COMMENT '资源创建时间', resource_content VARCHAR ( 255 ) COMMENT '资源内容', resource_category_id INT NOT NULL COMMENT '资源所属分类ID', resource_clases_id INT NOT NULL COMMENT '资源所属班级ID', resource_area_id INT NOT NULL COMMENT '资源所属领域ID', resource_meta_id INT NOT NULL COMMENT '资源所属素材ID', resource_format_id INT NOT NULL COMMENT '资源所属格式ID', resource_image VARCHAR ( 255 ) COMMENT '资源封面图片', resource_price VARCHAR ( 255 ) COMMENT '资源价格', is_focus INT ( 1 ) COMMENT '是否上轮播图,0:不上 1:上', focus_img VARCHAR ( 255 ) COMMENT '轮播图图片' ) COMMENT '幼教资源数据表'
- 效果
㈡、职场人生
1、学前教育培训数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_job_pre; CREATE TABLE t_job_pre ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', pre_edu_name VARCHAR ( 255 ) NOT NULL COMMENT '学前教师培训名称' ) COMMENT '职场人生学前教育培训数据表'
- 暂时添加默认培训内容到数据表中
insert into t_job_pre(pre_edu_name) values ('学前教育发展最新趋势'),('学前教育基础理论'),('教育活动设计主要策略')
- 效果
2、家园共育培训数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_job_family; CREATE TABLE t_job_family ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', job_family_name VARCHAR ( 255 ) NOT NULL COMMENT '家园共育培训名称' ) COMMENT '职场人生家园共育培训数据表'
- 暂时添加默认数据到数据表中
insert into t_job_family(job_family_name) values ('幼小衔接'),('亲子活动案例')
- 效果
3、职场人生主表
- 建表语句
DROP TABLE IF EXISTS t_job; CREATE TABLE t_job ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', job_name VARCHAR ( 255 ) NOT NULL COMMENT '标题', job_img VARCHAR ( 255 ) COMMENT '封面', job_author VARCHAR ( 255 ) COMMENT '作者', job_views int ( 12 ) COMMENT '观看数量', job_publish_time VARCHAR ( 255 ) COMMENT '发布时间', job_content VARCHAR ( 255 ) COMMENT '主要内容', job_pre_edu_id INT COMMENT '所属学前教育培训ID', job_family_edu_id INT COMMENT '所属家园共育培训ID', is_focus INT DEFAULT(0) COMMENT '是否上轮播图,0:不上 1:上', focus_img VARCHAR(255) COMMENT '轮播图图片' ) COMMENT '职场人生数据表'
- 效果
㈢、活动专区
1、活动地点数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_activities_address; CREATE TABLE t_activities_address ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', activities_address VARCHAR ( 255 ) NOT NULL COMMENT '活动地点名称' ) COMMENT '活动专区活动地点数据表'
- 暂时添加默认活动地点到数据表中
insert into t_activities_address(activities_address) values ('北京'),('上海'),('南京'),('杭州'),('深圳')
- 效果
2、招生对象数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_activities_object; CREATE TABLE t_activities_object ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', activities_object_name VARCHAR ( 255 ) NOT NULL COMMENT '招生对象名称' ) COMMENT '活动专区招生对象数据表'
- 暂时添加默认招生对象到数据表中
insert into t_activities_object(activities_object_name) values ('托班(2-3岁)'),('小班(3-4岁)'),('中班(4-5岁)'),('大班(5-6岁)')
- 效果
3、营期数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_activities_bus; CREATE TABLE t_activities_bus ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', activities_bus_day VARCHAR ( 255 ) NOT NULL COMMENT '营期名称' ) COMMENT '活动专区营期数据表'
- 暂时添加默认营期到数据表中
insert into t_activities_bus(activities_bus_day) values ('1天'),('2天'),('3天'),('4天'),('5天'),('6天'),('7天')
- 效果
4、 活动标签数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_activities_tag; CREATE TABLE t_activities_tag ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', activities_tag_name VARCHAR ( 255 ) NOT NULL COMMENT '活动标签名称' ) COMMENT '活动专区活动标签数据表'
- 暂时添加默认活动标签
insert into t_activities_tag(activities_tag_name) values ('体验军旅'),('军事拓展'),('实弹射击')
- 效果
5、活动专区主表
- 建表语句
DROP TABLE IF EXISTS t_activities; CREATE TABLE t_activities ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', activities_name VARCHAR ( 255 ) NOT NULL COMMENT '标题', activities_time VARCHAR ( 255 ) NOT NULL COMMENT '发布时间', activities_img VARCHAR ( 255 ) COMMENT '封面', activities_price FLOAT ( 10, 2 ) COMMENT '价格', activities_tag FLOAT ( 10, 2 ) COMMENT '对应活动标签', activities_address_id INT COMMENT '对应活动地点ID', activities_object_id INT COMMENT '对应活动对象ID', activities_bus_day_id INT COMMENT '对应活动天数ID', activities_intro TEXT COMMENT '课程介绍', activities_trip TEXT COMMENT '行程安排', activities_days TEXT COMMENT '开营日期', activities_notice TEXT COMMENT '报名须知', is_focus INT ( 1 ) DEFAULT ( 0 ) COMMENT '是否上轮播图,0:不上 1:上', focus_img VARCHAR ( 255 ) COMMENT '轮播图图片' ) COMMENT '活动专区数据表'
- 效果
㈣、直播课堂
1、适用人群数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_live_person; CREATE TABLE t_live_person ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', live_person_name VARCHAR ( 255 ) NOT NULL COMMENT '适用人群名称' ) COMMENT '直播课堂适用人群数据表'
- 暂时添加默认适用人群到数据表中
insert into t_live_person(live_person_name) values ('园长'),('教师'),('家长')
- 效果
2、内容主题数据表
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_live_theme; CREATE TABLE t_live_theme ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', live_theme_title VARCHAR ( 255 ) NOT NULL COMMENT '内容主题名称' ) COMMENT '直播课堂内容主题数据表'
- 暂时添加默认内容主题到数据表中
insert into t_live_theme(live_theme_title) values ('园所管理'),('园所理念'),('园所发展')
- 效果
3、直播课堂总表
- 建表语句
DROP TABLE IF EXISTS t_live; CREATE TABLE t_live ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', live_title VARCHAR ( 255 ) NOT NULL COMMENT '标题', live_begin_time VARCHAR ( 255 ) NOT NULL COMMENT '起始时间', live_end_time VARCHAR ( 255 ) NOT NULL COMMENT '结束时间', live_img VARCHAR ( 255 ) COMMENT '封面', live_author VARCHAR ( 255 ) COMMENT '作者', live_price VARCHAR ( 255 ) COMMENT '价格', live_url VARCHAR ( 255 ) COMMENT '链接', live_person_id VARCHAR ( 255 ) COMMENT '对应适用人群ID', live_theme_id VARCHAR ( 255 ) COMMENT '对应内容主题ID', is_focus INT ( 1 ) DEFAULT ( 0 ) COMMENT '是否上轮播图,0:不上 1:上', focus_img VARCHAR ( 255 ) COMMENT '轮播图图片' ) COMMENT '直播课堂数据表'
- 效果
㈤、个人中心
1、用户信息数据表
-
对应内容:对应每个用户自己的数据模型
-
建表语句
DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_name VARCHAR(255) NOT NULL COMMENT '用户名称', user_password VARCHAR(255) COMMENT '密码', user_phone VARCHAR(255) COMMENT '手机', user_count_money FLOAT(10,2) COMMENT '学习币', user_intro VARCHAR(255) COMMENT '介绍', user_icon VARCHAR(255) COMMENT '头像' ) COMMENT '用户信息数据表'
-
效果
2、我的账户
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_user_account; CREATE TABLE t_user_account ( id INT ( 11 ) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_id INT ( 11 ) NOT NULL COMMENT '与用户关联', account_change_time VARCHAR ( 255 ) NOT NULL COMMENT '账户金额变动时间', account_change_money FLOAT ( 10, 2 ) NOT NULL COMMENT '账户金额变动金额', account_change_methods INT ( 1 ) DEFAULT ( 0 ) NOT NULL COMMENT '账户金额变动类型 默认为0 0:充值入账 1:支出出账' ) COMMENT '个人中心我的账户数据表'
- 效果
3、我的收藏
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_user_fav; CREATE TABLE t_user_fav ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_id INT NOT NULL COMMENT '与用户关联', resource_id INT COMMENT '与收藏资源关联', activities_id INT COMMENT '与收藏活动关联', live_id INT COMMENT '与收藏直播关联' ) COMMENT '个人中心我的收藏数据表'
- 效果
4、我的直播
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_user_live; CREATE TABLE t_user_live ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_id INT NOT NULL COMMENT '与用户关联', live_id INT COMMENT '与直播关联' ) COMMENT '个人中心我的直播数据表'
- 效果
5、我的活动
- 对应内容
- 建表语句
DROP TABLE IF EXISTS t_user_activities; CREATE TABLE t_user_activities ( id INT ( 11 ) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_id INT ( 11 ) NOT NULL COMMENT '与用户关联', activities_id INT ( 11 ) NOT NULL COMMENT '与活动关联' ) COMMENT '个人中心我的活动数据表'
- 效果
6、我的资源
-
对应内容
-
建表语句
DROP TABLE IF EXISTS t_user_resource; CREATE TABLE t_user_resource ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', user_id INT NOT NULL COMMENT '与用户关联', resource_id INT NOT NULL COMMENT '与资源关联' ) COMMENT '个人中心我的资源数据表'
-
效果
㈥、管理员数据表
通过Navicat图形化界面直接建立管理员数据表
二、Web服务器搭建
Ⅰ、使用Git管理项目代码
Git基础学习: Git学习网址
㈠、使用GitHub新建一个仓库
- 打开
GitHub
新建一个仓库 - 编辑仓库基本信息并新建
- 复制仓库地址
㈡、将仓库克隆到本地
- 打开项目文件夹
- 右键选择
Git Bash Here
- 使用
git clone 仓库地址
命令克隆仓库 hk_server_api
仓库成功克隆到本地
Ⅱ、初始化Web项目
㈠、通过express创建一个Node web项目
-
运行命令
express --view=ejs hkserver
-
进入文件夹,并安装所有依赖
-
将所有文件拷贝到仓库中
-
删除hkserver
㈡、将文件同步到GitHub仓库
1、创建ignore忽略文件
- 通过命令
touch .gitignore
创建.gitignore文件 - 在.gitignore文件夹下,将node_modules文件夹忽略
2、上传文件
- 通过
git add .
命令将所有文件上传到暂存区 - 通过
git commit -m '信息'
命令将暂存区内容添加到本地仓库中 - 通过
git push
命令将本地的分支版本上传到远程并合并
3、查看GitHub仓库
通过上面步骤,代码就已经被同步到GitHub上了
㈢、修改Web服务器默认端口
将bin目录下的www.js文件夹内做以下修改,将端口号改为5000,防止和React端口冲突
㈣、根据MVC模式修改Web服务器项目目录
- 可以将routes中内容看作model,将views看作View,新建controller存放控制器业务。
- 新建config文件夹,存放各种配置
- 新建middleWare文件夹,存放各种中间件
㈤、整理app.js
修改var为const、增加注释等
㈥、配置nodemon
1. 介绍
nodemon是一种工具,可以自动检测到目录中的文件更改时通过重新启动应用程序来调试基于node.js的应用程序。
在开发环境下,往往需要一个工具来自动重启项目工程。
2. 配置
-
安装
npm install -g nodemon
-
配置开发依赖
3、使用
配置完成后,就可以使用npm run dev
就可以基于nodemon运行项目了
Ⅳ、保持版本迭代,推送代码
打版本号 v1.0
git add .
git commit -m '完成1.0版本开发'
git push
git tag -a v1.0 -m '标记1.0版本'
git push origin v1.0
Ⅲ、Web服务器express连接MySQL数据库
㈠、安装mysql
- 命令
npm install mysql
- 网址
㈡、统一创建数据库配置文件
- 创建
config/mysqlConfig.js
文件,并在文件夹内写入以下代码const config = { // 数据库信息配置 dataBase: { HOST: 'localhost', // 主机 PORT: '3306', // 端口 USER: 'root', // MySQL认证的用户名 PASSWORD: '131415', // MySQL认证的密码 DATABASE: "kaisar's edu" // 数据库名称 } // 其他配置 ... } module.exports = config;
- 相关参数配置说明
㈢、统一创建数据库查询文件
- 创建文件
config/dbHelper.js
,并分别执行以下步骤
① 引入mysql和配置文件
② 创建数据库连接池
③ 创建通用方法, 通过promise方式返回
④ 导出模块 - 具体代码
// 1. 引入mySQL const mySQL = require('mysql'); const dbConfig = require('./config').dataBase; // 2. 创建数据库连接池 const pool = mySQL.createPool({ host: dbConfig.HOST, port: dbConfig.PORT, user: dbConfig.USER, password: dbConfig.PASSWORD, database: dbConfig.DATABASE }); // 3. 创建通用查询方法,可以通过promise返回 let Query = (sql, value) => { return new Promise((resolve, reject) => { // 3.1 建立连接查询 pool.getConnection((error, connection) => { // 3.2 连接失败 if (error) { reject({ code: 0, data: error }); } // 3.3 连接成功,通过连接查询数据库 connection.query(sql, value, (error, results, fields) => { // 3.4 关闭连接 connection.release(); // 3.5 SQL语句执行失败 if (error) { reject({code: 0, data: error, msg: 'SQL语句执行失败'}); } // 3.6 返回SQL语句操作完成结果 resolve({code: 1, data: results, msg: 'SQL语句执行成功!'}); }) }) }); }; module.exports = Query;
㈣、调用方法插入数据到数据库
-
先在可视化工具中调试SQL语句
INSERT INTO t_admin(account, password, account_name) VALUES ('admin', 'admin', '小撩宝宝');
-
路由中进行错误和正确调试
这里不直接传递SQL语句,而是通过?代替参数的位置,将参数放置在数组中进行传递,防止SQL注入let sql = `INSERT INTO t_admin(account, password, account_name) VALUES (?, ?, ?);`; let value = ['admin', 'admin', '小撩宝宝']; Query(sql, value).then((result)=>{ console.log(result); }).catch((error)=>{ console.log(error); });
三、后台管理系统搭建
Ⅰ、初始化工程
- 使用
create-react-app hk_manager
创建后台管理系统项目
Ⅱ、完善目录结构
㈠、public文件夹
- 更换favicon.ico
- public/css/cssReset.css:清理各种间距、空隙等
- 在public/index.html中引入cssReset.css 并删除多余部分
㈡、src文件夹
-
删除无用文件,修改index.js和App.js
-
配置项目基本目录
① api文件夹:后台管理系统和服务器交互的接口
将axios封装为ajax网络请求,后续如果更换网络请求组件,直接更改这里即可。
使用axios之前需要安装axios:yarn add axios
② components文件夹:组件,各个页面会用到的公共组件
③ config文件夹:到处需要用的配置
④ pages文件夹:各个界面
⑤ store文件夹-
安装依赖
yarn add redux redux-saga react-redux
-
新建store/index.js
-
新建store/reducers.js
-
新建store/actionTypes.js
-
新建store/sagas
-
进行基础默认配置
-
配置react-redux
① 修改index.js,引入provider并包裹APP:React:Redux简介
② 修改App.js,将state和dispatch放到props中
⑥ tools 工具文件夹
-
Ⅲ、集成antd
官网学习
㈠、安装
- 使用命令
yarn add antd
安装antd
㈡、配置开发阶段按需加载
1、直接使用antd存在的问题
- 无法进行主题配置
- 加载全部的antd组件的样式
2、解决
- 使用 react-app-rewired
- 安装
yarn add react-app-rewired customize-cra
- 修改package.json
"scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", }
- 在项目根目录创建一个config-overrides.js用于修改默认配置
module.exports = function override(config, env) { // do stuff with the webpack config... return config; };
- 使用babel-plugin-import
① 安装yarn add babel-plugin-import
② 配置config-overrides.js+ const { override, fixBabelImports } = require('customize-cra'); - module.exports = function override(config, env) { - // do stuff with the webpack config... - return config; - }; + module.exports = override( + fixBabelImports('antd', { + libraryDirectory: 'es', + style: 'css', + }), + );
㈢、完成配置
完成上述配置后,antd在开发过程中就成为了按需加载。
Ⅳ、配置路由
㈠、安装
- 使用命令
yarn add react-router-dom
安装路由
㈡、新建界面
- 在pages下新建pages/admin/admin.jsx和pages/login/login.jsx界面
- 书写最简单界面,完成界面显示
㈢、配置路由
在App.js中配置主路由
Ⅴ、Login界面搭建
㈠、引入文件
login文件夹下新建login/css/login.css文件,存放样式;新建login/image/login.png,当作登录图片
㈡、使用natd的form登录表单
- 使用form普通登录表单
- 将代码引入项目,自己按需修改CSS,最终效果
Ⅵ、主界面搭建
㈠、界面设计,创建文件夹
界面分为左右两大部分,同时右边又分为上中下三部分。
㈡、admin主界面进行配置
㈢、LeftNav左边导航栏进行配置
1、侧边栏界面搭建
- 左边导航栏需要点击进行路由切换,但是需要在admin中配置路由,并不在left-nav中配置,所以需要引入
withRouter
- 配置left-nav.jsx
除了设置defaultSelectedKeys,还需要设置selectedKeys,不然路由与导航无法很好的搭配 - 按照需求修改样式,效果
2、将导航抽成JSON文件进行渲染
-
创建JSON文件存储菜单数据
-
修改left-nav.jsx
-
defaultSelectedKeys
接收默认选中路径
3、使用自己的字体图标
字体图标的
㈣、右侧头部配置
1、基础配置
2、配置侧边导航栏收缩
① 因为左侧和右侧头部要协调控制导航栏收缩,所以将右侧头部中的state和toggle放到他们的共同父组件中。并将它们作为属性传递给子组件。
② 右侧头部设置
③ 左侧导航栏设置
3、收缩样式调整
默认收缩样式为
需要进行调整
-
调整照片
设置行内样式,根据是否有collapsed设置宽高 -
调整文字:当收缩的时候 ① 文字隐藏 ② 图标放大
① 设置文字② 设置图标
4、添加天气预报模块
5、底部文字居中
㈤、主界面基本样式
Ⅴ、配置一级路由
㈠、创建组件
㈡、设置组件基本格式
import React, {Component} from 'react'
export default class Lives extends Component {
render() {
return (
<div style={{backgroundColor: 'gold'}}>
设置
</div>
);
}
}
㈢、在admin.jsx配置路由
1、引入路由组件
2、引入一级路由
3、配置路由
4、404界面配置
- 配置notFound.jsx
- 配置notFound.css
- 因为notFound在路由中,所以可以直接通过
this.props.history.replace('/')
进行跳转 - 其他界面目前为最简单格式
Ⅵ、配置二级路由
㈠、配置
㈡、注意
配置二级路由的时候,一级路由不能设置exact
精准匹配,否则无法匹配到对应界面!
Ⅶ、配置首页
㈠、首页上部分
1、基础样式配置
2、利用TypeProps信息当作参数传递
㈡、图表集成
使用echart加载图表。
借助echart-for-reacts集成echarts。
1、安装
- 安装echarts:
yarn add echarts
- 安装echarts-for-react:
yarn add echarts-for-react
- 注意:echarts-for-react不兼容echarts,安装以下版本可以使用
2、使用
-
分别新建home/component/buyCount/buyCount.js和home/component/sourceCount/sourceCount.jsx制作两张图表
-
配置静态图表
3、效果
Ⅷ、登录功能实现
㈠、接口配置
1、主管理员注册接口
在hk_server_api
Web服务器中配置接口
- 主管理员注册接口
① 在routes目录下新建routes/admin.js
② 在app中引入
③ 定义接口
2、登录接口
关于token和session知识点可以看额外知识点—Ⅷ、用户登录信息存储
3、退出登录接口
- 最基础退出接口:
存在的问题:正常来说只有登录后才能访问退出接口,因此需要做权限控制。 - 使用全局中间件进行权限控制
权限控制详解请看额外知识点 Ⅷ、权限控制
㈡、客户端功能实现
1、判断是否登录
-
定义接口
-
界面调用
2、登录
-
定义接口
-
界面调用
-
跨域配置
3、退出登录
- 接口定义
- 界面调用
Ⅸ、设置中心实现
㈠ 、修改用户信息实现
1、账户设置界面完善
-
界面搭建
① 用户信息界面
-
接口定义
① 管理员头像上传接口
② 修改用户数据接口 -
页面业务实现
① 定义接口
② 界面实现
2、左边菜单栏用户信息更新
㈡、修改密码实现
1、界面配置
2、定义接口
3、页面业务
- 客户端接口配置
- 页面业务实现
Ⅹ、幼教资源管理实现
㈠、新建界面
- resource目录下新建pages/resourceAdd.jsx和pages/resourceList.jsx
㈡、路由配置
在resource.jsx里面配置路由
㈢、静态界面
1、幼教资源列表
- 静态界面
2、添加幼教资源
- 静态界面
㈣、接口处理
1、多文件上传
- public/uploads文件夹下新建resource文件夹
- controller/manageAPI/uploadImg.js中向外暴露新上传接口,并上传到resource文件夹
- 该接口依赖于之前两步
2、单文件上传
- public/uploads/images文件夹下新建resource文件夹
- controller/manageAPI/uploadImg.js中向外暴露新上传接口,并上传到resource文件夹
- 该接口依赖于之前封装的图片上传接口与图片上传文件夹
3、获取所属分类、班级、领域、格式、素材
4、添加资源
5、获取资源列表
6、设置是否轮播图
7、删除一个资源
8、获取上传的文件
9、修改一条资源
10、app.js中配置
㈤、前端业务实现
1、页面SDK实现
- api目录下新建resourceApi.js,存放直播类接口
- 完善SDK
2、新建用户关联数据表
- 建表语句
DROP TABLE IF EXISTS t_resource_file; CREATE TABLE t_resource_file ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', url VARCHAR ( 255 ) COMMENT '文件地址', name VARCHAR ( 255 ) COMMENT '文件名称', uid VARCHAR ( 255 ) COMMENT '唯一识别', tag VARCHAR ( 255 ) COMMENT '关联标识' ) COMMENT '多文件上传关系对应表'
3、业务实现
-
添加幼教资源界面完善——resourceAdd.jsx
import React from 'react' import {Card, Form, Input, Select, Upload, message, Button} from 'antd' import {InboxOutlined} from '@ant-design/icons' import Moment from 'moment' import KaiUploadImg from '../../../components/KaiUploadImg' import { getResourceClasses, getResourceMeta, getResourceFormat, getResourceCategory, getResourceArea, addResource } from "../../../api/resourceApi"; import {getUser} from "../../../api/adminApi"; const {Option} = Select; export default class AddResource extends React.Component { constructor(props) { super(props); this.state = { imageUrl: '', focusImgUrl: '', dragFileList: [], // 存放上传的文件 resource_classes: [], resource_meta: [], resource_format: [], resource_category: [], resource_area: [], } } componentDidMount() { getResourceClasses().then((result) => { if (result && result.status === 1) { this.setState({ resource_classes: result.data }) } }).catch((error) => { console.log(error); }); getResourceArea().then((result) => { if (result && result.status === 1) { this.setState({ resource_area: result.data }) } }).catch((error) => { console.log(error); }); getResourceCategory().then((result) => { if (result && result.status === 1) { this.setState({ resource_category: result.data }) } }).catch((error) => { console.log(error); }); getResourceFormat().then((result) => { if (result && result.status === 1) { this.setState({ resource_format: result.data }) } }).catch((error) => { console.log(error); }); getResourceMeta().then((result) => { if (result && result.status === 1) { this.setState({ resource_meta: result.data }) } }).catch((error) => { console.log(error); }); } render() { const formItemLayout = { labelCol: { xs: {span: 2} }, wrapperCol: { xs: {span: 12} }, }; const { resource_classes, resource_meta, resource_format, resource_category, resource_area } = this.state; const onFinish = values => { const {imageUrl, focusImgUrl, dragFileList} = this.state; if (!imageUrl) { message.warning('请上传资源封面!'); return; } // 1. 生成创建日期 const resource_publish_time = Moment(new Date()).format('YYYY-MM-DD HH:mm:ss'); // 2. 上传资源 addResource(getUser().token, values.resource_name, values.resource_author, resource_publish_time, dragFileList, values.resource_category_id, values.resource_classes_id, values.resource_area_id, values.resource_meta_id, values.resource_format_id, imageUrl, values.resource_price, focusImgUrl).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('添加直播课失败!'); }) }; return ( <Card title="新增幼教资源"> <Form {...formItemLayout} onFinish={onFinish}> <Form.Item label={"资源名称"} name="resource_name" rules={[{required: true, message: '请输入资源名称!'}]} > <Input/> </Form.Item> <Form.Item label={"资源作者"} name="resource_author" rules={[{required: true, message: '请输入作者姓名!'}]} > <Input/> </Form.Item> <Form.Item label="所属分类" name="resource_category_id" rules={[{required: true, message: '请选择所属分类!'}]} > <Select placeholder={"请选择所属分类"} style={{width: 200}}> { resource_category.map(item => { return ( <Option value={item.id} key={item.id}>{item.category_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="所属班级" name="resource_classes_id" rules={[{required: true, message: '请选择所属班级!'}]} > <Select placeholder={"请选择所属班级"} style={{width: 200}}> { resource_classes.map(item => { return ( <Option value={item.id} key={item.id}>{item.classes_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="所属领域" name="resource_area_id" rules={[{required: true, message: '请选择所属领域!'}]} > <Select placeholder={"请选择所属领域"} style={{width: 200}}> { resource_area.map(item => { return ( <Option value={item.id} key={item.id}>{item.area_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="素材选择" name="resource_meta_id" rules={[{required: true, message: '请选择素材!'}]} > <Select placeholder={"请选择素材"} style={{width: 200}}> { resource_meta.map(item => { return ( <Option value={item.id} key={item.id}>{item.material_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="素材格式" name="resource_format_id" rules={[{required: true, message: '请选择素材格式!'}]} > <Select placeholder={"请选择素材格式"} style={{width: 200}}> { resource_format.map(item => { return ( <Option value={item.id} key={item.id}>{item.format_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label={"资源价格"} name="resource_price" rules={[{required: true, message: '请输入资源的价格!'}]} > <Input style={{width: 120}}/> </Form.Item> <Form.Item label={"资源封面图"} name="resource_img" > <KaiUploadImg upLoadBtnTitle={"上传资源封面图"} upLoadName={"resource_upload_img"} upLoadAction={"/api/auth/resource/upload_resource"} successCallBack={(name) => { this.setState({ imageUrl: name }) }} /> </Form.Item> <Form.Item label={"首页轮播图"} name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传首页焦点图"} upLoadName={"resource_upload_img"} upLoadAction={"/api/auth/resource/upload_resource"} successCallBack={(name) => { this.setState({ focusImgUrl: name }) }} /> </Form.Item> <Form.Item label={"幼教资源"} name="resource_content" > <Upload.Dragger name='resource_file' multiple={true} action={'/api/auth/resource/upload_many_file'} onChange={(info) => { const {status} = info.file; if (status !== 'uploading') { // console.log(info.file, info.fileList); } if (status === 'done') { if (info.file.response && info.file.response.status === 1) { /* console.log(`-----------------`); console.log(info.file.response.data); console.log(`-----------------`); */ let tempArr = this.state.dragFileList; tempArr.push(info.file.response.data); this.setState({ dragFileList: tempArr }, () => { console.log(this.state.dragFileList); }) } message.success(`${info.file.name} 文件上传成功!`); } else if (status === 'error') { message.error(`${info.file.name} 文件上传失败!`); } }} onRemove={(file) => { console.log(file); let tempArr = this.state.dragFileList; let newTempArr = []; for (let i = 0; i < tempArr.length; i++) { if (tempArr[i].uid !== file.response.data.uid) { newTempArr.push(tempArr[i]); } } // 更新状态 this.setState({ dragFileList: newTempArr }, () => { console.log(this.state.dragFileList); }) }} > <p className="ant-upload-drag-icon"> <InboxOutlined/> </p> <p className="ant-upload-text">单击或者拖到文件到此区域上传</p> <p className="ant-upload-hint">支持单个或多上文件上传</p> </Upload.Dragger> </Form.Item> <Form.Item wrapperCol={{span: 16}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type={"primary"} htmlType={"submit"} style={{marginRight: 15}}>保存</Button> <Button onClick={() => { this.props.history.goBack() }}>取消</Button> </div> </Form.Item> </Form> </Card> ) } }
-
编辑幼教资源界面完善——resourceEdit.jsx
import React from 'react' import {Card, Form, Input, Select, Upload, message, Button} from 'antd' import {InboxOutlined} from '@ant-design/icons' import Moment from 'moment' import KaiUploadImg from '../../../components/KaiUploadImg' import {getResourceClasses, getResourceMeta, getResourceFormat, getResourceCategory, getResourceArea, editResource, getFileList} from "../../../api/resourceApi"; import {getUser} from "../../../api/adminApi"; const {Option} = Select; export default class ResourceEdit extends React.Component { constructor(props) { super(props); this.state = { imageUrl: '', focusImgUrl: '', dragFileList: [], // 存放上传的文件 resource_classes: [], resource_meta: [], resource_format: [], resource_category: [], resource_area: [], resource_id: '', resource_content_tag: '' }; this.resourceFormRef = React.createRef(); } componentDidMount() { if(!this.props.location.state){ this.setState = ()=> false; this.props.history.goBack(); } // 0. 获取上一个界面传递的数据 if(this.props.location.state){ const resourceItem = this.props.location.state.resource; if(resourceItem){ this.resourceFormRef.current.setFieldsValue(resourceItem); // 封面图/轮播图/直播信息id this.setState({ imageUrl: resourceItem.resource_img, // 资源封面 focusImgUrl: resourceItem.focus_img, // 轮播图封面 resource_id: resourceItem.id, resource_content_tag: resourceItem.resource_content }); } // 1. 获取文件 getFileList(resourceItem.resource_content).then((result) => { if (result && result.status === 1) { this.setState({ dragFileList: result.data }) } }).catch((error) => { console.log(error); }); } getResourceClasses().then((result) => { console.log(result); if (result && result.status === 1) { this.setState({ resource_classes: result.data }) } }).catch((error) => { console.log(error); }); getResourceArea().then((result) => { if (result && result.status === 1) { this.setState({ resource_area: result.data }) } }).catch((error) => { console.log(error); }); getResourceCategory().then((result) => { if (result && result.status === 1) { this.setState({ resource_category: result.data }) } }).catch((error) => { console.log(error); }); getResourceFormat().then((result) => { if (result && result.status === 1) { this.setState({ resource_format: result.data }) } }).catch((error) => { console.log(error); }); getResourceMeta().then((result) => { if (result && result.status === 1) { this.setState({ resource_meta: result.data }) } }).catch((error) => { console.log(error); }); } render() { const formItemLayout = { labelCol: { xs: {span: 2} }, wrapperCol: { xs: {span: 12} }, }; const { resource_classes, resource_meta, resource_format, resource_category, resource_area } = this.state; const onFinish = values => { const {imageUrl, focusImgUrl, dragFileList, resource_id, resource_content_tag} = this.state; if(!imageUrl){ message.warning('请上传资源封面!'); return; } // 1. 生成创建日期 const resource_publish_time = Moment(new Date()).format('YYYY-MM-DD HH:mm:ss'); // 2. 上传资源 editResource(getUser().token, resource_id, values.resource_name, values.resource_author, resource_publish_time, dragFileList, values.resource_category_id, values.resource_classes_id, values.resource_area_id,values.resource_meta_id, values.resource_format_id, imageUrl, values.resource_price, focusImgUrl, resource_content_tag).then((result)=>{ console.log(result); if(result && result.status === 1){ message.success(result.msg); this.props.history.goBack(); } }).catch((error)=>{ console.log(error); message.error('编辑直播课失败!'); }) }; const {imageUrl, focusImgUrl} = this.state; return ( <Card title="编辑幼教资源"> <Form {...formItemLayout} onFinish={onFinish} ref={this.resourceFormRef}> <Form.Item label={"资源名称"} name="resource_name" rules={[{required: true, message: '请输入资源名称!'}]} > <Input/> </Form.Item> <Form.Item label={"资源作者"} name="resource_author" rules={[{required: true, message: '请输入作者姓名!'}]} > <Input/> </Form.Item> <Form.Item label="所属分类" name="resource_category_id" rules={[{required: true, message: '请选择所属分类!'}]} > <Select placeholder={"请选择所属分类"} style={{width: 200}}> { resource_category.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.category_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="所属班级" name="resource_classes_id" rules={[{required: true, message: '请选择所属班级!'}]} > <Select placeholder={"请选择所属班级"} style={{width: 200}}> { resource_classes.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.classes_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="所属领域" name="resource_area_id" rules={[{required: true, message: '请选择所属领域!'}]} > <Select placeholder={"请选择所属领域"} style={{width: 200}}> { resource_area.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.area_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="素材选择" name="resource_meta_id" rules={[{required: true, message: '请选择素材!'}]} > <Select placeholder={"请选择素材"} style={{width: 200}}> { resource_meta.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.material_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="素材格式" name="resource_format_id" rules={[{required: true, message: '请选择素材格式!'}]} > <Select placeholder={"请选择素材格式"} style={{width: 200}}> { resource_format.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.format_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label={"资源价格"} name="resource_price" rules={[{required: true, message: '请输入资源的价格!'}]} > <Input style={{width: 120}}/> </Form.Item> <Form.Item label={"资源封面图"} name="resource_img" > <KaiUploadImg upLoadBtnTitle={"上传资源封面图"} upLoadName={"resource_upload_img"} upLoadAction={"/api/auth/resource/upload_resource"} upImage={imageUrl} successCallBack={(name) => { this.setState({ imageUrl: name }) }} /> </Form.Item> <Form.Item label={"首页轮播图"} name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传首页焦点图"} upLoadName={"resource_upload_img"} upImage={focusImgUrl} upLoadAction={"/api/auth/resource/upload_resource"} successCallBack={(name) => { this.setState({ focusImgUrl: name }) }} /> </Form.Item> <Form.Item label={"幼教资源"} name="resource_content" > <Upload.Dragger name='resource_file' multiple={true} fileList={this.state.dragFileList} action={'/api/auth/resource/upload_many_file'} onChange={(info) => { console.log(info); // 补: 官方漏洞 this.setState({ dragFileList: info.fileList.slice() }); const {status} = info.file; if (status !== 'uploading') { // console.log(info.file, info.fileList); } if (status === 'done') { if (info.file.response && info.file.response.status === 1) { let tempArr = this.state.dragFileList; let tempA = []; for(let i=0; i<tempArr.length; i++){ if(!tempArr[i].response){ tempA.push(tempArr[i]); } } tempArr = tempA; tempArr.push(info.file.response.data); this.setState({ dragFileList: tempArr }, () => { console.log(this.state.dragFileList); }) } message.success(`${info.file.name} 文件上传成功!`); } else if (status === 'error') { message.error(`${info.file.name} 文件上传失败!`); } }} onRemove={(file) => { console.log(file); let tempArr = this.state.dragFileList; let newTempArr = []; for (let i = 0; i < tempArr.length; i++) { if (tempArr[i].uid !== file.uid) { newTempArr.push(tempArr[i]); } } // 更新状态 this.setState({ dragFileList: newTempArr }, () => { console.log(this.state.dragFileList); }) }} > <p className="ant-upload-drag-icon"> <InboxOutlined/> </p> <p className="ant-upload-text">单击或者拖到文件到此区域上传</p> <p className="ant-upload-hint">支持单个或多上文件上传</p> </Upload.Dragger> </Form.Item> <Form.Item wrapperCol={{span: 16}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type={"primary"} htmlType={"submit"} style={{marginRight: 15}}>修改</Button> <Button onClick={() => { this.props.history.goBack() }}>取消</Button> </div> </Form.Item> </Form> </Card> ) } }
-
幼教资源列表界面完善——resourceList.jsx
import React from 'react' import {Card, Button, Table, Switch, Divider, Modal, message, notification} from 'antd' import {getResourceList, setFocusResource, deleteResource} from "../../../api/resourceApi"; import config from "../../../config/config"; export default class ResourceList extends React.Component { constructor(props) { super(props); this.state = { resourceList: [], totalSize: 0, pageSize: 4 } } componentDidMount() { // 加载列表数据 this._loadData(); } _loadData = (page_num = 1, page_size = 4) => { getResourceList(page_num, page_size).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.setState({ totalSize: result.data.resource_count, resourceList: result.data.resource_list }) } }).catch(() => { message.error('获取资源列表失败!'); }) }; // 列的配置信息 columns = [ {title: 'ID', dataIndex: 'id', key: 'id', width: 50, align: 'center'}, {title: '幼教标题', dataIndex: 'resource_name', key: 'resource_name', align: 'center'}, { title: '幼教封面', dataIndex: 'resource_img', key: 'resource_img', align: 'center', render: (text, record) => { return ( <img src={config.BASE_URL + record.resource_img} alt="课程封面" width={100}/> ) } }, {title: '所属作者', dataIndex: 'resource_author', key: 'resource_author', align: 'center'}, {title: '所属分类', dataIndex: 'category_name', key: 'category_name', align: 'center'}, { title: '首页焦点', dataIndex: 'is_focus', key: 'is_focus', align: 'center', render: (text, record) => { return ( <Switch checkedChildren="是" unCheckedChildren="否" disabled={record.focus_img.length === 0} defaultChecked={record.is_focus === 1} onChange={(checked) => { setFocusResource(record.id, checked ? 1 : 0).then((result) => { if (result && result.status === 1) { notification["success"]({ message: `课程: ${record.resource_name}`, description: `${checked ? '设置为' : '取消'}焦点活动!` }); } }) }} /> ) } }, { title: '操作', align: 'center', render: (text, record) => { return ( <div> <Button onClick={() => { this.props.history.push({ pathname: '/resource/resource-edit', state: { resource: record } }); }}>编辑</Button> <Divider type="vertical"/> <Button onClick={() => { Modal.confirm({ title: '确认删除吗?', content: '删除此资源,所有关联的内容都会被删除', okText: '确认', cancelText: '取消', onOk: () => { deleteResource(record.id).then(result => { if (result && result.status === 1) { message.success(result.msg); this._loadData(); } else { message.error('删除失败!'); } }).catch(() => { message.error('删除失败!'); }) } }); }}>删除</Button> </div> ) } }, ]; render() { // 添加按钮 let addBtn = ( <Button type={"primary"} onClick={() => { this.props.history.push('/resource/resource-add'); }}> 添加幼教资源 </Button> ); return ( <Card title={"幼教资源列表"} extra={addBtn}> <Table columns={this.columns} dataSource={this.state.resourceList} rowKey={"id"} pagination={{ total: this.state.totalSize, pageSize: this.state.pageSize, onChange: (pageNum, pageSize) => { console.log('需要加载' + pageNum, pageSize); this._loadData(pageNum, pageSize) } }} /> </Card> ) } }
Ⅺ、职场人生管理实现
㈠、新建界面
㈡、路由配置
㈢、静态界面
1、职场人生列表
2、 添加职场人生资源
㈣、接口处理
1、直播封面图和轮播图接口上传
-
public/uploads/images文件夹下新建lifejob文件夹
-
controller/manageAPI/uploadImg.js中向外暴露新上传接口,并上传到lifejob文件夹
-
该接口依赖于之前封装的图片上传接口与图片上传文件夹
2、获取学前所属分类、所属家园
3、添加一个职场人生
4、获取职场人生列表
5、设置是否为轮播图
6、删除一个职场人生
7、修改一个职场人生
8、app.js中配置
㈤、前端业务实现
1、页面SDK实现
- api目录下新建lifejobApi.js,存放直播类接口
- 完善SDK
2、业务实现
- 添加、编辑活动界面完善——lifeAddEdit.jsx
import React from 'react' import {Card, Form, Input, Select, Button, message} from 'antd' import KaiUploadImg from '../../../components/KaiUploadImg' import RichTextEditor from './../../../components/RichTextEditor' import {getJobPre, getJobFamily, addJob, editJob} from "../../../api/lifejobApi"; import {getUser} from "../../../api/adminApi"; import Moment from "moment"; import {getObj} from "../../../tools/cache-tool"; const { Option } = Select; export default class LifeAddEdit extends React.Component{ constructor(props){ super(props); this.state = { imageUrl: '', // 资源封面 focusImgUrl: '', // 轮播图封面 job_id: '', job_content: '', job_pre: [], job_family: [] }; this.job_life_ref = React.createRef(); this.jobFormRef = React.createRef(); } componentDidMount() { // 0. 刷新页面处理 if(getObj('life_job_tag')==='edit' && !this.props.location.state){ this.setState = ()=> false; this.props.history.goBack(); } // 1. 获取上一个界面传递的数据 if(this.props.location.state){ const jobItem = this.props.location.state.job; if(jobItem){ this.jobFormRef.current.setFieldsValue(jobItem); this.setState({ imageUrl: jobItem.job_img, // 资源封面 focusImgUrl: jobItem.focus_img, // 轮播图封面 job_id: jobItem.id, job_content: jobItem.job_content }) } } getJobPre().then((result)=>{ if(result && result.status === 1){ this.setState({ job_pre: result.data }) } }).catch((error)=>{ console.log(error); }); getJobFamily().then((result)=>{ if(result && result.status === 1){ this.setState({ job_family: result.data }) } }).catch((error)=>{ console.log(error); }); } render() { const formItemLayout = { labelCol: { xs: { span: 3 } }, wrapperCol: { xs: { span: 12 } }, }; const onFinish = values => { // 0. 容错 const {imageUrl, focusImgUrl, job_id} = this.state; if(!imageUrl){ message.warning('请上传活动封面!'); return; } // 1. 活动时间 const job_time = Moment(new Date()).format('YYYY-MM-DD HH:mm:ss'); // 2. 获取富文本输入框中的内容 let job_content = this.job_life_ref.current.getContent(); // 4. 调用接口 if(job_id){ editJob(getUser().token, job_id, values.job_name, imageUrl, values.job_author, job_time, job_content, values.job_pre_edu_id, values.job_family_edu_id, focusImgUrl).then((result)=>{ if(result && result.status === 1){ message.success(result.msg); this.props.history.goBack(); } }).catch(()=>{ message.error('修改人生失败!'); }) }else { addJob(getUser().token, values.job_name, imageUrl, values.job_author, job_time, job_content, values.job_pre_edu_id, values.job_family_edu_id, focusImgUrl).then((result)=>{ if(result && result.status === 1){ message.success(result.msg); this.props.history.goBack(); } }).catch(()=>{ message.error('添加人生失败!'); }) } }; const {job_pre, job_family, imageUrl, focusImgUrl, job_content} = this.state; return ( <Card title={getObj('life_job_tag') !== 'edit' ? "新增人生资源": "编辑人生资源"}> <Form {...formItemLayout} onFinish={onFinish} ref={this.jobFormRef}> <Form.Item label={"人生名称"} name = "job_name" rules={[{ required: true, message: '请输入职场人生名称!' }]} > <Input /> </Form.Item> <Form.Item label={"人生作者"} name = "job_author" rules={[{ required: true, message: '请输入作者姓名!' }]} > <Input /> </Form.Item> <Form.Item label="学前所属分类" name="job_pre_edu_id" rules={[{ required: true, message: '请选择学前所属分类!' }]} > <Select placeholder={"请选择学前所属分类"} style={{width: 200}}> { job_pre && job_pre.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.pre_edu_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="所属家园分类" name="job_family_edu_id" rules={[{ required: true, message: '请选择所属家园分类!' }]} > <Select placeholder={"请选择所属家园分类"} style={{width: 200}}> { job_family && job_family.map(item=>{ return ( <Option value={item.id} key={item.id}>{item.job_family_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="人生封面图" name="job_img" > <KaiUploadImg upLoadBtnTitle={"上传封面图"} upLoadName={"job_img"} upImage={imageUrl} upLoadAction={"/api/auth/lifejob/upload_life_job"} successCallBack={(name)=>{ this.setState({ imageUrl: name }) }} /> </Form.Item> <Form.Item label="焦点图" name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传焦点图"} upImage={focusImgUrl} upLoadName={"job_img"} upLoadAction={"/api/auth/lifejob/upload_life_job"} successCallBack={(name)=>{ this.setState({ focusImgUrl: name }) }} /> </Form.Item> <Form.Item label="职场人生内容" name="job_content" wrapperCol={{span: 20}} > <RichTextEditor uploadName={'job_img'} uploadAction={'/api/auth/lifejob/upload_life_job'} htmlContent={job_content} ref={this.job_life_ref} /> </Form.Item> <Form.Item wrapperCol={{span: 16}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type={"primary"} htmlType="submit" style={{marginRight: 15}}> {getObj('life_job_tag') !== 'edit' ? '添加' : '修改'} </Button> <Button onClick={()=>{this.props.history.goBack()}}>取消</Button> </div> </Form.Item> </Form> </Card> ) } }
- 活动列表界面完善——lifeList.jsx
import React from 'react' import {Card, Button, Table, Switch, Divider, Modal, message, notification} from 'antd' import {getJobList, setFocusJob, deleteJob} from "../../../api/lifejobApi"; import config from "../../../config/config"; import {saveObj} from "../../../tools/cache-tool"; export default class LifeList extends React.Component { constructor(props) { super(props); this.state = { jobList: [], totalSize: 0, pageSize: 4 } } componentDidMount() { // 加载列表数据 this._loadData(); } _loadData = (page_num=1, page_size=4)=>{ getJobList(page_num, page_size).then((result)=>{ console.log(result); if(result && result.status === 1){ message.success(result.msg); this.setState({ totalSize: result.data.job_count, jobList: result.data.job_list }) } }).catch(()=>{ message.error('获取直播课程失败!'); }) }; // 列的配置信息 columns = [ {title: 'ID', dataIndex: 'id', key: 'id', width: 50, align: 'center'}, {title: '职场人生标题', dataIndex: 'job_name', key: 'job_name',align: 'center'}, { title: '职场人生封面', dataIndex: 'job_img', key: 'job_img',align: 'center', render: (text, record) => { return ( <img src={config.BASE_URL + record.job_img} alt="人生封面" width={100}/> ) } }, {title: '所属作者', dataIndex: 'job_author', key: 'job_author',align: 'center'}, { title: '首页焦点', dataIndex: 'is_focus', key: 'is_focus',align: 'center', render: (text, record) => { return ( <Switch checkedChildren="是" unCheckedChildren= "否" disabled={record.focus_img.length === 0} defaultChecked={record.is_focus === 1} onChange={(checked)=>{ setFocusJob(record.id, checked ? 1 : 0).then((result)=>{ if(result && result.status === 1){ notification["success"]({ message: `人生: ${record.job_name}`, description: `${checked ? '设置为' : '取消'}焦点人生!` }); } }) }} /> ) } }, { title: '操作', align: 'center', render: (text, record) => { return ( <div> <Button onClick={()=>{ // 往本地存储一个tag saveObj('life_job_tag','edit'); this.props.history.push({ pathname: '/lifejob/add-edit', state: { job: record } }); }}>编辑</Button> <Divider type="vertical" /> <Button onClick={()=>{ Modal.confirm({ title: '确认删除吗?', content: '删除此资源,所有关联的内容都会被删除', okText: '确认', cancelText: '取消', onOk: ()=> { deleteJob(record.id).then(result=>{ if(result && result.status === 1){ message.success(result.msg); this._loadData(); }else { message.error('删除失败!'); } }).catch(()=>{ message.error('删除失败!'); }) } }); }}>删除</Button> </div> ) } }, ]; render() { // 添加按钮 let addBtn = ( <Button type={"primary"} onClick={() => { // 往本地存储一个tag saveObj('life_job_tag','add'); this.props.history.push('/lifejob/add-edit'); }}> 添加人生资源 </Button> ); return ( <Card title={"人生资源列表"} extra={addBtn}> <Table columns={this.columns} dataSource={this.state.jobList} rowKey={"id"} pagination={{ total: this.state.totalSize, pageSize: this.state.pageSize, onChange: (pageNum, pageSize)=>{ console.log('需要加载' + pageNum, pageSize); this._loadData(pageNum, pageSize); } }} /> </Card> ) } }
Ⅻ、活动专区管理实现
㈠、新建界面
㈡、路由配置
㈢、静态界面
1、活动列表
2、添加活动
㈣、接口处理
1、直播封面图和轮播图上传
- public/uploads/images文件夹下新建activities文件夹
- controller/manageAPI/uploadImg.js中向外暴露新上传接口,并上传到activities文件夹
- 该接口依赖于之前封装的图片上传接口与图片上传文件夹
2、获取活动地点、招生对象、营期
3、添加一个活动
4、获取直播课程列表
5、设置是否为轮播图
6、删除一个活动
7、编辑一个活动
8、app.js中配置
㈤、前端业务实现
1、页面SDK实现
- api目录下新建activitiesApi.js,存放直播类接口
- 完善SDK
2、业务实现
-
添加活动界面完善——activitiesAdd.jsx
import React, {Component} from "react" import {Card, Form, Input, Select, Button, DatePicker, message} from 'antd' // 引入富文本编辑器 import RichTextEdit from "./../../../components/RichTextEditor" // 引入tag选择 import KaiUploadImg from "../../../components/KaiUploadImg"; import KaiTag from './../../../components/KaiTag' import {getActivitiesObject, getActivitiesAddress, getActivitiesBus, addActivities} from '../../../api/activitiesApi' import Moment from "moment"; import {getUser} from "../../../api/adminApi"; const {Option} = Select; export default class ActivitiesAdd extends Component { constructor(props) { super(props); this.activities_intro_ref = React.createRef(); this.activities_trip_ref = React.createRef(); this.activities_days_ref = React.createRef(); this.activities_notice_ref = React.createRef(); this.state = { imageUrl: '', // 资源封面 focusImgUrl: '', // 轮播图封面 activities_address: [], // 活动地址数组 activities_object: [], // 活动对象数组 activities_bus: [], // 活动营期数组 activities_tag: [] // 活动标签 } } componentDidMount() { getActivitiesBus().then((result) => { if (result && result.status === 1) { this.setState({ activities_bus: result.data }) } }).catch((error) => { console.log(error); }); getActivitiesAddress().then((result) => { if (result && result.status === 1) { this.setState({ activities_address: result.data }) } }).catch((error) => { console.log(error); }); getActivitiesObject().then((result) => { if (result && result.status === 1) { this.setState({ activities_object: result.data }) } }).catch((error) => { console.log(error); }); } formItemLayout = { labelCol: {span: 3}, wrapperCol: {span: 12}, }; render() { const onFinish = values => { const {imageUrl, focusImgUrl, activities_tag} = this.state; if (!imageUrl) { message.warning('请上传活动封面!'); return; } // 1. 活动时间 const activities_time = Moment(values.activities_time).format('YYYY-MM-DD HH:mm:ss'); // 2. 处理活动的tag ['海上', '天上'] ---> 海上,天上 let tagStr = activities_tag.join(','); // 3. 获取各个富文本输入框中的内容 let activities_intro = this.activities_intro_ref.current.getContent(); let activities_trip = this.activities_trip_ref.current.getContent(); let activities_days = this.activities_days_ref.current.getContent(); let activities_notice = this.activities_notice_ref.current.getContent(); // 4. 调用接口 addActivities(getUser().token, values.activities_name, activities_time, imageUrl, values.activities_price, tagStr, values.activities_address_id, values.activities_object_id, values.activities_bus_day_id, activities_intro, activities_trip, activities_days, activities_notice, focusImgUrl).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('添加活动失败!'); }) }; const {activities_address, activities_object, activities_bus} = this.state; return ( <Card title="新增活动"> <Form {...this.formItemLayout} onFinish={onFinish}> <Form.Item label="活动标题" name="activities_name" rules={[{required: true, message: '请输入活动标题!'}]} > <Input/> </Form.Item> <Form.Item name="activities_time" label="活动日期" rules={[{type: 'object', required: true, message: '请选择活动时间!'}]} > <DatePicker placeholder="请选择日期"/> </Form.Item> <Form.Item label="活动价格" name="activities_price" rules={[{required: true, message: '请输入活动的价格!'}]} wrapperCol={{span: 6}} > <Input/> </Form.Item> <Form.Item label="活动地点" name="activities_address_id" rules={[{required: true, message: '请选择活动地点!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择活动地点"> { activities_address && activities_address.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_address}</Option> ) }) } </Select> </Form.Item> <Form.Item label="招生对象" name="activities_object_id" rules={[{required: true, message: '请选择招生对象!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择招生对象!"> { activities_object && activities_object.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_object_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="选择营期" name="activities_bus_day_id" rules={[{required: true, message: '请选择营期!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择招生对象!"> { activities_bus && activities_bus.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_bus_day}</Option> ) }) } </Select> </Form.Item> <Form.Item label="添加活动标签" name="activities_tag" > <KaiTag tagsCallBack={(tags) => { this.setState({ activities_tag: tags }) }}/> </Form.Item> <Form.Item label="活动封面图" name="activities_img" > <KaiUploadImg upLoadBtnTitle={"上传活动封面"} upLoadName={"activities_img"} upLoadAction={"/api/auth/activities/upload_activities"} successCallBack={(imgUrl) => { this.setState({ imageUrl: imgUrl }) }} /> </Form.Item> <Form.Item label="首页轮播图" name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传首页轮播图"} upLoadName={"activities_img"} upLoadAction={"/api/auth/activities/upload_activities"} successCallBack={(imgUrl) => { this.setState({ focusImgUrl: imgUrl }) }} /> </Form.Item> <Form.Item label="活动介绍" name="activities_intro" wrapperCol={{span: 20}} > <RichTextEdit ref={this.activities_intro_ref}/> </Form.Item> <Form.Item label="行程安排" name="activities_trip" wrapperCol={{span: 20}} > <RichTextEdit ref={this.activities_trip_ref}/> </Form.Item> <Form.Item label="开营日期" name="activities_days" wrapperCol={{span: 20}} > <RichTextEdit ref={this.activities_days_ref}/> </Form.Item> <Form.Item label="报名须知" name="activities_notice" wrapperCol={{span: 20}} > <RichTextEdit ref={this.activities_notice_ref}/> </Form.Item> <Form.Item wrapperCol={{span: 24}} > <div style={{textAlign: 'center'}}> <Button type="primary" htmlType="submit" style={{marginRight: 10}}> 保存 </Button> <Button onClick={() => { this.props.history.goBack() }}> 取消 </Button> </div> </Form.Item> </Form> </Card> ) } }
-
编辑活动界面完善——activitiesEdit.jsx
import React, {Component} from "react" import { Card, Form, Input, Select, Button, DatePicker, message } from 'antd' // 引入富文本编辑器 import RichTextEdit from "./../../../components/RichTextEditor" // 引入tag选择 import KaiTag from './../../../components/KaiTag' import KaiUploadImg from '../../../components/KaiUploadImg' import {getActivitiesObject, getActivitiesAddress, getActivitiesBus, editActivities} from '../../../api/activitiesApi' import Moment from "moment"; import {getUser} from "../../../api/adminApi"; const {Option} = Select; export default class ActivitiesEdit extends Component { constructor(props) { super(props); this.activities_intro_ref = React.createRef(); this.activities_trip_ref = React.createRef(); this.activities_days_ref = React.createRef(); this.activities_notice_ref = React.createRef(); this.activities_form_ref = React.createRef(); this.state = { imageUrl: '', // 资源封面 focusImgUrl: '', // 轮播图封面 activities_address: [], // 活动地址数组 activities_object: [], // 活动对象数组 activities_bus: [], // 活动营期数组 activities_tag: [], // 活动标签 activities_id: '', activities_intro: '', activities_trip: '', activities_days: '', activities_notice: '', } } componentDidMount() { if (!this.props.location.state) { this.setState = () => false; this.props.history.goBack(); } // 1. 获取上一个界面的数据 if (this.props.location.state) { const activitiesItem = this.props.location.state.activities; activitiesItem.activities_time = Moment(activitiesItem.activities_time); if (activitiesItem) { this.activities_form_ref.current.setFieldsValue(activitiesItem); // 1.1 处理活动标签 if (activitiesItem.activities_tag) { this.setState({ activities_tag: activitiesItem.activities_tag.split(',') }) } // 1.2 处理其它的 this.setState({ imageUrl: activitiesItem.activities_img, // 资源封面 focusImgUrl: activitiesItem.focus_img, // 轮播图封面 activities_id: activitiesItem.id, activities_intro: activitiesItem.activities_intro, activities_trip: activitiesItem.activities_trip, activities_days: activitiesItem.activities_days, activities_notice: activitiesItem.activities_notice }) } } getActivitiesBus().then((result) => { if (result && result.status === 1) { this.setState({ activities_bus: result.data }) } }).catch((error) => { console.log(error); }); getActivitiesAddress().then((result) => { if (result && result.status === 1) { this.setState({ activities_address: result.data }) } }).catch((error) => { console.log(error); }); getActivitiesObject().then((result) => { if (result && result.status === 1) { this.setState({ activities_object: result.data }) } }).catch((error) => { console.log(error); }); } componentWillUnmount() { /* this.setState = (state, callback)=>{ return false; }*/ } formItemLayout = { labelCol: {span: 3}, wrapperCol: {span: 12}, }; render() { const onFinish = values => { const {imageUrl, focusImgUrl, activities_tag, activities_id} = this.state; if (!imageUrl) { message.warning('请上传活动封面!'); return; } // 1. 活动时间 const activities_time = Moment(values.activities_time).format('YYYY-MM-DD HH:mm:ss'); // 2. 处理活动的tag ['海上', '天上'] ---> 海上,天上 let tagStr = activities_tag.join(','); // 3. 获取各个富文本输入框中的内容 let activities_intro = this.activities_intro_ref.current.getContent(); let activities_trip = this.activities_trip_ref.current.getContent(); let activities_days = this.activities_days_ref.current.getContent(); let activities_notice = this.activities_notice_ref.current.getContent(); // 4. 调用接口 editActivities(getUser().token, activities_id, values.activities_name, activities_time, imageUrl, values.activities_price, tagStr, values.activities_address_id, values.activities_object_id, values.activities_bus_day_id, activities_intro, activities_trip, activities_days, activities_notice, focusImgUrl).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('修改活动失败!'); }) }; const {activities_address, activities_object, activities_bus, activities_tag, activities_intro, activities_trip, activities_days, activities_notice, imageUrl, focusImgUrl} = this.state; return ( <Card title="新增活动"> <Form {...this.formItemLayout} onFinish={onFinish} ref={this.activities_form_ref}> <Form.Item label="活动标题" name="activities_name" rules={[{required: true, message: '请输入活动标题!'}]} > <Input/> </Form.Item> <Form.Item name="activities_time" label="活动日期" rules={[{type: 'object', required: true, message: '请选择活动时间!'}]} > <DatePicker placeholder="请选择日期"/> </Form.Item> <Form.Item label="活动价格" name="activities_price" rules={[{required: true, message: '请输入活动的价格!'}]} wrapperCol={{span: 6}} > <Input/> </Form.Item> <Form.Item label="活动地点" name="activities_address_id" rules={[{required: true, message: '请选择活动地点!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择活动地点"> { activities_address && activities_address.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_address}</Option> ) }) } </Select> </Form.Item> <Form.Item label="招生对象" name="activities_object_id" rules={[{required: true, message: '请选择招生对象!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择招生对象!"> { activities_object && activities_object.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_object_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="选择营期" name="activities_bus_day_id" rules={[{required: true, message: '请选择营期!'}]} wrapperCol={{span: 6}} > <Select placeholder="请选择招生对象!"> { activities_bus && activities_bus.map(item => { return ( <Option value={item.id} key={item.id}>{item.activities_bus_day}</Option> ) }) } </Select> </Form.Item> <Form.Item label="添加活动标签" name="activities_tag" > <KaiTag tagsArr={activities_tag} tagsCallBack={(tags) => { this.setState({ activities_tag: tags }) }}/> </Form.Item> <Form.Item label="活动封面图" name="activities_img" > <KaiUploadImg upLoadBtnTitle={"上传活动封面"} upLoadName={"activities_img"} upImage={imageUrl} upLoadAction={"/api/auth/activities/upload_activities"} successCallBack={(imgUrl) => { this.setState({ imageUrl: imgUrl }) }} /> </Form.Item> <Form.Item label="首页轮播图" name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传首页轮播图"} upLoadName={"activities_img"} upLoadAction={"/api/auth/activities/upload_activities"} upImage={focusImgUrl} successCallBack={(imgUrl) => { this.setState({ focusImgUrl: imgUrl }) }} /> </Form.Item> <Form.Item label="活动介绍" name="activities_intro" wrapperCol={{span: 20}} > <RichTextEdit uploadAction={'/api/auth/activities/upload_activities'} uploadName={'activities_img'} htmlContent={activities_intro} ref={this.activities_intro_ref} /> </Form.Item> <Form.Item label="行程安排" name="activities_trip" wrapperCol={{span: 20}} > <RichTextEdit uploadAction={'/api/auth/activities/upload_activities'} uploadName={'activities_img'} htmlContent={activities_trip} ref={this.activities_trip_ref} /> </Form.Item> <Form.Item label="开营日期" name="activities_days" wrapperCol={{span: 20}} > <RichTextEdit uploadAction={'/api/auth/activities/upload_activities'} uploadName={'activities_img'} htmlContent={activities_days} ref={this.activities_days_ref} /> </Form.Item> <Form.Item label="报名须知" name="activities_notice" wrapperCol={{span: 20}} > <RichTextEdit uploadAction={'/api/auth/activities/upload_activities'} uploadName={'activities_img'} htmlContent={activities_notice} ref={this.activities_notice_ref} /> </Form.Item> <Form.Item wrapperCol={{span: 24}} > <div style={{textAlign: 'center'}}> <Button type="primary" htmlType="submit" style={{marginRight: 10}}> 修改 </Button> <Button onClick={() => { this.props.history.goBack() }}> 取消 </Button> </div> </Form.Item> </Form> </Card> ) } }
-
活动列表界面完善——activitiesList.jsx
import React from 'react' import {Card, Button, Table, Switch, Divider, Modal, message, notification} from 'antd' import {getActivitiesList, setFocusActivities, deleteActivities} from "../../../api/activitiesApi"; import config from './../../../config/config' export default class ActivitiesList extends React.Component { constructor(props) { super(props); this.state = { activitiesList: [], totalSize: 0, pageSize: 3 } } componentDidMount() { this._loadData(); } _loadData = (page_num=1, page_size=4)=>{ getActivitiesList(page_num, page_size).then((result)=>{ if(result && result.status === 1){ message.success(result.msg); this.setState({ totalSize: result.data.activities_count, activitiesList: result.data.activities_list }) } }).catch(()=>{ message.error('获取活动列表失败!'); }) }; // 列的配置信息 columns = [ {title: 'ID', dataIndex: 'id', key: 'id', width: 50, align: 'center'}, {title: '活动名称', dataIndex: 'activities_name', key: 'activities_name',align: 'center'}, {title: '开始时间', dataIndex: 'activities_time', key: 'activities_time',align: 'center'}, { title: '活动封面', dataIndex: 'activities_img', key: 'activities_img',align: 'center', render: (text, record) => { return ( <img src={config.BASE_URL + record.activities_img} alt="活动封面" width={100}/> ) } }, {title: '活动价格', dataIndex: 'activities_price', key: 'activities_price',align: 'center'}, {title: '活动天数', dataIndex: 'activities_bus_day_id', key: 'activities_bus_day_id',align: 'center'}, { title: '首页焦点', dataIndex: 'is_focus', key: 'is_focus',align: 'center', render: (text, record) => { return ( <Switch checkedChildren="是" unCheckedChildren= "否" disabled={record.focus_img.length === 0} defaultChecked={record.is_focus === 1} onChange={(checked)=>{ setFocusActivities(record.id, checked ? 1 : 0).then((result)=>{ if(result && result.status === 1){ notification["success"]({ message: `活动: ${record.activities_name}`, description: `${checked ? '设置为' : '取消'}焦点活动!` }); } }) }} /> ) } }, { title: '操作', align: 'center', render: (text, record) => { return ( <div> <Button onClick={()=>{ this.props.history.push({ pathname: '/activities/edit-activities', state: { activities: record } }); }}>编辑活动</Button> <Divider type="vertical" /> <Button onClick={()=>{ Modal.confirm({ title: '确认删除吗?', content: '删除此资源,所有关联的内容都会被删除', okText: '确认', cancelText: '取消', onOk: ()=> { deleteActivities(record.id).then(result=>{ if(result && result.status === 1){ message.success(result.msg); this._loadData(); }else { message.error('删除失败!'); } }).catch(()=>{ message.error('删除失败!'); }) } }); }}>删除活动</Button> </div> ) } }, ]; render() { // 添加按钮 let addBtn = ( <Button type={"primary"} onClick={() => { this.props.history.push('/activities/add-activities'); }}> 添加活动 </Button> ); return ( <Card title={"活动列表"} extra={addBtn}> <Table columns={this.columns} dataSource={this.state.activitiesList} rowKey={"id"} pagination={{ total: this.state.totalSize, pageSize: this.state.pageSize, onChange: (pageNum, pageSize)=>{ console.log('需要加载' + pageNum, pageSize); } }} /> </Card> ) } }
XIII、直播课堂管理直线
㈠、新建界面
㈡、路由配置
㈢、静态界面
1、直播课列表
2、添加直播课
㈣、接口处理
1、直播封面图和轮播图接口上传
-
public/uploads/images文件夹下新建live文件夹
-
controller/manageAPI/uploadImg.js中向外暴露新上传接口,并上传到live文件夹
-
该接口依赖于之前封装的图片上传接口与图片上传文件夹
2、获取园区主题、适用人群
3、新增直播课堂
4、获取直播列表
5、设置是否为轮播图
6、删除一个课程
7、编辑一个课程
8、app.js中配置
㈤、前端界面业务实现
1、页面SDK实现
- api目录下新建liveApi.js,存放直播类接口
- 完善SDK
2、业务实现
-
添加直播课界面完善——liveAdd.jsx
import React, {Component} from "react" import {Card, Form, Input, Select, message, Button, DatePicker} from 'antd' import Moment from 'moment' import KaiUploadImg from '../../../components/KaiUploadImg' import {addLive, getLivePerson, getLiveTheme} from '../../../api/liveApi' import {getUser} from '../../../api/adminApi' const {RangePicker} = DatePicker; const {Option} = Select; export default class Resource extends Component { state = { imageUrl: '', // 资源封面 focusImgUrl: '', // 轮播图封面 live_theme: [], // 直播主题数组 live_person: [], // 直播适用人群数组 }; componentDidMount() { // 1. 获取直播适用人群 getLivePerson().then((result) => { if (result && result.status === 1) { this.setState({ live_person: result.data }) } }).catch((error) => { console.log(error); }); // 2. 获取直播主题 getLiveTheme().then((result) => { if (result && result.status === 1) { this.setState({ live_theme: result.data }) } }).catch((error) => { console.log(error); }) } formItemLayout = { labelCol: {span: 3}, wrapperCol: {span: 12}, }; render() { const onFinish = values => { const {imageUrl, focusImgUrl} = this.state; if (!imageUrl) { message.warning('请上传直播课封面!'); return; } // 开始时间和结束时间 const live_begin_time = Moment(values.live_time[0]).format('YYYY-MM-DD HH:mm:ss'); const live_end_time = Moment(values.live_time[1]).format('YYYY-MM-DD HH:mm:ss'); // 调用接口 addLive(getUser().token, values.live_title, values.live_url, values.live_author, values.live_price, imageUrl, live_begin_time, live_end_time, values.live_person_id, values.live_theme_id, focusImgUrl).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('添加直播课失败!'); }) }; const {live_theme, live_person} = this.state; return ( <Card title="新增直播课"> <Form {...this.formItemLayout} onFinish={onFinish}> <Form.Item label="直播课名称" name="live_title" rules={[{required: true, message: '请输入直播课名称!'}]} > <Input/> </Form.Item> <Form.Item label="直播课作者" name="live_author" rules={[{required: true, message: '请输入直播课作者!'}]} > <Input/> </Form.Item> <Form.Item label="直播课价格" name="live_price" rules={[{required: true, message: '请输入直播课价格!'}]} > <Input/> </Form.Item> <Form.Item label="直播课时间" name="live_time" rules={[{required: true, message: '请输入直播课时间!'}]} > <RangePicker showTime/> </Form.Item> <Form.Item label="适用人群" name="live_person_id" rules={[{required: true, message: '请选择适用人群!'}]} > <Select placeholder={"请选择适用人群"} style={{width: 150}}> { live_person.map(item => { return ( <Option value={item.id} key={item.id}>{item["live_person_name"]}</Option> ) }) } </Select> </Form.Item> <Form.Item label="内容主题" name="live_theme_id" rules={[{required: true, message: '请选择内容主题!'}]} > <Select placeholder={"请选择内容主题"} style={{width: 150}}> { live_theme.map(item => { return ( <Option value={item.id} key={item.id}>{item["live_theme_title"]}</Option> ) }) } </Select> </Form.Item> <Form.Item label="直播课地址" name="live_url" rules={[{required: true, message: '请输入直播课地址!'}]} > <Input placeholder="请输入直播课的地址"/> </Form.Item> <Form.Item label="直播课封面图" name="live_img" > <KaiUploadImg upLoadBtnTitle={"上传封面图"} upLoadName={"live_img"} upLoadAction={"/api/auth/live/upload_live"} successCallBack={(name) => { message.success('直播课程封面上传成功!'); this.setState({ imageUrl: name }); }} /> </Form.Item> <Form.Item label="首页轮播图" name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传焦点图"} upLoadName={"live_img"} upLoadAction={"/api/auth/live/upload_live"} successCallBack={(name) => { message.success('直播课焦点图上传成功!'); this.setState({ focusImgUrl: name }); }} /> </Form.Item> <Form.Item wrapperCol={{span: 24}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type="primary" htmlType="submit" style={{marginRight: 10}}> 保存 </Button> <Button onClick={() => { this.props.history.goBack() }}> 取消 </Button> </div> </Form.Item> </Form> </Card> ) } }
-
编辑直播课程界面完善——liveEdit.jsx
import React, {Component} from "react" import {Card, Form, Input, Select, message, Button, DatePicker} from 'antd' import Moment from 'moment' import KaiUploadImg from './../../../components/KaiUploadImg' import {editLive, getLivePerson, getLiveTheme} from '../../../api/liveApi' import {getUser} from '../../../api/adminApi' const {RangePicker} = DatePicker; const {Option} = Select; export default class LiveEdit extends Component { constructor(props) { super(props); this.state = { imageUrl: '', // 资源封面 focusImgUrl: '', // 轮播图封面 live_theme: [], // 直播主题数组 live_person: [], // 直播适用人群数组 live_id: '' // 直播信息id }; // ref去绑定表单 this.liveFormRef = React.createRef(); } componentDidMount() { // 0. 获取上一个界面传递的数据 if (this.props.location.state) { const liveItem = this.props.location.state.live; console.log(liveItem); if (liveItem) { this.liveFormRef.current.setFieldsValue({ live_title: liveItem.live_title, live_author: liveItem.live_author, live_time: [Moment(liveItem.live_begin_time), Moment(liveItem.live_end_time)], live_url: liveItem.live_url, live_price: liveItem.live_price, live_person_id: liveItem.live_person_id, live_theme_id: liveItem.live_theme_id }) } // 封面图/轮播图/直播信息id this.setState({ imageUrl: liveItem.live_img, // 资源封面 focusImgUrl: liveItem.focus_img, // 轮播图封面 live_id: liveItem.id }); } // 1. 获取直播适用人群 getLivePerson().then((result) => { if (result && result.status === 1) { this.setState({ live_person: result.data }) } }).catch((error) => { console.log(error); }); // 2. 获取直播主题 getLiveTheme().then((result) => { if (result && result.status === 1) { this.setState({ live_theme: result.data }) } }).catch((error) => { console.log(error); }) } formItemLayout = { labelCol: {span: 3}, wrapperCol: {span: 12}, }; render() { // 提交修改的内容 const onFinish = values => { const {imageUrl, focusImgUrl, live_id} = this.state; if (!imageUrl) { message.warning('请上传直播课封面!'); return; } // 开始时间和结束时间 const live_begin_time = Moment(values.live_time[0]).format('YYYY-MM-DD HH:mm:ss'); const live_end_time = Moment(values.live_time[1]).format('YYYY-MM-DD HH:mm:ss'); // 调用接口 editLive(getUser().token, live_id, values.live_title, values.live_url, values.live_author, values.live_price, imageUrl, live_begin_time, live_end_time, values.live_person_id, values.live_theme_id, focusImgUrl).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('修改直播课失败!'); }) }; const {live_theme, live_person, imageUrl, focusImgUrl} = this.state; return ( <Card title="编辑直播课"> <Form {...this.formItemLayout} onFinish={onFinish} ref={this.liveFormRef}> <Form.Item label="直播课名称" name="live_title" rules={[{required: true, message: '请输入直播课名称!'}]} > <Input/> </Form.Item> <Form.Item label="直播课作者" name="live_author" rules={[{required: true, message: '请输入直播课作者!'}]} > <Input/> </Form.Item> <Form.Item label="直播课价格" name="live_price" rules={[{required: true, message: '请输入直播课价格!'}]} > <Input/> </Form.Item> <Form.Item label="直播课时间" name="live_time" rules={[{required: true, message: '请输入直播课时间!'}]} > <RangePicker showTime/> </Form.Item> <Form.Item label="适用人群" name="live_person_id" rules={[{required: true, message: '请选择适用人群!'}]} > <Select placeholder={"请选择适用人群"} style={{width: 150}}> { live_person.map(item => { return ( <Option value={item.id} key={item.id}>{item.live_person_name}</Option> ) }) } </Select> </Form.Item> <Form.Item label="内容主题" name="live_theme_id" rules={[{required: true, message: '请选择内容主题!'}]} > <Select placeholder={"请选择内容主题"} style={{width: 150}}> { live_theme.map(item => { return ( <Option value={item.id} key={item.id}>{item.live_theme_title}</Option> ) }) } </Select> </Form.Item> <Form.Item label="直播课地址" name="live_url" rules={[{required: true, message: '请输入直播课地址!'}]} > <Input placeholder="请输入直播课的地址"/> </Form.Item> <Form.Item label="直播课封面图" name="live_img" > <KaiUploadImg upLoadBtnTitle={"上传封面图"} upLoadName={"live_img"} upImage={imageUrl} upLoadAction={"/api/auth/live/upload_live"} successCallBack={(name) => { message.success('直播课程封面上传成功!'); this.setState({ imageUrl: name }); }} /> </Form.Item> <Form.Item label="首页轮播图" name="focus_img" > <KaiUploadImg upLoadBtnTitle={"上传焦点图"} upLoadName={"live_img"} upImage={focusImgUrl} upLoadAction={"/api/auth/live/upload_live"} successCallBack={(name) => { message.success('直播课焦点图上传成功!'); this.setState({ focusImgUrl: name }); }} /> </Form.Item> <Form.Item wrapperCol={{span: 24}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type="primary" htmlType="submit" style={{marginRight: 10}}> 修改 </Button> <Button onClick={() => { this.props.history.goBack() }}> 取消 </Button> </div> </Form.Item> </Form> </Card> ) } }
-
直播课列表界面完善——liveList.jsx
import React, {Component} from "react" import {Card, Button, Table, Switch, Divider, Modal, message, notification} from 'antd' import {getLive, deleteLive, setFocusLive} from '../../../api/liveApi' import config from './../../../config/config' export default class LiveList extends Component { state = { isLoading: false, liveList: [], totalSize: 0, pageSize: 4 }; componentDidMount() { // 加载列表数据 this._loadData(); } _loadData = (page_num = 1, page_size = 4) => { getLive(page_num, page_size).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.setState({ totalSize: result.data.live_count, liveList: result.data.live_list }) } }).catch(() => { message.error('获取直播课程失败!'); }) }; // 列的配置信息 columns = [ {title: 'ID', dataIndex: 'id', key: 'id', align: "center", width: 50}, {title: '直播课标题', dataIndex: 'live_title', key: 'live_title', align: "center",}, { title: '直播课封面', dataIndex: 'live_img', key: 'live_img', align: "center", render: (text, record) => { return ( <img src={config.BASE_URL + record.live_img} alt="课程封面" width={100}/> ) } }, {title: '直播课时间', dataIndex: 'live_begin_time', key: 'live_begin_time', align: "center"}, {title: '直播课老师', dataIndex: 'live_author', key: 'live_author', align: "center"}, {title: '直播课价格', dataIndex: 'live_price', key: 'live_price', align: "center"}, { title: '首页焦点', dataIndex: 'is_focus', key: 'is_focus', align: "center", width: 100, render: (text, record) => { return ( <Switch checkedChildren="是" unCheckedChildren="否" disabled={record.focus_img.length === 0} defaultChecked={record.is_focus === 1} onChange={(checked) => { setFocusLive(record.id, checked ? 1 : 0).then((result) => { if (result && result.status === 1) { notification["success"]({ message: `课程: ${record.live_title}`, description: `${checked ? '设置为' : '取消'}焦点课程!` }); } }) }} /> ) } }, { title: '操作', key: 'action', align: "center", width: 250, render: (text, record) => { return ( <span> <Button onClick={() => { this.props.history.push({ pathname: '/lives/edit-live', state: { live: record } }); }}>编辑</Button> <Divider type="vertical"/> <Button onClick={() => { Modal.confirm({ title: '确认是否删除', content: '删除此课程, 所有关联内容都会被全部删除?', okText: '确认', cancelText: '取消', onOk: () => { deleteLive(record.id).then(result => { if (result && result.status === 1) { message.success(result.msg); this._loadData(); } else { message.error('删除失败!'); } }).catch(() => { message.error('删除失败!'); }) } }); }}>删除</Button> </span> ) } }, ]; render() { // 添加直播课 let addBtn = ( <Button type={"primary"} onClick={() => { this.props.history.push("/lives/add-live"); }}> 添加直播课 </Button>); return ( <Card title={"直播课列表"} extra={addBtn}> <Table dataSource={this.state.liveList} columns={this.columns} rowKey={"id"} bordered={true} loading={this.state.isLoading} pagination={{ total: this.state.totalSize, pageSize: this.state.pageSize, onChange: (pageNum, pageSize) => { console.log("需要加载第" + pageNum, pageSize); } }}/> </Card> ) } }
XIV、首页功能完善
㈠、更新数据表
1、t_activities
- 添加buy_count字段
- 更新数据
2、t_job
- 添加buy_count字段
- 更新数据
3、t_live
- 添加buy_count字段
- 更新数据
4、t_resource
-
添加buy_count字段
-
更新数据
㈡、后台接口配置
1、新建接口文件
2、挂载接口
3、完善路由
-
资源统计
-
购买数量统计
-
图片上传
-
获取网站的配置信息
-
修改网站的配置信息
㈢、前端业务实现
1、页面SDK实现
- 新建src/api/homeApi.js
- 完善Api
2、业务实现
-
sourceCount.jsx 统计资源数量界面完善
import React from 'react' import {Card} from 'antd' import ReactEcharts from 'echarts-for-react' import {getSourceCount} from '../../../../api/homeApi' export default class SourceCount extends React.Component { constructor(props) { super(props); this.state = { data: [] } } componentDidMount() { this._loadData(); } _loadData = () => { getSourceCount().then((result) => { console.log(result); let tempData = []; if (result.status === 1) { for (let k in result.data) { tempData.push(result.data[k]); } // 更新状态机 this.setState({ data: tempData }); } }); }; getOption = () => { return { xAxis: { type: 'category', data: ['幼教', '职场', '活动', '直播'] }, yAxis: { type: 'value' }, series: [{ data: this.state.data, type: 'bar' }] } }; render() { return ( <Card title="各版块资源总数量统计"> <ReactEcharts option={this.getOption()}/> </Card> ) } }
-
buyCount.js 购买数量界面完善
import React from 'react' import {Card} from 'antd' import ReactEcharts from 'echarts-for-react' import {getBuyCount} from '../../../../api/homeApi' export default class BuyCount extends React.Component { constructor(props) { super(props); this.state = { data: [] } } componentDidMount() { this._loadData(); } _loadData = () => { getBuyCount().then((result) => { console.log(result); let tempData = []; if (result.status === 1) { for (let k in result.data) { tempData.push(result.data[k]); } // 更新状态机 this.setState({ data: tempData }); } }); }; getOption = () => { const {data} = this.state; return { tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' }, legend: { orient: 'vertical', left: 'left', data: ['幼教资源', '职场人生', '活动专区', '直播课堂'] }, series: [ { name: '购买数量', type: 'pie', radius: '55%', center: ['50%', '60%'], data: [ {value: data[0], name: '幼教资源'}, {value: data[1], name: '职场人生'}, {value: data[2], name: '活动专区'}, {value: data[3], name: '直播课堂'} ], emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] } }; render() { return ( <Card title="各业务购买数量统计"> <ReactEcharts option={this.getOption()}/> </Card> ) } }
-
通用配置完善
① 新建数据库表DROP TABLE IF EXISTS t_home_message; CREATE TABLEt_home_message ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增ID', site_name VARCHAR ( 255 ) COMMENT '网站标题', site_keyword VARCHAR ( 255 ) COMMENT '网站关键字', site_des VARCHAR ( 255 ) COMMENT '网站描述', site_logo VARCHAR ( 255 ) COMMENT '网站LOGO', site_copy VARCHAR ( 255 ) COMMENT '版权信息', site_bei VARCHAR ( 255 ) COMMENT '备案号' ) COMMENT '网站信息表'
② homeCommon.jsx
import React from 'react' import {Card, Form, Input, Button, message,} from 'antd' import {getWebSiteMsg, editWebSiteMsg} from "../../../api/homeApi"; import {getUser} from "../../../api/adminApi"; import KaiUploadImg from "../../../components/KaiUploadImg"; const {TextArea} = Input; export default class HomeCommon extends React.Component { constructor(props) { super(props); this.state = { siteLogoUrl: '' }; this.homeFormRef = React.createRef(); } componentDidMount() { // 1. 获取网站的配置信息 getWebSiteMsg().then((result) => { if (result && result.status === 1) { console.log(result.data[0]); const homeItem = result.data[0]; if (homeItem) { this.homeFormRef.current.setFieldsValue(homeItem); this.setState({ siteLogoUrl: homeItem.site_logo }) } } }).catch((error) => { console.log(error); }); } render() { const formItemLayout = { labelCol: { xs: {span: 3} }, wrapperCol: { xs: {span: 12} }, }; const onFinish = values => { // 0. 容错 const {siteLogoUrl} = this.state; if (!siteLogoUrl) { message.warning('请上传网站Logo!'); return; } editWebSiteMsg(getUser().token, values.site_name, values.site_keyword, values.site_des, siteLogoUrl, values.site_copy, values.site_bei).then((result) => { if (result && result.status === 1) { message.success(result.msg); this.props.history.goBack(); } }).catch(() => { message.error('修改配置信息失败!'); }) }; const {siteLogoUrl} = this.state; return ( <Card title="修改网站配置信息"> <Form {...formItemLayout} onFinish={onFinish} ref={this.homeFormRef}> <Form.Item label={"网站标题"} name="site_name" rules={[{required: true, message: '请输入网站标题!'}]} > <Input placeholder={"请输入网站标题"}/> </Form.Item> <Form.Item label={"关键字"} name="site_keyword" rules={[{required: true, message: '请输入网站关键字!'}]} > <TextArea placeholder={"请输入网站关键字"} rows={2}/> </Form.Item> <Form.Item label={"描述"} name="site_des" rules={[{required: true, message: '请输入网站描述!'}]} > <TextArea placeholder={"请输入网站描述"} rows={4}/> </Form.Item> <Form.Item label="网站LOGO" name="site_job" > <KaiUploadImg upLoadBtnTitle={"上传LOGO"} upLoadName={"site_job_img"} upImage={siteLogoUrl} upLoadAction={"/api/auth/home/upload_home"} successCallBack={(name) => { this.setState({ siteLogoUrl: name }) }} /> </Form.Item> <Form.Item label={"版权信息"} name="site_copy" rules={[{required: true, message: '请输入网站版权信息!'}]} > <Input placeholder={"请输入网站版权信息"}/> </Form.Item> <Form.Item label={"备案号"} name="site_bei" rules={[{required: true, message: '请输入网站备案号!'}]} > <Input placeholder={"请输入网站备案号"}/> </Form.Item> <Form.Item wrapperCol={{span: 16}} > <div style={{textAlign: 'center', marginTop: 30}}> <Button type={"primary"} htmlType="submit" style={{marginRight: 15}}> 立即提交 </Button> <Button onClick={() => { this.props.history.goBack() }}>取消</Button> </div> </Form.Item> </Form> </Card> ) } }
③ 结果
四、性能优化
Ⅰ、自动登录过期处理
㈠、服务器端配置
设置不同状态码
㈡、客户端配置
- ajax封装中进行状态码判断
- admin.jsx中订阅消息并返回登录界面
Ⅱ、get请求缓存处理
设置接口相关请求不做重定向,全部200重新请求
只要请求链接不同,就会重新做请求而不重定向。因此,只要在get请求后面拼接一个时间戳即可。
Ⅲ、MVC设计
㈠、后台路由业务抽取
五、额外知识点
Ⅰ、字体图标的使用
㈠、下载图标
进入阿里巴巴矢量图标库下载需要图标
㈡、在项目中引入字体图标
㈢、代码中使用
Ⅱ、子组件中规定接收参数类型
㈠、介绍
子组件中通过prop-types约定参数类型
㈡、安装
安装命令:yarn add prop-types
㈢、使用
Ⅲ、天气预报模块
㈠、接口地址
㈡、key
创建一个web项目申请一下即可
㈢、示例
http://api.map.baidu.com/weather/v1/?district_id=222405&data_type=all&ak=你的key
㈣、代码
_weather() {
const cityId = '110105';
const key = '你的Key';
const url = `/baidu_api/?district_id=${cityId}&data_type=now&ak=${key}`
ajax(url).then((data) => {
let nowWeather = data.result.now;
let notice = nowWeather["text"] + ' ' + nowWeather["temp"] + ' ' + nowWeather["wind_dir"] + ' ' + nowWeather["wind_class"];
let picURL = `http://api.map.baidu.com/images/weather/night/yin.png`; // 目前API已经不支持获取图片了
// 更新状态机
this.setState({picURL, notice})
}).catch((error) => {
message.error('网络异常:' + error)
})
}
Ⅳ、解决跨域
解决方法:借助http-proxy-middleware
解决跨域问题
在客户端解决
㈠、安装
使用命令yarn add http-proxy-middleware
安装
㈡、使用
- src目录下,新建
setupProxy.js
文件夹 - 在文件中进行如下配置
const {createProxyMiddleware} = require('http-proxy-middleware') module.exports = function (app) { app.use( '/baidu_api', createProxyMiddleware({ target: 'http://api.map.baidu.com/weather/v1', changeOrigin: true, pathRewrite: { '^/baidu_api': '/' } }) ) }
- 代码中将
http://api.map.baidu.com/weather/v1
替换为/baidu_api
Ⅴ、小组件中路由跳转
在一些界面的部分组件中,可能需要进行路由跳转,但是组件并不在路由之中,就需要使用withRouter
Ⅵ、敏感信息—md5加密
㈠、概念
- MD5(Message-Digest Algorithm)是计算机安全领域广泛使用的散列函数(又称哈希算法、摘要算法),主要用来确保消息的完整和一致性
- 常见的应用场景有密码保护、下载文件校验等
㈡、应用场景
- 文件完整性校验
比如从网上下载一个软件,一般网站都会将软件的md5值附在网页上,用户下载完软件后,可对下载到本地的软件进行md5运算,然后跟网站上的md5值进行对比,确保下载的软件是完整的(或正确的) - 密码保护
将md5后的密码保存到数据库,而不是保存明文密码,避免拖库等事件发生后,明文密码外泄 - 防篡改
比如数字证书的防篡改,就用到了摘要算法。(当然还要结合数字签名等手段)
㈢、密码保护
- 安全性
将明文密码保存到数据库是很不安全的,最起码也要进行md5后进行保存
比如用户密码是123456
,md5运行后,得到输出:e10adc3949ba59abbe56e057f20f883e
- 好处
① 防内部攻击:网站主人也不知道用户的明文密码,避免网站主人拿着用户明文密码干坏事
② 防外部攻击:如果网站被黑客入侵,黑客也只能拿到md5后的密码,而不是用户的明文密码
㈣、单纯对密码进行md5并不安全
- 当攻击者知道算法是md5, 可以将事先准备好的常见明文密码的md5值来进行匹配暴力破解
- 密码加盐
① 在密码特定位置插入特定字符串后,再对修改后的字符串进行md5运算
② 注意- 同样的密码,当“盐”值不一样时,md5值的差异非常大
- 通过密码加盐,可以防止最初级的暴力破解,如果攻击者事先不知道”盐“值,破解的难度就会非常大
- 密码加盐增强: 随机盐值
㈤、使用
借助blueimp-md5
进行加密
- 安装
npm i blueimp-md5
- 在config/config.js中,定义盐值
- 代码中进行盐值加密
Ⅶ、用户登录信息存储
㈠、客户端
㈡、服务器端
1、验证用户名和密码是否正确
2、如果正确,则登陆成功
-
根据用户信息生成token
① token的主要作用-
公共参数
① 服务器接收到客户端的请求之后,会取出token值与保存在本地(数据库)中的token值做对比
② 如果两个 token 值相同, 说明用户登录成功过, 当前用户处于登录状态。 如果没有这个 token 值, 则没有登录成功
③ 如果 token 值不同: 说明原来的登录信息已经失效, 让用户重新登录 -
token 值有失效时间
① 如果 app 是新闻类/游戏类/聊天类等需要长时间用户粘性的, 一般可以设置1年的有效时间
② 如果 app 是 支付类/银行类的, 一般token只得有效时间比较短: 15分钟左右
③ 每次登录之后, 无论用户密码是否改变, 只要调用登录接口并且登录成功, 都会在服务器生成新的token值, 原来的token值就会失效 -
关于安全性问题
① 问题: 假如我拿到 token 就可以调用服务器端的任何接口?
② 解决方案:- token 劫持,可以校验登陆的 ip 和调用接口时的 ip,如果有不同就要求用户重新登陆
- 使用HTTPS,双向证书校验
③ 注意:token 不保证安全, 保证安全的是 HTTPS
② 安装jsonwebtoken:官方文档
npm install jsonwebtoken
③ 生成token:利用sign()生成tokenconst jwt = require('jsonwebtokens'); let userObj = {name:'张三', id:'3232'}; let secret = '引擎计划'; let token = jwt.sign(userObj,secret); console.log(token)
④ 解码token:利用verify()生成token
let userObj = jwt.verify(token,secret) console.log( userObj)
-
-
把token存入session
① 什么是session- 概念:Session用于记录客户状态的一种机制,不同于Cookie的是,Cookie存储在客户端,而Session则将数据存储在服务器上
- Session用途
① session 运行在服务器端,当客户端第一次访问服务器时,可以将客户的登录信息保存
② 当客户访问其他页面时,可以判断客户的登录状态,做出提示,相当于登录拦截
③ session 可以和 Redis 或者数据库等结合做持久化操作,当服务器挂掉时也不会导致某些客户信息(购物车)丢失 - 工作原理
① 当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于key,value 的键值对
② 将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie),找到对应的 session(value)
③ 最后将存在session中的信息返回到页面上
② 使用
4. 项目中安装express-session和express-mysql-session
npm install express-session --save
:session
npm install express-mysql-session --save
:在MySQL数据库上做数据持久化
5. 代码中使用
进行默认session配置,配置完成后在代码中就可以直接使用了。
① 引入express-session
javascript var session = require('express-session');
② 把session信息存入数据库中
javascript var MySQLStore = require('express-mysql-session')(session);
③ 中间件使用session
javascript app.use(session({ //参数配置 key: 'yqPlan', secret: 'itLike',//加密字符串 resave: false, //强制保存session,即使它没有变化 saveUninitialized: true,//强制将未初始化的session存储。当新建一个session且未设定属性或值时,它就处于未初始化状态。在设定cookie前,这对于登录验证,减轻服务器存储压力,权限控制是有帮助的,默认为true cookie: {maxAge: 24 * 3600 * 1000}, rolling: true, //在每次请求时进行设置cookie,将重置cookie过期时间 store: sessionStore }));
④ 设置session
javascript req.session.token = 'itlike'
⑤ 获取session
javascript req.session.token
-
返回token给客户端
$.ajax({ url: 'http://localhost:3000/api/auth/user/login', type: 'post', data: `user_name=${user_name}&user_pwd=${md5_user_pwd}`, success: function (msg) { if(msg.status === 1){ //登录成功 // 把token存在本地 localStorage.setItem(msg.data.user_name, msg.data.token); // 跳转到首页 window.location.href = '/back'; }else { // 登录失败 alert('登录失败, 请检查用户名和密码是否正确!'); // 清空输入框 $('#user_name').val(''); $('#user_pwd').val(''); } } });
3、销毁session
- 方式1:设置过期时常为0
// 1. 方式1:设置过期时常为 0 req.session.cookie.maxAge = 0;
- 方式2:直接调用destory()方法
// 2. 方式2:直接调用destroy()方法 req.session.destroy();
㈢、测试
用户登录信息存储、权限控制见Ⅷ、权限控制 ㈢、测试
Ⅷ、权限控制
㈠、要求
- 所有后端的业务接口,都必须要登录之后才可以访问
- 没有登录统一返回没有足够权限
㈡、技术手段
全局中间件
-
在middleWare文件夹下新建authControl.js文件
-
在app.js中配置中间件,意味着所有的get、post请求都会经过该中间件
-
实现代码
① 在app.js中,设置了后端接口以/api/auth/
开头所以可以根据是否以/api/auth/
开头判断是否为后端接口② 不需要登录就可以访问的接口要做单独的匹配验证,比如登录、注册接口,如果匹配到直接放行
③ 根据session中是否有token来判断用户是否登录过。具体的token值相等的判断可以放到接口中实现。(也可以权限集中,都在这里面做)
④ 如果处于登录状态,需要进行判断是是请求的接口还是界面
⑤ 其他情况就报错
⑥ 完整代码module.exports = (req, res, next) => { const path = req.path; // 1. 验证是否是后端接口,所有非后端接口会跳过 后端接口均已/api/auth开头 if (path && path.indexOf('/api/auth/') === -1) { return next(); } // 2. 所有后端接口(不需要登录的接口需要进行放行) if ( path.indexOf('/api/auth/admin/login') !== -1 || // 登录 path.indexOf('/api/auth/admin/reg') !== -1 // 注册 ) { return next(); } // 3. 判断是否处于登录状态 if (req.session.token) { return next(); } // 4. 没有登录 // 4.1 如果是接口相关 if (path.indexOf('/api/auth') !== -1) { return res.json({ status: 0, msg: '非法访问,没有权限' }) } // 4.2 其他情况 return next(new Error('非法访问')); };
㈢、注册、登录、退出测试
1、注册
- 使用接口
http://localhost:5000/api/auth/admin/reg
注册用户
2、登录
- 查看数据库中sessions数据表
- 使用接口
http://localhost:5000/api/auth/admin/login
登录 - 再次查看sessions数据表
3、退出
- 使用接口
http://localhost:5000/api/auth/admin/logout
退出 - 查看sessions数据表
Ⅸ、客户端接口路径配置
㈠、公共路径抽离
- 在config文件夹下新建config/config.js文件
- 配置config.js
㈡、代码中使用
Ⅹ、数据本地缓存持久化
㈠、说明
借助store.js可以帮助我们实现本地缓存。
store.js兼容高版本浏览器和低版本浏览器,自动帮我们选择localStorage and sessionStorage或者低版本浏览器的方法。
官方文档
㈡、使用
-
安装
yarn add store
-
封装cache-tool本地缓存工具。
好处:① 使用方便 ② 如果以后修改缓存类库,直接修改这里就可以。
-
使用举例——保存
Ⅺ、文件上传
㈠、两个文件上传库
㈡、单文件上传实操
以头像单文件上传为例:使用antd中的upload组件,借助multer组件
1、安装
使用命令yarn add multer
安装
2、封装接口
因为文件上传操作在很多地方都用得到,所以可以封装成通用接口。
- 在controller文件夹下新建controller/manageAPI/uploadImg.js
- 完成上传方法
3、使用
- 后端接口配置
- 前端调用接口
㈢、多文件上传实操
1、Upload.Dragger
2、服务端:多文件上传接口
3、客户端:业务实现
- 界面中使用状态机存储上传的文件
- onChange上传事件中更新状态机
- onRemove删除事件中更新状态机
Ⅻ、组件中获取antd组件
方法:通过ref绑定
- 定义ref
- 绑定ref
- 使用
XIII、侧边导航栏完善
㈠、刷新默认展开
使用defaultOpenKeys
控制默认展开的导航栏
㈡、二级路由选中丢失问题
上面为一级路由路径,下面为二级路由路径。只要将一级路由路径截取出来进行匹配即可
XIV、富文本编辑器
㈠、官方推荐使用:braft-editor
import React from 'react'
// 引入编辑器组件
import BraftEditor from 'braft-editor'
// 引入编辑器样式
import 'braft-editor/dist/index.css'
export default class RichTextEdit extends React.Component {
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(null)
};
submitContent = async () => {
// 在编辑器获得焦点时按下ctrl+s会执行此方法
// 编辑器内容提交到服务端之前,可直接调用editorState.toHTML()来获取HTML格式的内容
};
handleEditorChange = (editorState) => {
this.setState({ editorState })
};
render () {
const { editorState } = this.state;
return (
<div className="my-component">
<BraftEditor
value={editorState}
style={{border: '1px solid lightgray'}}
onChange={this.handleEditorChange}
onSave={this.submitContent}
/>
</div>
)
}
}
㈡、外界调用
1、引入
2、挂载
㈢、图片上传优化
1、原因
braft-editor在上传图片的时候默认是将图片转换为base64编码,这显然无法很好的存储到服务器数据库中,因此需要自己进行优化
2、方法
结合Ant Design上传组件,借鉴普通图片上传的方式,以formdata的形式上传到服务器,返回图片地址,进行显示存储。
安装braft-utils:yarn add braft-utils
3、代码
import React from 'react'
// 引入编辑器组件
import BraftEditor from 'braft-editor'
// 引入编辑器样式
import 'braft-editor/dist/index.css'
import {ContentUtils} from 'braft-utils'
import {Upload} from 'antd'
import PropTypes from 'prop-types'
import config from './../config/config'
export default class RichTextEdit extends React.Component {
static propTypes = {
uploadName: PropTypes.string.isRequired, // 上传的key
uploadAction: PropTypes.string.isRequired, // 上传图片的接口地址
htmlContent: PropTypes.string
};
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.htmlContent) {
this.setState({
editorState: BraftEditor.createEditorState(nextProps.htmlContent)
})
}
}
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(null)
};
getContent = () => {
return this.state.editorState.toHTML();
};
submitContent = async () => {
// 在编辑器获得焦点时按下ctrl+s会执行此方法
// 编辑器内容提交到服务端之前,可直接调用editorState.toHTML()来获取HTML格式的内容
console.log(this.state.editorState.toHTML());
};
handleEditorChange = (editorState) => {
this.setState({editorState})
};
editorControls = [
'undo', 'redo', 'separator',
'font-size', 'line-height', 'letter-spacing', 'separator',
'text-color', 'bold', 'italic', 'underline', 'strike-through', 'separator',
'superscript', 'subscript', 'remove-styles', 'emoji', 'separator', 'text-indent', 'text-align', 'separator',
'headings', 'list-ul', 'list-ol', 'blockquote', 'code', 'separator',
'link', 'separator', 'hr',
'clear'
];
uploadHandler = (info) => {
if (info.file.status === 'uploading') {
return;
}
// 获取服务器返回的数据
if (info.file.response && info.file.status === 'done' && info.file.response.status === 1) {
const name = info.file.response.data.name;
this.setState({
editorState: ContentUtils.insertMedias(this.state.editorState, [{
type: 'IMAGE',
url: config.BASE_URL + name
}])
})
}
};
extendControls = [
{
key: 'antd-uploader',
type: 'component',
component: (
<Upload
name={this.props.uploadName}
accept="image/*"
action={this.props.uploadAction}
showUploadList={false}
onChange={this.uploadHandler}
>
<button type="button" className="control-item button upload-button" data-title="插入图片">
插入图片
</button>
</Upload>
)
}
];
render() {
const {editorState} = this.state;
return (
<div className="my-component">
<BraftEditor
value={editorState}
controls={this.editorControls}
style={{border: '1px solid lightgray'}}
onChange={this.handleEditorChange}
onSave={this.submitContent}
extendControls={this.extendControls}
/>
</div>
)
}
}
更改后样式
XV、自己封装组件
㈠、封装图片上传组件
1、新建组件文件
在components中,新建KaiUploadImg.jsx
2、找基础组件
在antd中找原型组件
3、封装修改基础代码
将其代码拷贝到jsx中并进行修改
import React from 'react'
import {Upload, message} from 'antd';
import {LoadingOutlined, PlusOutlined} from '@ant-design/icons';
import PropTypes from 'prop-types'
import config from './../config/config'
function beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('上传图片格式不正确!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('上传图片大小不能超过2MB!');
}
return isJpgOrPng && isLt2M;
}
class KaiUploadImg extends React.Component {
static propTypes = {
upLoadBtnTitle: PropTypes.string.isRequired, // 上传图片的按钮标题
upLoadName: PropTypes.string.isRequired, // 上传图片的key
upLoadAction: PropTypes.string.isRequired, // 上传图片的接口地址
uploadImg: PropTypes.string, // 如果有默认值
successCallBack: PropTypes.func.isRequired
};
static defaultProps = {
upLoadBtnTitle: '上传图片'
}
state = {
loading: false,
imageUrl: ''
};
componentWillReceiveProps(nextProps, nextContext) {
this.setState({
imageUrl: nextProps.uploadImg
})
}
handleChange = info => {
if (info.file.status === 'uploading') {
this.setState({loading: true});
return;
}
// 获取服务器返回的数据
if (info.file.response && info.file.status === 'done' && info.file.response.status === 1) {
const name = info.file.response.data.name;
// 把结果给调用者返回
this.props.successCallBack(name);
this.setState({
imageUrl: name,
loading: false
});
}
};
render() {
const {upLoadBtnTitle, upLoadName, upLoadAction} = this.props;
const {imageUrl} = this.state;
console.log(imageUrl);
const uploadButton = (
<div>
{this.state.loading ? <LoadingOutlined/> : <PlusOutlined/>}
<div className="ant-upload-text">{upLoadBtnTitle}</div>
</div>
);
return (
<Upload
name={upLoadName}
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action={upLoadAction}
beforeUpload={beforeUpload}
onChange={this.handleChange}
>
{imageUrl ? <img src={config.BASE_URL + imageUrl} alt="avatar" style={{width: '100%'}}/> : uploadButton}
</Upload>
);
}
}
export default KaiUploadImg;
4、外界调用
- 引入
- 使用
- 效果:
㈡、标签组件
1、定义
2、使用
3、优化
import React from 'react'
import {Tag, Input} from 'antd';
import {TweenOneGroup} from 'rc-tween-one';
import {PlusOutlined} from '@ant-design/icons';
import PropTypes from 'prop-types'
export default class KaiTag extends React.Component {
state = {
tags: [], // 标签数组
inputVisible: false, // 控制输入框的显示和隐藏
inputValue: '', // 输入框的内容
};
static propTypes = {
tagsCallBack: PropTypes.func.isRequired,
tagsArr: PropTypes.array
};
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.tagsArr) {
this.setState({
tags: nextProps.tagsArr
})
}
}
handleClose = removedTag => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
this.setState({tags});
// 返回标签数组
this.props.tagsCallBack(tags);
};
showInput = () => {
this.setState({inputVisible: true}, () => this.input.focus());
};
handleInputChange = e => {
this.setState({inputValue: e.target.value});
};
handleInputConfirm = () => {
const {inputValue} = this.state;
let {tags} = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
this.setState({
tags,
inputVisible: false,
inputValue: '',
});
// 返回标签数组
this.props.tagsCallBack(tags);
};
saveInputRef = input => (this.input = input);
forMap = tag => {
const tagElem = (
<Tag
closable
onClose={e => {
e.preventDefault();
this.handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{display: 'inline-block'}}>
{tagElem}
</span>
);
};
render() {
const {tags, inputVisible, inputValue} = this.state;
const tagChild = tags.map(this.forMap);
return (
<div>
<div>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{opacity: 0, width: 0, scale: 0, duration: 200}}
appear={false}
>
{tagChild}
</TweenOneGroup>
</div>
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{width: 78}}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag onClick={this.showInput} className="site-tag-plus">
<PlusOutlined/> 新的活动标签
</Tag>
)}
</div>
);
}
}
XVI、antd语言改为中文版
- 安装moment.js
yarn add moment
- 在index.js中进行配置:引入
ConfigProvider
包裹在App外层
XVII、antd表单上传
XVIII、antd表单赋值
使用ref通过setFieldsValue
进行赋值
XIX、不同组件界面之间数据传递
-
使用state
① 传递
② 读取 -
存储在本地
-
redux
经验
- 数据库中图片地址不要带服务器IP和端口号,万一如果更换服务器或者端口号换起来就比较麻烦。只需要存放路径,前缀以config.BASE_URLD的形式加载。
- 后端写好接口后,前端专门写好SDK文件,然后直接在界面中调用接口即可,不要再在界面中实现复杂逻辑。
- 当从一个界面直接返回到之前的界面的时候,要将界面组件的state全部置空,否则会造成内存泄露的问题。