React+Egg.js开发个人blog

前言

该项目是仿造技术胖的个人博客,原视频地址(jspang.com/detailed?id…
我已经将其上传到我的的github项目(github.com/duskya/-), 欢迎下载。

页面展示截图

2021-04-23 (1)_副本.png
2021-04-23 (1).png

1. 创建数据库

打开数据库新建一个数据库react_blog,新建两个表type和article。

type表:  
    1. id:类型编号int类型  
    2. typename:文章类型名称varchar类型  
    3. orderNum:类型编号int类型      
    
article表:  
    1. id: 文章编号int类型  
    2. type_id: 文章类型编号int类型  
    3. title: 文章标题,varchar类型  
    4. article_content: 文章主体内容,text类型  
    5. introduce: 文章简介,text类型  
    6. addTime: 文章发布时间,date类型  
    
复制代码

2. 中台搭建

2.1 准备阶段

1.建立一个service文件夹,当做中台

    egg-init --type=simple
    npm install
    npm run dev
复制代码

2.service/controller文件夹下建立两个文件夹admin(管理端使用所有API接口)和default(客户端使用所有的api接口)。

3.这里为了把路由分成前后端,所以在app文件夹下新建一个router文件夹。在文件夹下新建两个文件default.js和admin.js。

default.js

module.exports = app =>{
    const {router,controller} = app
    router.get('/default/index',controller.default.home.index)
}
复制代码

service/app/router.js

'use strict';
module.exports = app => {

  require('./router/default')(app)
};
复制代码

这里先放下目录截图

8FCU307R(H_`@S5KKUVIK_V.png

2.2 使用数据库

  1. 要在egg.js中使用mysql数据库,那需要先进行安装egg-mysql模块。
yarn add egg-mysql
复制代码
  1. plugin.js中进行配置

/server/config/plugin.js

'use strict';
exports.mysql = {
  enable: true,
  package: 'egg-mysql'
}
复制代码
  1. /config/config.default.js配置数据库连接
 config.mysql = {
    // database configuration
    client: {
      // host
      host: 'localhost',
      // port
      port: '3306',
      // username
      user: 'root',
      // password
      password: '',//这里自己填写数据库密码
      // database
      database: 'react_blog',
    },
    // load into app, default is open
    app: true,
    // load into agent, default is close
    agent: false,
  };
复制代码

2.3 解决跨域

1.egg-cors模块是专门用来解决egg.js跨域问题的,只要简单的配置就可以完成跨域的设置

yarn add egg-cors
复制代码

2.完成后需要对/service/config/plugin.js文件进行修改,加入egg-cors模块即可

exports.cors: {
    enable: true,
    package: 'egg-cors'
}
复制代码

3.在配置完成plugin.js文件以后,还需要设置config.default.js文件。这个文件主要设置的是允许什么域名和请求方法可以进行跨域访问。配置代码如下。

config.security = {
    csrf: {enable: false},
    domainWhiteList: [ '*' ]
  };
  config.cors = {
    origin: 'http://localhost:3000', //只允许这个域进行访问接口
    credentials: true,   // 开启认证
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
    };
复制代码

3. 前台搭建

3.1 准备阶段

  1. 建立前端文件夹
npx create-next-app blog
yarn dev
复制代码

2.用yarn命令来安装@zeit/next-css包,它的主要功能就是让Next.js可以加载CSS文件。

yarn add @zeit/next-css
复制代码

在blog根目录下,新建一个next.config.js文件。这个就是Next.js的总配置文件。写入下面的代码:

const withCss = require('@zeit/next-css')

if(typeof require !== 'undefined'){
    require.extensions['.css']=file=>{}
}

module.exports = withCss({})
复制代码

3.yarn来安装antd

yarn add antd 
yarn add babel-plugin-import
复制代码

在项目根目录建立.babelrc文件,然后写入如下配置文件。

{
    "presets":["next/babel"],  //Next.js的总配置文件,相当于继承了它本身的所有配置
    "plugins":[     //增加新的插件,这个插件就是让antd可以按需引入,包括CSS
        [
            "import",
            {
                "libraryName":"antd"
            }
        ]
    ]
}
复制代码

4.在pages目录下,新建一个_app.js文件,把css全局引入

import App from 'next/app'
import 'antd/dist/antd.css'
export default App
复制代码

5.在blog下建立根目录config文件夹,新建apiUrl.js文件获取中台数据接口

let ipUrl = 'http://127.0.0.1:7001/default/' //写自己中台数据接口
let servicePath = {
}
export default servicePath;
复制代码

这里给下目录截图

1616726813561.jpg

3.2 头部制作+中台获取数据

/blog/components/目录下新建一个Header.js文件,我们头部左侧显示下用户名 , 右侧显示首页和文章类型,供用户点击

1.先在中台获取类型数据
service/app/default/home.js里设sql语言查询id,获取type类型

'use strict';
const Controller = require('egg').Controller

class HomeController extends Controller {

    async getTypeInfo(){
        const result = await this.app.mysql.select('type')
        this.ctx.body ={data:result}
    }
    async getListById(){
        let id = this.ctx.params.id
        let sql = 'SELECT article.id as id,'+
        'article.title as title,'+
        'article.introduce as introduce,'+
        "DATE_FORMAT(article.addTime,'%Y-%m-%d') as addTime,"+
        'article.view_count as view_count ,'+
        'type.typeName as typeName '+
        'FROM article LEFT JOIN type ON article.type_id = type.Id '+
        'WHERE type_id='+id
        const result = await this.app.mysql.query(sql)
        this.ctx.body={data:result}
    }
}
module.exports = HomeController
复制代码

2.在service/app/router/default.js中配置路由

module.exports = app =>{
    const {router,controller} = app
    router.get('/default/index',controller.default.home.index)
    router.get('/default/getTypeInfo',controller.default.home.getTypeInfo)
 router.get('/default/getListById/:id',controller.default.home.getListById)
}
复制代码

3.blog/config/apiUrl.js中加入中端

let servicePath = {
    getTypeInfo:ipUrl + 'getTypeInfo',
    getListById:ipUrl + 'getListById/', 
}
复制代码

4.blog/components/Header.js获取终端类型进行数据类型显示,之后页面跳转传参数id到类型分类页

import React, { useState, useEffect } from 'react'
import './header.css'
import { Row, Col, Menu, Icon } from 'antd'
import Router from 'next/router'
import Link from 'next/link'
import axios from 'axios'
import servicePath from '../config/apiUrl'

const Header = () => {
    const [navArray , setNavArray] = useState([])

    useEffect(()=>{
        const fetchData = async ()=>{
           const result= await axios(servicePath.getTypeInfo).then(
                (res)=>{
                    console.log(res.data,res.data.data,'header.js里面的第16行')
                    // setNavArray(res.data.data)
                    return res.data.data
                }
              )
           setNavArray(result)
        }
        fetchData()
    },[])

    const handleClick = (e)=>{
        if(e.key==0){
            Router.push('/')
        }else if(e){
            Router.push('/list?id='+e.key)
        }
    }

    return (
        <div className="header">
            <Row type="flex" justify="center">
                <Col xs={24} sm={24} md={10} lg={15} xl={12}>
                    <span className="header-logo">阿巴阿巴阿巴</span>
                    <span className="header-txt">前端学习</span>
                </Col>
                <Col className="memu-div" xs={0} sm={0} md={14} lg={10} xl={7}>
                    <Menu
                        mode="horizontal"
                        onClick={handleClick}
                    >
                        <Menu.Item key="0">
                            <Icon type="home" />
                            首页
                        </Menu.Item>
                        {
                            navArray.map((item) => {
                                return (
                                    <Menu.Item key={item.Id}>
                                        <Icon type={item.icon} />
                                        {item.typeName}
                                    </Menu.Item>
                                )
                            })
                        }
                    </Menu>
                </Col>
            </Row>
        </div>
    )
}
export default Header
复制代码

5.blog/pages/list.js获取id跳转,并返回数据
next提供了在服务端运行的getInitialProps方法,我们可以在这个方法中执行接口获取页面数据。getInitialProps传入的参数中有一个req对象即url域名后面的参数,可以通过解析得到,值得一提的是在刷新getInitialProps方法在跳转页面的过程中有可能获取不到req对象这时候就通过res.query来获取路径上的参数,

import Head from 'next/head'
import { Row, Col, List, Icon, Breadcrumb } from 'antd'
import Header from '../components/Header'
import Author from '../components/Author'
import Advert from '../components/Advert'
import Footer from '../components/Footer'
import '../styles/pages/index.css'
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import servicePath from '../config/apiUrl'
import Link from 'next/link'

const Mylist = (list) => {

  const [mylist, setMylist] = useState(list.data);
  useEffect(()=>{
    setMylist(list.data)
  })
  return (
    <>
      <Header />
      <Row className="comm-main" type="flex" justify="center">
        <Col className="comm-left" xs={24} sm={24} md={16} lg={18} xl={14}  >
          <div>  
            <div className="header">
              搜索结果
            </div>
            <List
              itemLayout="vertical"
              dataSource={mylist}
              renderItem={item => (
                <List.Item>
                  <div className="list-title">
                    <Link href={{ pathname: '/detail', query: { id: item.id } }}>
                      <a>{item.title}</a>
                    </Link>
                  </div>
                  <div className="list-icon">
                    <span><Icon type="calendar" />{item.addTime}</span>
                    <span><Icon type="folder" /> {item.typeName}</span> 
                  </div>
                  <div className="list-context">{item.introduce}</div>
                </List.Item>
              )}
            />
          </div>
        </Col>
        <Col className="comm-right" xs={0} sm={0} md={7} lg={5} xl={4}>
          <Author />
          <Advert />
        </Col>
      </Row>
      <Footer />
    </>
  )
}
Mylist.getInitialProps = async (context) => {

  let id = context.query.id
  const promise = new Promise((resolve) => {
    axios(servicePath.getListById + id).then(
      (res) => resolve(res.data)
    )
  })
  return await promise
}

export default Mylist 
复制代码

实现效果截图

3.3 侧边作者栏

实现结果截图:

这里直接放代码blog/components/Author.js

import {Avatar,Divider} from 'antd'
import '../styles/components/Author.css'

const Author =()=>{

    return (
        <div className="author-div comm-box">
            <div> <Avatar size={100} src="" />         
            </div>
            <div className="author-introduction">
                敲代码一分钟 改bug一小时
                <Divider>社交账号</Divider>
                <div> 微信:abaabaaba0222</div>
                <div>github:https://github.com/duskya/-</div>
            </div>
        </div>
    )
}
export default Author
复制代码

3.4 主页显示+中端获取文章数据

  1. 先在service里获取文章,在service/app/default/home.js下使用sql查询获取数据
 async getArticleList() {
        let sql = 'SELECT article.id as id,'+
                 'article.title as title,'+
                 'article.introduce as introduce,'+
                 //主要代码----------start
                 "DATE_FORMAT(article.addTime,'%Y-%m-%d') as addTime,"+
                 //主要代码----------end
                 'article.view_count as view_count ,'+
                 'type.typeName as typeName '+
                 'FROM article LEFT JOIN type ON article.type_id = type.Id'

        const results = await this.app.mysql.query(sql)

        this.ctx.body = {
            data: results
        }
    }
复制代码
  1. 在service/app/router/default.js文件下配置路由
router.get('/default/getArticleList',controller.default.home.getArticleList)
复制代码

3.blog\config\apiUrl.js下配置接口请求数据

getArticleList:ipUrl + 'getArticleList' ,  //  首页文章列表接口
复制代码

4.blog\pages\index.js下获取文章数据,使用link标签跳转详情页

import Head from 'next/head'
import Header from '../components/Header'
import { Row, Col, List } from 'antd'
import axios from 'axios'
import Link from 'next/link'
import React, { useState } from 'react'
import '../styles/pages/index.css'
import Author from '../components/Author'
import Footer from '../components/Footer'
import  servicePath  from '../config/apiUrl'

const Home = (list) => {
  console.log(list,'------')
  const [mylist, setMylist] = useState(list.data);
  return (
    <>
      <Head>
        <title>Home</title>
      </Head>
      <Header />
      <Row className="comm-main" type="flex" justify="center">
        <Col className="comm-left" xs={24} sm={24} md={16} lg={18} xl={14}  >
          <div>

            <List
              header={<div>所有笔记</div>}
              itemLayout="vertical"
              dataSource={mylist}
              renderItem={item => (
                <List.Item>
                  <div className="list-title">
                    <Link href={{ pathname: '/detail/', query: { id: item.id } }}>
                      <a>{item.title}</a>
                    </Link>
                  </div>
                  <div className="list-icon">
                    <span> {item.addTime}</span>
                    <span>{item.typeName}</span>
                    {/* <span><Icon type="fire" /> {item.view_count}人</span> */}
                  </div>
                  <div className="list-context">{item.introduce}</div>
                </List.Item>
              )}
            />
          </div>
        </Col>

        <Col className="comm-right" xs={0} sm={0} md={7} lg={5} xl={4}>
          <Author />
          {/* <Advert /> */}
        </Col>
      </Row>
      <Footer />

    </>
  )

}

Home.getInitialProps = async ()=>{
  const promise = new Promise((resolve)=>{
    axios(servicePath.getArticleList).then(
      (res)=>{
        resolve(res.data)
      }
    )
  })

  return await promise
}

export default Home
复制代码

3.5 详情页跳转+中台获取id数据显示

1.先在service里获取id内容,在service/app/default/home.js下使用sql查询参数

async getArticleById(){
        //先配置路由的动态传值,然后再接收值
        let id = this.ctx.params.id

        let sql = 'SELECT article.id as id,'+
        'article.title as title,'+
        'article.introduce as introduce,'+
        'article.article_content as article_content,'+
        "DATE_FORMAT(article.addTime,'%Y-%m-%d') as addTime,"+
        'article.view_count as view_count ,'+
        'type.typeName as typeName ,'+
        'type.id as typeId '+
        'FROM article LEFT JOIN type ON article.type_id = type.Id '+
        'WHERE article.id='+id

        const result = await this.app.mysql.query(sql)

        this.ctx.body={data:result}

    }
复制代码

2.在service/app/router/default.js文件下配置路由

router.get('/default/getArticleById/:id',controller.default.home.getArticleById)
复制代码

3.blog\config\apiUrl.js下配置接口请求数据

getArticleById:ipUrl + 'getArticleById/',  // 文章详细页内容接口 ,需要接收参数
复制代码
  1. 详情页
  • 先安装两个模块mark和highlight,使其代码高亮,文章格式更加好看。在下载lodash文件,引入tocify.tsx目录自动生成目录结构
yarn add marked
yarn add highlight
yarn add lodash
复制代码
  • tocify.tsx代码
import React from 'react';
import { Anchor } from 'antd';
import { last } from 'lodash/array';

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 => (
      <Link key={item.anchor} href={`#${item.anchor}`} title={item.text}>
        {item.children && this.renderToc(item.children)}
      </Link>
    ));
  }

  render() {
    return (
      <Anchor affix showInkInFixed>
         {this.renderToc(this.tocItems)}
      </Anchor>
    );
  }
}
复制代码
  • 详情页引入tocify,使用marked和highlight对文章格式进行配置,获取id远程传递id获取数据
