# react_blog
**Repository Path**: mus-z/react_blog
## Basic Information
- **Project Name**: react_blog
- **Description**: No description available
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-05-24
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 个人技术博客实战--react_blog
#### [跟随技术胖老师的步伐](https://www.bilibili.com/video/BV1CJ411377B?p=2)--React、React hooks、Next.js、Antd、Koa2->egg.js、mysql、axios、marked+highlight.js(取代React-markdown)、react-router-dom、nginx

[TOC]
### day01 初始化+前端布局结束
#### 1.1环境搭建
项目初始化,使用`npx create-next-app blog`
引入`@zeit/next-css`使用并且配置根目录下`next.config.js`
想使用Icon单独引入`yarn add @ant-design/icons --save`
```js
const withCss = require('@zeit/next-css')
if(typeof require !== 'undefined'){
require.extensions['.css']=file=>{}
}
module.exports = withCss({})
```
引入`babel-plugin-import`,根目录下`.babelrc`
```json
{
"presets":["next/babel"], //Next.js的总配置文件,相当于继承了它本身的所有配置
"plugins":[ //增加新的插件,这个插件就是让antd可以按需引入,包括CSS
[
"import",
{
"libraryName":"antd"
,
"style":"css"
////取消按需加载才能build
}
]
]
}
```

#### 1.2博客头部header
栅格化布局 UI组件中已经都封装好了,原理也很简单是24网格式,可以配置响应式


预览一下组件

#### 1.3两栏布局
同样运用栅格化布局
```js
import Head from 'next/head'
import Header from '../component/Header'
import { Row, Col, Menu, } from "antd";
export default function Home() {
return (
)
}
```



#### 1.4 List博客列表
通过antd里的`List`组件和各种内置的icon可以很简单配置
```js
最新日志
}//表头
dataSource={mylist}//数据源数组
// loading={true}
itemLayout={"vertical"}//文字竖直方向
renderItem={(item) => (//渲染每一项
{item.title}
{"2020-5-24"}
{"测试文章"}
{"12"}
{item.context}
)}
/>
```

#### 1.5 右侧作者栏
单独写一个Author组件
```js
const Author = () => {
return (
{"2020开始看前端知识的,想找工作的17级萌新。现在已经学习html、css、javascript、es6语法、react、redux等的比较入门的内容,现在在学习实战项目知识orzzz"}
平台
);
};
```


#### 1.6 右侧作者栏下面的功能栏(广告为例)
```js
const Advert = () => {
return (
我觉得这个栏目在没有广告就隐藏吧,或者用于其他功能
);
};
export default Advert
```


#### 1.7 Footer
```js
const Footer=()=>{
return (
)
}
export default Footer
```

#### 1.8 AList
目前基本上和index差不多
然后跟着实现了一个面包屑导航
```js
首页
列表
2
3
```

#### 1.9 详情页-基本页面构造
```js
import Head from 'next/head'
import Header from '../component/Header'
import { Row, Col,Breadcrumb } from "antd";
import {CalendarOutlined,FolderOpenFilled,EyeFilled} from "@ant-design/icons";
import "../public/css/pages/common.css"
import "../public/css/pages/detail.css"
import Author from "../component/Author";
import Advert from "../component/Advert";
import Footer from "../component/Footer";
function Detail() {
return (
detail
xxxxxx项目
{"2020-05-24"}
{"类型"}
{"12"}人
markdown解析内容
)
}
export default Detail
```