import React, { useState } from 'react'
import Head from 'next/head'
import { Row, Col, Affix, Icon, Breadcrumb } from 'antd'
import Header from '../components/Header'
import Author from '../components/Author'
import Advert from '../components/Advert'
import Footer from '../components/Footer'
import servicePath from '../config/apiUrl'
import '../styles/pages/detailed.css'
import axios from 'axios'
import marked from 'marked'
import hljs from "highlight.js";
import 'highlight.js/styles/monokai-sublime.css';
import Tocify from '../components/tocify.tsx'

const Detailed = (props) => {

  let articleContent = props.article_content

  const tocify = new Tocify()
  const renderer = new marked.Renderer();
  renderer.heading = function (text, level, raw) {
    const anchor = tocify.add(text, level);
    return `<a id="${anchor}" href="" class="anchor-fix"><h${level}>${text}</h${level}></a>\n`;
  };

  marked.setOptions({
    renderer: renderer,//自定义的Renderer渲染出自定义的格式
    gfm: true,//gfm:启动类似Github样式的Markdown,填写true或者false
    pedantic: false,//只解析符合Markdown定义的
    sanitize: false,//原始输出,忽略HTML标签
    tables: true,//支持Github形式的表格,必须打开gfm选项
    breaks: false,//支持Github换行符
    smartLists: true,//优化列表输出
    smartypants: false,//

    highlight: function (code) {
            return hljs.highlightAuto(code).value;
    }//高亮显示规则 
  });
  let html = marked(props.article_content)
  return (
    <>
      <Head>
        <title>博客详细页</title>
      </Head>
      <Header />
      <Row className="comm-main" type="flex" justify="center">
        <Col className="comm-left" xs={24} sm={24} md={16} lg={18} xl={14}  >
          <div>
            <div>
              <div className="detailed-title">
                {props.title}
              </div>

              <div className="list-icon center">
                <span> {props.addTime}</span>
                <span> {props.typeName}</span>
              </div>

              <div className="detailed-content"
                dangerouslySetInnerHTML={{ __html: html }}   >
              </div>
            </div>
          </div>
        </Col>

        <Col className="comm-right" xs={0} sm={0} md={7} lg={5} xl={4}>
          <Author />
          <Advert />
          <Affix offsetTop={5}>
            <div className="detailed-nav comm-box">
              <div className="nav-title">文章目录</div>
              <div className="toc-list">
                {tocify && tocify.render()}
              </div>

            </div>
          </Affix>
        </Col>
      </Row>
      <Footer />
    </>
  )
}

Detailed.getInitialProps = async (context) => {

  console.log(context.query.id)
  let id = context.query.id
  const promise = new Promise((resolve) => {
    axios(servicePath.getArticleById + id).then(
      (res) => {
        // console.log(res.data,'+++++')
        resolve(res.data.data[0])//sql返回的是数组
      }
    )
  })
  return await promise
}

export default Detailed
复制代码

4. 后台搭建

4.1 准备阶段

1.使用create-react-app admin 生成目录,安装一些插件

yarn add antd
yarn add react-router-dom

复制代码
  1. App.js可以删掉,建立Pages/Main.js文件夹

src/index

import React from 'react';
import ReactDOM from 'react-dom';
import Main from './Pages/Main';

ReactDOM.render(
    <Main />,
  document.getElementById('root')
);
复制代码

Main.js

import React from 'react';
import { BrowserRouter as Router, Route} from "react-router-dom";
import Login from './Login'
import AdminIndex from './AdminIndex'
function Main(){
    return (
        <Router>      
            <Route path="/" exact component={Login} />
            <Route path="/index/"  component={AdminIndex} />
        </Router>
    )
}
export default Main
复制代码

admin/src/目录截图

4.2 登录页面+中台查询id是否存在

1.我们先在数据库里新建一个表admin_user,表里放用户数据