#### 1.10 详情页-react-markdown解析
[https://github.com/rexxars/react-markdown](https://github.com/rexxars/react-markdown)
```
yarn add react-markdown
```
```javascript
import ReactMarkdown from "react-markdown"
let markdown='#\n # P01:课程介绍和环境搭建\n' +
'[ **M** ] arkdown + E [ **ditor** ] = **Mditor** \n' +
'> Mditor 是一个简洁、易于集成、方便扩展、期望舒服的编写 markdown 的编辑器,仅此而已... \n\n' +
'**这是加粗的文字**\n\n' +
'*这是倾斜的文字*`\n\n' +
'***这是斜体加粗的文字***\n\n' +
'~~这是加删除线的文字~~ \n\n'+
'\`console.log(111)\` \n\n'+
'# p02:来个Hello World 初始Vue3.0\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n'+
'***\n\n\n' +
'# p03:Vue3.0基础知识讲解\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n\n'+
'# p04:Vue3.0基础知识讲解\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n\n'+
'# p05:Vue3.0基础知识讲解\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n\n'+
'# p06:Vue3.0基础知识讲解\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n\n'+
'# p07:Vue3.0基础知识讲解\n' +
'> aaaaaaaaa\n' +
'>> bbbbbbbbb\n' +
'>>> cccccccccc\n\n'+
'```var a=11; ```'
```
```javascript
markdown解析内容
```


#### 1.11 详情页-markdown-navbar导航
```js
yarn add markdown-navbar
```
markdown-navbar有个小bug 会自己不显示第一个目录`ordered={false}`的时候
所以前面要加一个空换行
```js
```
加绑定位置用antd的`Affix` 给一个fix状态时的top10
```js
```

### day02 中台搭建接口
#### 2.1安装egg.js
[https://github.com/eggjs/egg](https://github.com/eggjs/egg)
```
cnpm install -g egg-init
```
```
cd service
```
```
egg-init --type=simple
```
```
npm install
```
缓慢的下载过程之后

```
yarn dev
```
或
```
npm run dev
```
运行一下


#### 2.2 egg.js 目录结构+规范

主要app编写、config配置


尝试配置路由地址


#### 2.3 RESTful风格+路由配置

> 前台会访问controller里面的default,然后配置这个home.js 中的 index方法
>
> 
> 新建router文件夹分别对应前台的default和后台的admin
>
> 
> 在主router里面用require方法引入 ./router/default
>
> 

#### 2.4 egg-mysql
安装mysql插件
```
yarn add egg-mysql
```
配置插件就需要配置config
找到config/plugin.js
```js
exports.mysql={
enable:true,
packgae:'egg-mysql'
}
```
我跟视频着装了个phpstudy,虽然电脑里面本来就有mysqlworkbench

然后转入config.default.js
查看[https://www.npmjs.com/package/egg-mysql](https://www.npmjs.com/package/egg-mysql)配置
添加到config中,然后修改数据库配置
```js
config.mysql = {
// database configuration
client: {
// host
host: 'localhost',
// port
port: '3306',
// username
user: 'root',
// password
password: 'mysql57',
// database
database: 'react_blog',
},
// load into app, default is open
app: true,
// load into agent, default is close
agent: false,
};
```
在home.js中修改

```js
let result=await this.app.mysql.get("blog_content",{})
console.log(result)
ctx.body = result;
```
可以查到我们连接 的数据库中的blog_content表中的默认数据
#### 2.5 配置数据库并查询

default/home.js
```js
async getArticleList() {
let sql=`SELECT article.id as id ,`+
`article.article_title as title ,`+
`article.article_content as content ,`+
`article.article_introduce as introduce ,`+
`article.article_addtime as addtime ,`+
`article.article_viewcount as viewcount ,`+
`type.typeName as typename ,`+
`type.orderNum as ordernum `+
`FROM article LEFT JOIN type ON article.type_id=type.Id`
//console.log(sql)
const { ctx } = this;
const results= await this.app.mysql.query(sql);//查询语句
ctx.body = {data:results};
```
router/default.js
```js
router.get('/default/getArticleList',controller.default.home.getArticleList)
```
尝试获取数据

### day03 前中结合
#### 3.1 首页用axios获取中台数据
```js
Home.getInitialProps=async()=>{
let promise=new Promise((resolve,reject)=>{
let url='http://127.0.0.1:7001/default/getArticleList'//egg.js中配置好的RESTful接口
try{axios(url).then(
(res)=>{
console.log(res.data)
resolve(res.data)
}
)
}catch{
reject('err')
}
})
return await promise;
//需要返回promise类型对象
}
```


#### 3.2 详细页拿数据+解决跨域(egg-cors
设置文本标题链接
```js
```
尝试获取



但是不让跨域了2333

所以要使用`egg-cors`,在service中安装
```
yarn add egg-cors
```
修改config.js

然后的config.default.js

对于主页的get请求,detail接收到上下文参数id

修改一下之前的动态路由让接口方法能拿到id
```js
router.get('/default/getArticleById/:id',controller.default.home.getArticleById)
```

请求到数据输出到控制台了,但是还没改到文章中
#### 3.3 重构markdown,高亮(marked+highlight)
```
yarn add highlight.js
yarn add marked
```

```js
const renderer = new marked.Renderer();
marked.setOptions({
renderer: renderer,
gfm: true,
pedantic: false,
sanitize: false,
tables: true,
breaks: false,
smartLists: true,
smartypants: false,
highlight: function (code) {
return hljs.highlightAuto(code).value;
}
});
let html = marked(props.article_content)
```
- renderer: 这个是必须填写的,你可以通过自定义的`Renderer`渲染出自定义的格式
- gfm:启动类似Github样式的Markdown,填写true或者false
- pedatic:只解析符合Markdown定义的,不修正Markdown的错误。填写true或者false
- sanitize: 原始输出,忽略HTML标签,这个作为一个开发人员,一定要写flase
- tables: 支持Github形式的表格,必须打开gfm选项
- breaks: 支持Github换行符,必须打开gfm选项,填写true或者false
- smartLists:优化列表输出,这个填写ture之后,你的样式会好看很多,所以建议设置成ture
- highlight: 高亮显示规则 ,这里我们将使用highlight.js来完成
** 增加Code的高亮显示 **
在设置`setOptions`属性时,可以直接设置高亮显示,代码如下:
```js
highlight: function (code) {
return hljs.highlightAuto(code).value;
}
```
设置完成后,你在浏览器检查代码时就可以出现hljs的样式,说明你的效果加成功了,实现了高亮显示代码。
这个配置完成后,就可以连接到数据库了。
改一下数据库中的content
```
# 创建数据库
现在我们还没有数据库,所以需要先建立数据库,直接使用PhP Study里的SQL_Front5.3来管理数据,如果你没有安装需要安装一下,安装完成后点后面的管理按钮,就可以管理了。(这个过程看视频吧)
输入你数据库的用户名和密码,然后点击进入。
进入后新建一个数据库react_blog,这个名字你可以自己起\n新建一个表`blog_content,`字段就是`title`、`type`、`introduce`和`content`\n随便写条数据进去,这个自由发挥吧
这样数据库的准备就写好了,接下来需要验证一下,数据库是否已经连接上了。
```
highlight: function (code) {
return hljs.highlightAuto(code).value;
}
```
```
这里技术胖老师更新了下本页的css样式,我也打算现在先复制粘贴,等之后有时间自己弄一下喜欢的风格
detail.js
```js
import Head from "next/head";
import Header from "../component/Header";
import { Row, Col, Breadcrumb, Affix } from "antd";
import {
CalendarOutlined,
FolderOpenFilled,
EyeFilled,
} from "@ant-design/icons";
import "../public/css/pages/common.css";
import "../public/css/pages/detail.css";
import Author from "../component/Author";
import Advert from "../component/Advert";
import Footer from "../component/Footer";
import MarkNav from "markdown-navbar";
import "markdown-navbar/dist/navbar.css";
import axios from "axios";
import marked from "marked";
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
function Detail(props) {
console.log(props)
const renderer= new marked.Renderer()
marked.setOptions({
renderer:renderer,
gfm:true,
pedantic:false,//false容错
sanitize:false,//false不忽略html标签
tables:true,//gfm
breaks:false,//gfm
smartypants: false,
smartLists:true,
highlight:function(code){return hljs.highlightAuto(code).value}
})
let html=marked (props.content)
//console.log(html,props.content)
return (
detail
{props.title}
{new Date(Number(props.addtime)).toLocaleString()}
{props.typename}
{props.viewcount}人
);
}
Detail.getInitialProps=async(context)=>{
//console.log(context)
let id=context.query.id;
let promise=new Promise((resolve,reject)=>{
let url=`http://127.0.0.1:7001/default/getArticleById/${id}`//egg.js中配置好的RESTful接口
try{axios(url).then(
(res)=>{
console.log(res.data.data[0])
resolve(res.data.data[0])
}
)
}catch{
reject('err')
}
})
return await promise;
//需要返回promise类型对象
}
export default Detail;
```

那个error是navbar有点问题
#### 3.4 重构导航栏
> 插件tocify.tsx
>
> ```js
> import React from 'react';
> import { Anchor } from 'antd';
> import { last } from 'lodash';
>
> const { Link } = Anchor;
>
> export interface TocItem {
> anchor: string;
> level: number;
> text: string;
> children?: TocItem[];
> }
>
> export type TocItems = TocItem[]; // TOC目录树结构
>
> export default class Tocify {
> tocItems: TocItems = [];
>
> index: number = 0;
>
> constructor() {
> this.tocItems = [];
> this.index = 0;
> }
>
> add(text: string, level: number) {
> const anchor = `toc${level}${++this.index}`;
> const item = { anchor, level, text };
> const items = this.tocItems;
>
> if (items.length === 0) { // 第一个 item 直接 push
> items.push(item);
> } else {
> let lastItem = last(items) as TocItem; // 最后一个 item
>
> if (item.level > lastItem.level) { // item 是 lastItem 的 children
> for (let i = lastItem.level + 1; i <= 2; i++) {
> const { children } = lastItem;
> if (!children) { // 如果 children 不存在
> lastItem.children = [item];
> break;
> }
>
> lastItem = last(children) as TocItem; // 重置 lastItem 为 children 的最后一个 item
>
> if (item.level <= lastItem.level) { // item level 小于或等于 lastItem level 都视为与 children 同级
> children.push(item);
> break;
> }
> }
> } else { // 置于最顶级
> items.push(item);
> }
> }
>
> return anchor;
> }
>
> reset = () => {
> this.tocItems = [];
> this.index = 0;
> };
>
> renderToc(items: TocItem[]) { // 递归 render
> return items.map(item => (
>
> {item.children && this.renderToc(item.children)}
>
> ));
> }
>
> render() {
> return (
>
> {this.renderToc(this.tocItems)}
>
> );
> }
> }
> ```
还未开源

```
yarn add lodash
```

引入之后
```js
renderer.heading = function (text, level, raw) {
const anchor = tocify.add(text, level);
//自定义bar的渲染方式
return `${text}\n`;
};
```
为了方便观察对比
整个detail.js代码
```js
import Head from "next/head";
import Header from "../component/Header";
import { Row, Col, Breadcrumb, Affix } from "antd";
import {
CalendarOutlined,
FolderOpenFilled,
EyeFilled,
} from "@ant-design/icons";
import "../public/css/pages/common.css";
import "../public/css/pages/detail.css";
import Author from "../component/Author";
import Advert from "../component/Advert";
import Footer from "../component/Footer";
import MarkNav from "markdown-navbar";
import "markdown-navbar/dist/navbar.css";
import axios from "axios";
import marked from "marked";
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
import Tocify from "../component/tocify.tsx";
function Detail(props) {
console.log(props);
const tocify = new Tocify();
const renderer = new marked.Renderer();
renderer.heading = function (text, level, raw) {
const anchor = tocify.add(text, level);
//自定义bar的渲染方式
return `${text}\n`;
};
marked.setOptions({
renderer: renderer,
gfm: true,
pedantic: false, //false容错
sanitize: false, //false不忽略html标签
tables: true, //gfm
breaks: false, //gfm
smartypants: false,
smartLists: true,
highlight: function (code) {
return hljs.highlightAuto(code).value;
},
});
let html = marked(props.content);
//console.log(html,props.content)
return (
detail
{props.title}
{new Date(Number(props.addtime)).toLocaleString()}
{props.typename}
{props.viewcount}人
文章目录
{tocify && tocify.render()}
);
}
Detail.getInitialProps = async (context) => {
//console.log(context)
let id = context.query.id;
let promise = new Promise((resolve, reject) => {
let url = `http://127.0.0.1:7001/default/getArticleById/${id}`; //egg.js中配置好的RESTful接口
try {
axios(url).then((res) => {
console.log(res.data.data[0]);
resolve(res.data.data[0]);
});
} catch {
reject("err");
}
});
return await promise;
//需要返回promise类型对象
};
export default Detail;
```

这效果是真心不错的orz,连分级和滚动动画都加进来了(但是这个滚动绑定是咋实现的)
### day04 继续前中
#### 4.1 配置url接口+Header链接

把接口集中在apiUrl文件中
引入后调用,如下图

配置新的接口流程(用来生成Header的link)
·方法`app/controller/default/home.js`实现

·路由`app/router/default.js`导出(通过router.js出去的)

·配置前端的自定义路由接口

之后在Header中使用,模拟生命周期的didmount

然后map循环生成组件

#### 4.2 list页面使用传递的数据
配置新的接口

过程略
list中获取传参


下面的内容也是根据之前的静态页面做修改即可



#### 4.3 让首页和列表页也都支持markdown解析预览
根据detail页面
引入
```js
import marked from "marked";
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
```
然后复制marked初始化部分
```js
const renderer = new marked.Renderer();
marked.setOptions({
renderer: renderer,
gfm: true,
pedantic: false, //false容错
sanitize: false, //false不忽略html标签
tables: true, //gfm
breaks: false, //gfm
smartypants: false,
smartLists: true,
highlight: function (code) {
return hljs.highlightAuto(code).value;
},
});
//marked(data);
```
在下面注入
```js
```

同理改到list页面中
不过这里还是通过封装函数的方式吧

```js
import marked from "marked";
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
export default function (md) {
const renderer = new marked.Renderer();
marked.setOptions({
renderer: renderer,
gfm: true,
pedantic: false, //false容错
sanitize: false, //false不忽略html标签
tables: true, //gfm
breaks: false, //gfm
smartypants: false,
smartLists: true,
highlight: function (code) {
return hljs.highlightAuto(code).value;
},
});
return marked(md);
}
```


直接引入即可,这时候发现css样式有点不对,
我把`detail.css`中关于code的样式放到`common.js`中就可以了
index中也可以用这个封装的函数得到解析之后的字符串
不过detail中不行,因为多了一个封装的bar导航,所以还是得单独写在detail中
### day05 后台初始化+添加页面
#### 5.1 后台项目初始化
用 create-react-app和antd作为项目结构
#### 5.2 加路由
安装 react-router-dom


#### 5.3登录card框
```js
import React, { useState } from "react";
import "antd/dist/antd.css";
import { Card, Input, Button, Spin } from "antd";
import { UserOutlined ,KeyOutlined} from "@ant-design/icons";
import "../css/Login.css"
function Login() {
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setInLoading] = useState(false);
const checkLogin=()=>{
setInLoading(true)
setTimeout(()=>{setInLoading(false)},1000)
}
return (
}
onChange={(e) => {
setUserName(e.target.value);
}}
/>
}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
);
}
```
#### 5.4 使用antd的layout侧栏布局
[https://ant.design/components/layout-cn/](https://ant.design/components/layout-cn/)
改写成hooks形式
```js
import React, { useState } from "react";
import { Layout, Menu, Breadcrumb } from "antd";
import {
DesktopOutlined,
PieChartOutlined,
FileOutlined,
CommentOutlined,
UserOutlined,
} from "@ant-design/icons";
import "../css/AdminIndex.css";
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
function AdminIndex() {
let [collapsed, setCollapsed] = useState(false);
const onCollapse = (collapsed) => {
console.log(collapsed);
setCollapsed(collapsed);
};
return (
<>
博客管理系统
工作台
工作台的内容
>
);
}
export default AdminIndex;
```

#### 5.5 添加文章页面
示意图

配置router

然后使用栅格布局
```js
import React, { useState } from "react";
import marked from "marked";
import "../css/addArticle.css";
import { Row, Col, Input, Select, Button, DatePicker } from "antd";
const { Option } = Select;
const { TextArea } = Input;
function AddArticle() {
return (
);
}
export default AddArticle;
```

然后添加右面的
```js
import React, { useState } from "react";
import marked from "marked";
import "../css/addArticle.css";
import { Row, Col, Input, Select, Button, DatePicker } from "antd";
const { Option } = Select;
const { TextArea } = Input;
function AddArticle() {
return (
{/* 左边 */}
{/* 右边 */}
);
}
export default AddArticle;
```

配置之前使用过的marked
然后定义些state并绑定


大致效果有一些了,但是还没有解决换行和多行代码块的问题。。
发现之前的highlight.js没装上,另外注意多行代码块的第一行不要放文本 不然会渲染的时候布局有问题

而且想要出现换行需要两次回车
#### 5.6 搞定导入解析
通过antd的Upload组件,我们这里用子组件Dragger
```js
import React, { useState, useEffect } from "react";
import "../css/addArticle.css";
import { Row, Col, Input, Select, Button, DatePicker } from "antd";
import Marked from "../component/Marked";
import { Upload } from "antd";
import { InboxOutlined } from "@ant-design/icons";
const { Dragger } = Upload;
const { Option } = Select;
const { TextArea } = Input;
function AddArticle() {
const [articleId, setArticleId] = useState(0); // 文章的ID,如果是0说明是新增加,如果不是0,说明是修改
const [articleTitle, setArticleTitle] = useState(""); //文章标题
const [articleContent, setArticleContent] = useState(""); //markdown的编辑内容
const [markdownContent, setMarkdownContent] = useState("预览内容"); //html内容
const [introducemd, setIntroducemd] = useState(""); //简介的markdown内容
const [introducehtml, setIntroducehtml] = useState("等待编辑"); //简介的html内容
const [showDate, setShowDate] = useState(); //发布日期
const [updateDate, setUpdateDate] = useState(); //修改日志的日期
const [typeInfo, setTypeInfo] = useState([]); // 文章类别信息
const [selectedType, setSelectType] = useState(1); //选择的文章类别
let [filename, setFile] = useState(""); //存储导入文件名
let props = {
name: "file",
beforeUpload(file, list) {
//注意:IE9 不支持该方法。
console.log(list);
if (window.FileReader) {
let filename = file.name; //文件名
var reader = new FileReader();
reader.onload = function () {
//加载完毕之后
//console.log(this.result);
setArticleContent(reader.result); //文件传给输入栏
setArticleTitle(filename.split(".") ? filename.split(".").slice(0,filename.split(".").length-1).join('.') : "");//文件标题处理,把后缀去掉
setFile(filename);
};
reader.readAsText(file);
}
return false;
},
showUploadList: false, //不显示文件list
};
const changeTitle = (e) => {
setArticleTitle(e.target.value);
};
const changeContent = (e) => {
//console.log(e.target.value);
setArticleContent(e.target.value);
//setMarkdownContent(Marked(e.target.value));
};
const changeIntroduce = (e) => {
setIntroducemd(e.target.value);
//setIntroducehtml(Marked(e.target.value));
};
useEffect(() => {
setMarkdownContent(Marked(articleContent));
}, [articleContent]); //预览随articleContent更新
useEffect(() => {
setIntroducehtml(Marked(introducemd));
}, [introducemd]); //预览随articleContent更新
return (
{/* 第一行 */}
{/* 第二行 */}
{filename.length == 0 ? (
<>
导入markdown
拖拽上传/点击上传
>
) : (
{filename}
)}
{
console.log(value);
}}
>
{/* 第三行 */}
);
}
export default AddArticle;
```
`beforeUpload` return false 阻止上传
`showUploadList` 不展示list

### day 06 后台的接口、添加/修改文章
#### 6.1 后台接口初始化

admin/main.js
```js
"use strict";
const Controller = require("egg").Controller;
class MainController extends Controller {
// async index() {
// const { ctx } = this;
// ctx.body = "api";
// }
//获得所有文章
async index() {
this.ctx.body = "hi maincontrol";
}
}
module.exports = MainController;
```
router/admin.js
```js
module.exports=app=>{
const {router,controller}=app
// router.get('/default/index',controller.default.home.index)
router.get('/admin/index',controller.admin.main.index)
}
```
router.js


初始配置一个数据库

```js
async checkLogin() {
let {userName, password} = this.ctx.request.body;
const sql =
"SELECT userName FROM admin_user WHERE userName = '" +
userName +
"'AND psssword = '" +
password +
"' ";
const res = await this.app.mysql.query(sql);
if (res.length > 0) {
let openId = new Date().getTime();
this.ctx.session.openId = { openId: openId };//如果有说明登陆成功
this.ctx.body = { data: "success" ,openId: openId};
} else {
this.ctx.body = { data: "fali" };
}
}
```

#### 6.2 登录验证
```js
import React, { useState } from "react";
import "antd/dist/antd.css";
import { Card, Input, Button, Spin, message } from "antd";
import { UserOutlined, KeyOutlined } from "@ant-design/icons";
import "../css/Login.css";
import servicePath from "../config/apiUrl";
import axios from "axios";
function Login(props) {
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setInLoading] = useState(false);
const checkLogin = () => {
setInLoading(true);
if (!userName) {
message.error("用户名不能为空");
setInLoading(false);
return false;
}
if (!password) {
message.error("用户名不能为空");
setInLoading(false);
return false;
}
let dataProps = {
userName: userName,
password: password,
};
axios({
method: "post",
url: servicePath.checkLogin,
data: dataProps,
withCredentials: true, //共享session
}).then((res) => {
setInLoading(false);
if (res.data.data == "success") {
//message.success(res.data.openId)
localStorage.setItem("openId", res.data.openId);
props.history.push("/adminIndex");
} else {
message.error("账号或密码错误");
}
});
setTimeout(() => {
setInLoading(false);
}, 1000);
};
return (
}
onChange={(e) => {
setUserName(e.target.value);
}}
/>
}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
);
}
export default Login;
```
#### 6.3 中台路由守卫配置
在service/app下新建middleware/adminauth
```js
module.exports= optinons=>{
return async function adminauth(ctx,next){
//console.log(ctx.session.openId)
if(ctx.session.openId){
await next()
}else{
ctx.body={data:'fali'}
}
}
}
```

暂时先配置好还没有引入
#### 6.4 获取文章类型(使用路由守卫中间件)
使用方法
service:


admin:
```js
const getTypeInfo = () => {
axios({
method: "get",
url: servicePath.getTypeInfo,
withCredentials: true,
}).then((res) => {
console.log(res)
if (res.data.data === "fail") {//守卫返回的错误信息
localStorage.removeItem("openId");
props.history.replace("/");//回到登录页面
} else {
setTypeInfo(res.data.data);
}
});
};
```
登录情况,前端用localStorage共享,后端用session共享
#### 6.5 添加文章
输入还是要处理
针对解析和标题的部分直接用绑定就好了
现在有一个`DatePicker`的问题,不接收Date值,只能用`moment.js`类型的数据
```
yarn add moment
```
```js
import moment from 'moment'
...
const [showDate, setShowDate] = useState(moment()); //发布日期
...
{
//console.log(date, dateString)
setShowDate(moment(dateString))
}}
>
```
```js
const saveArticle = () => {
//储存检验
let flag = true;
if (selectedType === "文章类型") {
message.error("请选择文章类型");
flag = false;
}
if (!articleTitle) {
message.error("请输入文章标题");
flag = false;
}
if (!articleContent) {
message.error("请输入文章内容");
flag = false;
}
if (!introducemd) {
message.error("请输入文章简介");
flag = false;
}
if (!showDate) {
message.error("请选择日期");
flag = false;
}
if (flag) {
message.success("验证通过");
}
};
```
然后是暂存检验


改进添加文章
```js
const saveArticle = () => {
//储存检验
let flag = true;
if (selectedType === "文章类型") {
message.error("请选择文章类型");
flag = false;
}
if (!articleTitle) {
message.error("请输入文章标题");
flag = false;
}
if (!articleContent) {
message.error("请输入文章内容");
flag = false;
}
// if (!introducemd) {
// message.error("请输入文章简介");
// flag = false;
// }//可以没有简介
if (!showDate) {
message.error("请选择日期");
flag = false;
}
if (flag) {
message.success("验证通过");
//console.log(showDate)
let date=showDate._i.replace('-','/')
let dataProps={
type_id:selectedType,
article_title:articleTitle,
article_content:articleContent,
article_introduce:introducemd,
article_addtime:new Date(date).getTime()
}
console.log(dataProps)
if(articleId===0){
dataProps.article_viewcount=0;
axios({
method: "post",
url:servicePath.addArticle,
withCredentials:true,
data:dataProps,
}).then(res=>{
console.log(res)
//if(res.data.insertId)setArticleId(res.data.insertId)
if(res.data.isSuccess){
message.success('文章保存成功')
}else{
message.warn('文章保存失败')
}
})
}
}
};
```
接口
```js
async addArticle() {
let tmpArticle = this.ctx.request.body;
const result = await this.app.mysql.insert("article", tmpArticle);
const insertSuccess = result.affectedRows === 1; //影响一行,说明成功
const insertId = result.insertId;
this.ctx.body = {
isSuccess: insertSuccess,
insertId: insertId,
};
}
```

#### 6.6 修改文章(缓存了id的情况下,如果刷新则还是重新添加)
```js
const saveArticle = () => {
//储存检验
let flag = true;
if (selectedType === "文章类型") {
message.error("请选择文章类型");
flag = false;
}
if (!articleTitle) {
message.error("请输入文章标题");
flag = false;
}
if (!articleContent) {
message.error("请输入文章内容");
flag = false;
}
// if (!introducemd) {
// message.error("请输入文章简介");
// flag = false;
// }//可以没有简介
if (!showDate) {
message.error("请选择日期");
flag = false;
}
if (flag) {
message.success("验证通过");
//console.log(showDate)
let date=showDate._i.replace('-','/')
let dataProps={
type_id:selectedType,
article_title:articleTitle,
article_content:articleContent,
article_introduce:introducemd,
article_addtime:new Date(date).getTime()
}
console.log(dataProps)
if(articleId===0){
dataProps.article_viewcount=0;
axios({
method: "post",
url:servicePath.addArticle,
withCredentials:true,
data:dataProps,
}).then(res=>{
console.log(res)
if(res.data.insertId)setArticleId(res.data.insertId)
if(res.data.isSuccess){
message.success('文章保存成功')
}else{
message.warn('文章保存失败')
}
})
}else{
dataProps.id=articleId
axios({
method: "post",
url:servicePath.updateArticle,
withCredentials:true,
data:dataProps,
}).then(res=>{
if(res.data.isSuccess){
message.success('文章保存成功')
}else{
message.warn('文章保存失败')
}
})
}
}
};
```
```js
async updateArticle() {
let tmpArticle = this.ctx.request.body;
console.log(tmpArticle)
const result = await this.app.mysql.update("article", tmpArticle);
if(result.affectedRows==0&&result.message.indexOf('matched: 0')>0){//id无效的时候直接新插入
result = await this.app.mysql.insert("article", tmpArticle);
}
const updateSuccess = result.affectedRows === 1; //影响一行,说明成功
this.ctx.body = {
isSuccess: updateSuccess,
};
}
```
### day 07 文章列表
#### 7.1 文章列表页初始ui
```js
import React, { useState, useEffect } from "react";
import { List, Row, Col, Modal, message, Button } from "antd";
import axios from "axios";
import servicePath from "../config/apiUrl";
const { confirm } = Modal;
function ArticleList(props) {
const [articleList, setArticleList] = useState([{}]);
return (
标题
类别
发布时间
浏览量
操作
}
bordered
dataSource={articleList}
renderItem={(item,index) => {
return(
{'标题'}
类别
发布时间
浏览量
)
}}
/>
);
}
export default ArticleList;
```

针对首页进行一个处理,语义化key值,onclick跳转路由


#### 7.2 删除文章
```js
async deleteArticle() {
let id=this.ctx.params.id;
//console.log(this.ctx.params)
const res= await this.app.mysql.delete('article',{id:id})
this.ctx.body={data:res}
}
```

```js
const delArticle=(id)=>{
confirm({
title:`确定删除(id:${id})?`,
content:'ok--删除',
onOk(){
console.log(servicePath.deleteArticle+`${id}`)
axios({
method:'get',
url:servicePath.deleteArticle+id,
withCredentials:true
}).then(
res=>{
console.log(res)
message.success('删除成功')
getList()
}
)
},
onCancel(){
message.info('取消删除')
},
})
}
```
#### 7.3 修改文章
根据路由跳转重新配置添加文章的页面,在动态路由中传递参数
再通过后端返回数据之后回调useState赋值(注意保存id,无id直接添加新文章)
```js
async getArticleById(){
let id=this.ctx.params.id||0;
//console.log(this.ctx.params,id)
let sql=`SELECT article.id as id ,`+
`article.article_title as title ,`+
`article.article_content as content ,`+
`article.article_introduce as introduce ,`+
`article.article_addtime as addtime ,`+
`article.article_viewcount as viewcount ,`+
`type.id as t_id ,`+
`type.typeName as typename ,`+
`type.orderNum as ordernum `+
`FROM article LEFT JOIN type ON article.type_id=type.Id `+
`WHERE article.id = ${id} `
const ids =await this.app.mysql.query('SELECT id FROM article ')//先调出所有id,判断是不是在里面
let id_s=ids.map(a=>a.id)
console.log(id,id_s,id_s.indexOf(+id))
if(id_s.indexOf(+id)>=0){//如果id存在,就返回查询结果,不存在就返回失败消息
const result =await this.app.mysql.query(sql)
//console.log(result)
this.ctx.body={data:result}
}
else{
this.ctx.body={message:"can't fount the id"}
}
}
```
```js
import React, {
useState,
useEffect,
useCallback,
useMemo,
useRef,
} from "react";
import "../css/addArticle.css";
import { Row, Col, Input, Select, Button, DatePicker, message } from "antd";
import Marked from "../component/Marked";
import { Upload } from "antd";
import { InboxOutlined } from "@ant-design/icons";
import axios from "axios";
import servicePath from "../config/apiUrl";
import moment from "moment";
const { Dragger } = Upload;
const { Option } = Select;
const { TextArea } = Input;
function AddArticle(props) {
//console.log(props)
let changer_id;//如果是跳转过来修改文章,保存这个id
const [articleId, setArticleId] = useState(0); // 文章的ID,如果是0说明是新增加,如果不是0,说明是修改
const [articleTitle, setArticleTitle] = useState(""); //文章标题
const [articleContent, setArticleContent] = useState(""); //markdown的编辑内容
const [markdownContent, setMarkdownContent] = useState("预览内容"); //html内容
const [introducemd, setIntroducemd] = useState(""); //简介的markdown内容
const [introducehtml, setIntroducehtml] = useState("等待编辑"); //简介的html内容
const [showDate, setShowDate] = useState(''); //发布日期
const [updateDate, setUpdateDate] = useState(); //修改日志的日期
const [typeInfo, setTypeInfo] = useState([]); // 文章类别信息
const [selectedType, setSelectType] = useState("文章类型"); //选择的文章类别
let [filename, setFile] = useState(""); //存储导入文件名
let prop = {
//用于文件上传的配置
name: "file",
beforeUpload(file, list) {
//注意:IE9 不支持该方法。
console.log(list);
if (window.FileReader) {
let filename = file.name; //文件名
var reader = new FileReader();
reader.onload = function () {
//加载完毕之后
//console.log(this.result);
setArticleContent(reader.result); //文件传给输入栏
setArticleTitle(
filename.split(".")
? filename
.split(".")
.slice(0, filename.split(".").length - 1)
.join(".")
: ""
); //文件标题处理,把后缀去掉
setFile(filename);
};
reader.readAsText(file);
}
return false;
},
showUploadList: false, //不显示文件list
};
const changeTitle = (e) => {
//改变标题
setArticleTitle(e.target.value);
};
const changeContent = (e) => {
//改变内容
//console.log(e.target.value);
setArticleContent(e.target.value);
//setMarkdownContent(Marked(e.target.value));
};
const changeIntroduce = (e) => {
//改变介绍
setIntroducemd(e.target.value);
//setIntroducehtml(Marked(e.target.value));
};
const saveArticle = () => {
//储存检验
let flag = true;
if (selectedType === "文章类型") {
message.error("请选择文章类型");
flag = false;
}
if (!articleTitle) {
message.error("请输入文章标题");
flag = false;
}
if (!articleContent) {
message.error("请输入文章内容");
flag = false;
}
// if (!introducemd) {
// message.error("请输入文章简介");
// flag = false;
// }//可以没有简介
if (!showDate) {
message.error("请选择日期");
flag = false;
}
if (flag) {
//message.success("验证通过");
//console.log(showDate)
let date=showDate.replace('-','/')
let dataProps={
type_id:selectedType,
article_title:articleTitle,
article_content:articleContent,
article_introduce:introducemd,
article_addtime:new Date(date).getTime()
}
console.log(dataProps)
if(articleId===0){
dataProps.article_viewcount=0;
axios({
method: "post",
url:servicePath.addArticle,
withCredentials:true,
data:dataProps,
}).then(res=>{
console.log(res)
if(res.data.insertId)setArticleId(res.data.insertId)
if(res.data.isSuccess){
message.success('文章发布成功')
}else{
message.warn('文章发布失败')
}
})
}else{
dataProps.id=articleId
//虽然数据库中字段是Id,但是这个地方用大写id会无法查找到,可能是egg进行了某种封装
axios({
method: "post",
url:servicePath.updateArticle,
withCredentials:true,
data:dataProps,
}).then(res=>{
if(res.data.isSuccess){
message.success('文章修改成功')
}else{
message.warn('文章修改失败')
}
})
}
}
};
//获取文章类型
const getTypeInfo = () => {
axios({
method: "get",
url: servicePath.getTypeInfo,
withCredentials: true,
}).then((res) => {
//console.log(res)
if (res.data.data === "fail") {
//守卫返回的错误信息
localStorage.removeItem("openId");
props.history.replace("/");
} else {
setTypeInfo(res.data.data);
}
});
};
//通过id获取文章
const getArticle=()=>{
if(props.match.params.id){
changer_id=props.match.params.id;
axios({
method:'get',
withCredentials:true,
url:servicePath.getArticleById+changer_id,
}).then(
res=>{
if(res.data.data){
console.log(res.data.data[0])
let data=res.data.data[0]
setArticleId(data.id)
setArticleTitle(data.title)
setArticleContent(data.content)
setIntroducemd(data.introduce)
setShowDate(moment(new Date(+data.addtime)).format('YYYY-MM-DD'))
setSelectType(data.t_id)
}else{
props.history.go(-1);
}
}
)
}
}
useEffect(() => {
getTypeInfo();
if(props.match.params.id)getArticle()
//console.log(typeInfo)
}, []);
useEffect(() => {
setMarkdownContent(Marked(articleContent));
}, [articleContent]); //预览随articleContent更新
useEffect(() => {
setIntroducehtml(Marked(introducemd));
}, [introducemd]); //预览随articleContent更新
return (
{/* 第一行 */}
{/* 第二行 */}
{filename.length == 0 ? (
<>
导入markdown
拖拽上传/点击上传
>
) : (
{filename}
)}
{
//console.log(date, dateString);
if (date) {
setShowDate(dateString);
} else {
setShowDate(null);
}
}}
>
{/* 第三行 */}
);
}
export default AddArticle;
```
#### 7.4 微调+搜索功能
微调就不详细说了。。。
```js
async getArticleListBytitle() {
let title=this.ctx.params.title;
//console.log(title)
let sql=`SELECT article.id as id ,`+
`article.article_title as title ,`+
//`article.article_content as content ,`+
//`article.article_introduce as introduce ,`+
`article.article_addtime as addtime ,`+
`article.article_viewcount as viewcount ,`+
//`type.id as t_id ,`+
`type.typeName as typename ,`+
`type.orderNum as ordernum `+
`FROM article LEFT JOIN type ON article.type_id=type.Id `+
`WHERE article.article_title LIKE "%${title}%" `+
`ORDER BY article.id DESC`//倒叙排列
const { ctx } = this;
const resList= await this.app.mysql.query(sql);//查询语句
ctx.body = {list:resList};
}
```

```js
{getListBytitle(value)}}
style={{ width: 200 }}
/>
```
```js
const getListBytitle = (title) => {
setIsLoading(true);
axios({
method: "get",
url: servicePath.getArticleListBytitle+title,
withCredentials: true,
}).then(async (res) => {
//console.log(res);
setIsLoading(false);
let list = await res.data.list.map((a) => {
let b = a.addtime;
b = moment(+b).format("YYYY-MM-DD"); //把时间戳format成需要的格式
a._addtime = b; //保留原始时间戳为addtime,转换后的为_addtime
return a;
});
setArticleList(list);
//console.log(res.data.list)
});
};
```

#### 7.5 可伸缩列Table找到解决方案
关于不能拖拽的bug,从网上找到了解决办法。解决方法是在onResizeStop的时候触发重置事件而不是onResize
感谢:
[https://blog.csdn.net/qq_34398777/article/details/106303169](https://blog.csdn.net/qq_34398777/article/details/106303169)
```js
import React, { useState, useEffect, useRef } from "react";
import { List, Row, Col, Modal, message, Button, Table, Input } from "antd";
import { AudioOutlined } from "@ant-design/icons";
import axios from "axios";
import servicePath from "../config/apiUrl";
import moment from "moment";
import "../css/ArticleList.css";
import { Resizable } from "react-resizable";
const { confirm } = Modal;
const { Search } = Input;
const ResizeableTitle = (props) => {
const { onResize, width, ...restProps } = props;
const [offset, setOffset] = useState(0);
if (!width) {
return | ;
}
return (
{
// 取消冒泡,不取消貌似容易触发排序事件
e.stopPropagation();
e.preventDefault();
}}
/>
}
// 拖拽事件实时更新
onResize={(e, { size }) => {
// 这里只更新偏移量,数据列表其实并没有伸缩
setOffset(size.width - width);
}}
// 拖拽结束更新
onResizeStop={(...argu) => {
// 拖拽结束以后偏移量归零
setOffset(0);
// 这里是props传进来的事件,在外部是列数据中的onHeaderCell方法提供的事件,请自行研究官方提供的案例
onResize(...argu);
}}
draggableOpts={{ enableUserSelectHack: false }}
>
|
);
};
function ArticleList(props) {
const [isLoading, setIsLoading] = useState(false);
const [articleList, setArticleList] = useState([{}]);
const [Columns, setColumns] = useState([{}]);
const modelStatusRef = useRef(null);
const getList = () => {
setIsLoading(true);
axios({
method: "get",
url: servicePath.getArticleList,
withCredentials: true,
}).then(async (res) => {
//console.log(res);
setIsLoading(false);
let list = await res.data.list.map((a) => {
let b = a.addtime;
b = moment(+b).format("YYYY-MM-DD"); //把时间戳format成需要的格式
a._addtime = b; //保留原始时间戳为addtime,转换后的为_addtime
return a;
});
setArticleList(list);
//console.log(res.data.list)
});
};
const delArticle = (id) => {
confirm({
title: `确定删除(id:${id})?`,
content: "ok--删除",
onOk() {
console.log(servicePath.deleteArticle + `${id}`);
axios({
method: "get",
url: servicePath.deleteArticle + id,
withCredentials: true,
}).then((res) => {
//console.log(res)
message.success("删除成功");
getList();
});
},
onCancel() {
message.info("取消删除");
},
});
};
const changeArticle = (id) => {
props.history.push(`/adminIndex/changeArticle/${id}`);
};
const getListBytitle = (title) => {
setIsLoading(true);
axios({
method: "get",
url: servicePath.getArticleListBytitle + title,
withCredentials: true,
}).then(async (res) => {
//console.log(res);
setIsLoading(false);
let list = await res.data.list.map((a) => {
let b = a.addtime;
b = moment(+b).format("YYYY-MM-DD"); //把时间戳format成需要的格式
a._addtime = b; //保留原始时间戳为addtime,转换后的为_addtime
return a;
});
setArticleList(list);
//console.log(res.data.list)
});
};
useEffect(() => {
getList();
let columns = [
{
title: "标题",
dataIndex: "title",
width: 300,
},
{
title: "类型",
dataIndex: "typename",
width: 100,
},
{
title: "发布时间",
dataIndex: "_addtime",
width: 150,
sorter: (a, b) => a.addtime - b.addtime,
},
{
title: "浏览量",
dataIndex: "viewcount",
width: 100,
sorter: (a, b) => a.viewcount - b.viewcount,
},
{
title: "操作",
key: "action",
render: (item) => (
),
},
];
columns = columns.map((col, index) => ({
...col,
onHeaderCell: (column) => ({
width: column.width,
onResize: handleResize(index),
}),
}));
setColumns(columns);
}, []);
useEffect(() => {
// 每次 更新 把值 复制给 modelStatusRef
modelStatusRef.current = Columns;
}, [Columns]); // 依赖的值 等modelStatus 改变了 才出发里面的值
const handleResize = (index) => (e, { size }) => {
//console.log(modelStatusRef.current)
console.log(size);
const nextColumns = [...modelStatusRef.current];
nextColumns[index] = {
...nextColumns[index],
width: size.width,
};
setColumns(nextColumns);
console.log(Columns, nextColumns);
};
const components = {
header: {
cell: ResizeableTitle,
},
};
return (
{
getListBytitle(value);
}}
style={{ width: 200 }}
/>
{/*
标题
类别
发布时间
浏览量
操作
}
bordered
dataSource={articleList}
renderItem={(item, index) => {
return (
{item.title}
{item.typename}
{moment(+item.addtime).format("YYYY-MM-DD")}
{item.viewcount}
);
}}
/> */}
);
}
export default ArticleList;
```
#### 7.6 端口修改
后台单页面(SPA)react-app改成9000端口

前台SSR渲染多页面应用(MPA)next-app改成9001端口

给中台服务的config.default.js的cors跨域做一下调整,设置白名单