2.admin/src/Pages/Login.js,编写登录界面,获取到用户名和密码,传送到中台判断数据库是否存在这个用户,如果有则跳转到主页面

import React , {useState,useEffect,createContext} from 'react';
import 'antd/dist/antd.css';
import '../static/css/Login.css';
import { Card, Input, Button ,Spin,message } from 'antd';
import axios from 'axios'
import  servicePath  from '../config/apiUrl'

const openIdContext = createContext()

function Login(props){

    const [userName , setUserName] = useState('')
    const [password , setPassword] = useState('')
    const [isLoading, setIsLoading] = useState(false)

    useEffect(()=>{
    },[])

    const checkLogin = ()=>{
        setIsLoading(true)

        if(!userName){
            message.error('用户名不能为空')
            return false
        }else if(!password){
            message.error('密码不能为空')
            return false
        }
        let dataProps = {
            'userName':userName,
            'password':password
        }
        axios({
            method:'post',
            url:servicePath.checkLogin,
            data:dataProps,
            withCredentials: true,
            // header:{'Access-Control-Allow-Origin':'*' }
        }).then(
           res=>{
                console.log(res.data)
                setIsLoading(false)
                if(res.data.data=='登录成功'){
                    localStorage.setItem('openId',res.data.openId)
                    
                    message.success('已经登录')
                    props.history.push('/index')
                }else{
                    message.error('用户名密码错误')
                }
           }
        )
        setTimeout(()=>{
            setIsLoading(false)
        },1000)
    }

    return (
        <div className="login-div">

            <Spin tip="Loading..." spinning={isLoading}>
                <Card title="Blog  System" bordered={true} style={{ width: 400 }} >
                    <Input
                        id="userName"
                        size="large"
                        placeholder="Enter your userName"
                        onChange={(e)=>{setUserName(e.target.value)}}
                    /> 
                    <br/><br/>
                    <Input.Password
                        id="password"
                        size="large"
                        placeholder="Enter your password"
                        onChange={(e)=>{setPassword(e.target.value)}}
                    />     
                    <br/><br/>
                    <Button type="primary" size="large" block onClick={checkLogin} > Login in </Button>
                </Card>
            </Spin>
        </div>
    )
}

export default Login
复制代码

3.中台设置数据库查询id是否存在
service\app\controller\admin\main.js

'use strict';

const Controller = require('egg').Controller

class MainController extends Controller{

    async checkLogin(){
        let userName =this.ctx.request.body.userName
        let password = this.ctx.request.body.password
        const sql = "SELECT userName FROM admin_user WHERE userName = '" +userName +
        "'AND password = '"+password+"'"
        const res = await this.app.mysql.query(sql)
        if(res.length>0){
            let openId =new Date().getTime()
            this.ctx.session.openId=openId
            this.ctx.body ={'data':'登录成功','openid':openId}
            console.log('--------')
        }else{
            this.ctx.body={data:'登录失败'}
        }
    }
 
}

module.exports = MainController
复制代码

4.service/app/router/admin.js配置路由

module.exports = app =>{
    const {router,controller} =app
    router.get('/admin/index',controller.admin.main.index)
    router.post('/admin/checkLogin',controller.admin.main.checkLogin)

}
复制代码

5.admin/src/config/apiUrl.js配置请求中台接口

let ipUrl = "http://127.0.0.1:7001/admin/"

let servicePath ={
    checkLogin:ipUrl + 'checkLogin' ,  //  检查用户名密码是否正确
}
export default servicePath
复制代码

4.3 主页面

  1. admin/Pages/AdminIndex.js设置侧边栏和路由跳转
import React,{useState} from 'react';
import { Layout, Menu, Breadcrumb } from 'antd';
import '../static/css/AdminIndex.css'
import {Route, Router} from 'react-router-dom'
import AddArticle from './AddArticle'
import ArticleList from './ArticleList'

const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;

function AdminIndex(props){

  const [collapsed,setCollapsed] = useState(false)

  const onCollapse = collapsed => {
    setCollapsed(collapsed)
  };
  const handleClickArticle = e =>{
    console.log(e.item.props)
    if(e.key =='addArticle'){
      props.history.push('/index/add')
    }else{
      props.history.push('/index/list')
    }
  }

    return (
      <Layout style={{ minHeight: '100vh' }}>
          {/* collapsible是否可收起  collapsed当前收起状态  onCollapse展开-收起时的回调函数, */}
        <Sider  collapsible collapsed={collapsed} onCollapse={onCollapse}>
          <div className="logo" />
          <Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
            <Menu.Item key="1">
              <span>工作台</span>
            </Menu.Item>
            <Menu.Item key="addArticle" onClick={handleClickArticle}>
              <span>添加文章</span>
            </Menu.Item>
            <Menu.Item key="articleList" onClick={handleClickArticle}>
              <span>文章列表</span>
            </Menu.Item>
            <Menu.Item key="9">
              <span>留言管理</span>
            </Menu.Item>
          </Menu>
        </Sider>
        <Layout>
          <Header style={{ background: '#fff', padding: 0 }} />
          <Content style={{ margin: '0 16px' }}>
            <div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
              <div>
                  <Route path ='/index/' exact component={AddArticle} />
                  <Route path="/index/add/" exact   component={AddArticle} />
                  <Route path="/index/add/:id"  exact   component={AddArticle} />
                  <Route path="/index/list/"   component={ArticleList} />
              </div>
            </div>
          </Content>
          <Footer style={{ textAlign: 'center' }}>abaabaaba.com</Footer>
        </Layout>
      </Layout>
    )

}

export default AdminIndex
复制代码

4.4 文章列表

1.service\app\controller\admin\main.js数据库获取所有文章,并传送到admin\src\Pages\ArticleList.js显示,并且可以删除编辑文章,获取id转到admin\src\Pages\AddArticle.js下,将获取到的文章内容、标题什么的显示出来,进行编辑。

admin\main.js

async getArticleList(){

        let sql = 'SELECT article.id as id,'+
                    'article.title as title,'+
                    'article.introduce as introduce,'+
                    "DATE_FORMAT(article.addTime,'%Y-%m-%d') as addTime,"+
                    'type.typeName as typeName '+
                    'FROM article LEFT JOIN type ON article.type_id = type.Id '+
                    'ORDER BY article.id DESC '
    
            const resList = await this.app.mysql.query(sql)
            this.ctx.body={list:resList}
    
    }
     async delArticle(){
        let id = this.ctx.params.id
        const res = await this.app.mysql.delete('article',{'id':id})
        this.ctx.body={data:res}
        console.log(id)
    }
 
    async getArticleById(){
        let id = this.ctx.params.id
    
        let sql = 'SELECT article.id as id,'+
        'article.title as title,'+
        'article.introduce as introduce,'+
        'article.article_content as article_content,'+
        "DATE_FORMAT(article.addTime,'%Y-%m-%d') as addTime,"+
        'article.view_count as view_count ,'+
        'type.typeName as typeName ,'+
        'type.id as typeId '+
        'FROM article LEFT JOIN type ON article.type_id = type.Id '+
        'WHERE article.id='+id
        const result = await this.app.mysql.query(sql)
        this.ctx.body={data:result}
    }
复制代码

2.service\app\router\admin.js配置路由

    router.get('/admin/getArticleList',controller.admin.main.getArticleList)
    router.get('/admin/delArticle/:id',controller.admin.main.delArticle)
    router.get('/admin/getArticleById/:id',controller.admin.main.getArticleById
复制代码

3.admin\src\config\apiUrl.js

    getArticleList:ipUrl + 'getArticleList' ,  //  文章列表 
    delArticle:ipUrl + 'delArticle/' ,  //  删除文章
    getArticleById:ipUrl + 'getArticleById/' ,  //  根据ID获得文章详情
复制代码

4.admin\src\Pages\ArticleList.js显示文章列表,并且可以删除编辑

import React,{useState,useEffect} from 'react';
import '../static/css/ArticleList.css'
import {List,Row,Col,Modal,message,Button,Switch} from 'antd';
import axios from 'axios'
import servicePath from '../config/apiUrl'
const {confirm} =Modal

function ArticleList(props){
    const [list,setList] = useState([])

    useEffect(()=>{
        getList()
    },[])

    const getList = ()=>{
       
        axios({
                method:'get',
                url: servicePath.getArticleList,
                withCredentials: true,
                header:{ 'Access-Control-Allow-Origin':'*' }
            }).then(
            res=>{
                setList(res.data.list)  
    
                }
            )
    } 
    const delArticle = (id)=>{
        confirm({
            title: '确定要删除这篇博客文章吗?',
            content: '如果你点击OK按钮,文章将会永远被删除,无法恢复。',
            onOk() {
                axios(servicePath.delArticle+id,{ withCredentials: true}).then(
                    res=>{ 
                        message.success('文章删除成功')
                        getList()
                        }
                    )
            },
            onCancel() {
                message.success('没有任何改变')
            },
         });
    
    }
    const updateArticle = (id,checked)=>{
        props.history.push('/index/add/'+id)
    }
    return (
        <div>
             <List
                header={
                    <Row className="list-div">
                        <Col span={6}>
                            <b>标题</b>
                        </Col>
                        <Col span={6}>
                            <b>类别</b>
                        </Col>
                        <Col span={6}>
                            <b>发布时间</b>
                        </Col>
                        <Col span={6}>
                            <b>操作</b>
                        </Col>
                    </Row>

                }
                bordered
                dataSource={list}
                renderItem={item => (
                    <List.Item>
                         <Row className="list-div">
                            <Col span={6}>
                                {item.title}
                            </Col>
                            <Col span={6}>
                             {item.typeName}
                            </Col>
                            <Col span={6}>
                                {item.addTime}
                            </Col>
                            <Col span={6}>
                              <Button type="primary"  onClick={()=>{updateArticle(item.id)}}>修改</Button>&nbsp;
                              <Button onClick={()=>{delArticle(item.id)}}>删除 </Button>
                            </Col>
                        </Row>
                    </List.Item>
                )}
                />

        </div>
    )

}

export default ArticleList
复制代码

4.5 添加文章

1.设置文本输入框、简介、类别、日期选择器等。判断是否传参数id过来,如果有,则表明为修改文章,获取文章值显示到页面上。

import React, { useState, useEffect } from 'react';
import marked from 'marked'
import '../static/css/AddArticle.css'
import { Row, Col, Input, Select, Button, DatePicker, message } from 'antd'
import axios from 'axios'
import servicePath from '../config/apiUrl'

const { Option } = Select;
const { TextArea } = Input
function AddArticle(props) {

    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) //选择的文章类别

    useEffect(() => {
        getTypeInfo()
        let tmpId = props.match.params.id
        if(tmpId){
            setArticleId(tmpId)
            getArticleById(tmpId)
        }
    }, [])

    marked.setOptions({
        renderer: marked.Renderer(),
        gfm: true,
        pedantic: false,
        sanitize: false,
        tables: true,
        breaks: false,
        smartLists: true,
        smartypants: false,
    });

    const changeContent = (e) => {
        setArticleContent(e.target.value)
        let html = marked(e.target.value)
        setMarkdownContent(html)
    }
    const changeIntroduce = (e) => {
        setIntroducemd(e.target.value)
        let html = marked(e.target.value)
        setIntroducehtml(html)
    }
    // 从中台得到文章类别信息
    const getTypeInfo = () => {

        axios({
            method: 'get',
            url: servicePath.getTypeInfo,
            header: { 'Access-Control-Allow-Origin': '*' },
            withCredentials: true
        }).then(
            res => {
                console.log(res.data.data)
                //    if(res.data.data=="没有登录"){
                //      localStorage.removeItem('openId')
                //  props.history.push('/')  
                //    }else{
                setTypeInfo(res.data.data)
                //    }

            }
        )
    }
    const selectTypeHandler = (value) => {
        setSelectType(value)
    }

    const saveArticle = () => {

        // markedContent()//先进行转换

        if (!selectedType) {
            message.error('必须选择文章类别')
            return false
        } else if (!articleTitle) {
            message.error('文章名称不能为空')
            return false
        } else if (!articleContent) {
            message.error('文章内容不能为空')
            return false
        } else if (!introducemd) {
            message.error('简介不能为空')
            return false
        } else if (!showDate) {
            message.error('发布日期不能为空')
            return false
        }
        // message.success('检验通过')
        let dataProps ={}
        dataProps.type_id = selectedType
        dataProps.title = articleTitle
        dataProps.article_content = articleContent
        dataProps.introduce = introducemd
        // let datetext = showDate.replace('-','/')
        // let datetext = showDate
        // dataProps.addTime =(new Date(datetext).getTime())/1000
        dataProps.addTime = showDate
        
        if(articleId==0){
            console.log('articleId=:'+articleId)
            // dataProps.view_count = Math.ceil(Math.random()*100)+1000
            axios({
                method:'post',
                url:servicePath.addArticle,
                header:{ 'Access-Control-Allow-Origin':'*' },
                data:dataProps,
                withCredentials:true
            }).then(
                res=>{
                    setArticleId(res.data.insertId)
                    if(res.data.isSuccess){
                        message.success('文章保存成功')
                    }else{
                        message.error('文章保存失败');
                    }
                }
            )
        }else{
            dataProps.id=articleId
            axios({
                method:'post',
                url:servicePath.updateArticle,
                header:{ 'Access-Control-Allow-Origin':'*' },
                data:dataProps,
                withCredentials:true
            }).then(
                res=>{
                    if(res.data.isScuccess){
                        message.success('文章保存成功')
                    }else{
                        message.error('保存失败');
                    }            
                }
            )
        }

    }

    const getArticleById =(id)=>{
        axios(servicePath.getArticleById+id,{
            withCredentials:true,
            header:{'Access-Control-Allow-Orign':'*'}
        }).then(
            res=>{
                setArticleTitle(res.data.data[0].title)
                setArticleContent(res.data.data[0].article_content)
                let html=marked(res.data.data[0].article_content)
                setMarkdownContent(html)
                setIntroducemd(res.data.data[0].introduce)
                let tmpInt = marked(res.data.data[0].introduce)
                setIntroducehtml(tmpInt)
                setShowDate(res.data.data[0].addTime)
                setSelectType(res.data.data[0].typeId)  
            }  
        )
    }

    return (
        <div>
            <Row gutter={5}>
                <Col span={18}>
                    <Row gutter={10} >
                        <Col span={20}>
                            <Input
                                value={articleTitle}
                                onChange={e => {
                                    setArticleTitle(e.target.value)
                                }}
                                placeholder="博客标题"
                                size="large" />
                        </Col>
                        {/* <Col span={4}>
                            &nbsp;
                            <Select defaultValue="Sign Up" size="large">
                                <Option value="Sign Up">视频教程</Option>
                            </Select>
                        </Col> */}
                        <Col span={4}>
                            &nbsp;
                            <Select defaultValue={selectedType} size="large" onChange={selectTypeHandler}>
                                {
                                    typeInfo.map((item, index) => {
                                        return (<Option key={index} value={item.Id}>{item.typeName}</Option>)
                                    })
                                }
                            </Select>
                        </Col>
                    </Row>
                    <br />
                    <Row gutter={10} >
                        <Col span={12}>
                            <TextArea
                                value={articleContent}
                                className="markdown-content"
                                rows={35}
                                placeholder="文章内容"
                                onChange={changeContent}
                                onPressEnter={changeContent}
                            />
                        </Col>
                        <Col span={12}>
                            <div
                                className="show-html"
                                dangerouslySetInnerHTML={{ __html: markdownContent }}>

                            </div>
                        </Col>
                    </Row>
                </Col>
                <Col span={6}>
                    <Row>
                        <Col span={24}>
                            {/* <Button size="large">暂存文章</Button>&nbsp; */}
                            <Button type="primary" size="large" onClick={saveArticle}>
                                发布文章
                            </Button>
                        </Col>
                        <Col span={24}>
                            <br />
                            <TextArea
                                rows={4}
                                value={introducemd}
                                onChange={changeIntroduce}
                                onPressEnter={changeIntroduce}
                                placeholder="文章简介"
                            />
                            <br /><br />
                            <div
                                className="introduce-html"
                                dangerouslySetInnerHTML={{ __html: introducehtml }} >
                            </div>
                        </Col>
                        <Col span={12}>
                            <div className="date-select">
                                <DatePicker
                                    onChange={(date, dateString) => setShowDate(dateString)}
                                    placeholder="发布日期"
                                    size="large"
                                />
                            </div>
                        </Col>
                    </Row>
                </Col>
            </Row>
        </div>
    )
}
export default AddArticle
复制代码

2.service\app\controller\admin\main.js配置sql语言添加和编辑文章

async getTypeInfo(){
        const resType = await this.app.mysql.select('type')
        this.ctx.body={data:resType}
    }

    async addArticle(){
        let tmpArticle = this.ctx.request.body
        //tmpArticle
        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
        }
    }

    async updateArticle(){
        let tmpArticle= this.ctx.request.body
    
        const result = await this.app.mysql.update('article', tmpArticle);
        const updateSuccess = result.affectedRows === 1;
        console.log(updateSuccess)
        this.ctx.body={
            isScuccess:updateSuccess
        }
    }  
复制代码

3.service\app\router\admin.js配置路由

    router.get('/admin/getTypeInfo',controller.admin.main.getTypeInfo)
    router.post('/admin/addArticle',controller.admin.main.addArticle)
    router.post('/admin/updateArticle',controller.admin.main.updateArticle)
复制代码

4.admin\src\config\apiUrl.js配置路由

    getTypeInfo:ipUrl + 'getTypeInfo' ,  //  获得文章类别信息
    addArticle:ipUrl +'addArticle',//
    updateArticle:ipUrl + 'updateArticle' ,  //  修改文章第api地址

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享