前言
该项目是仿造技术胖的个人博客,原视频地址(jspang.com/detailed?id…
我已经将其上传到我的的github项目(github.com/duskya/-), 欢迎下载。
页面展示截图
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)
};
复制代码
这里先放下目录截图
2.2 使用数据库
- 要在egg.js中使用mysql数据库,那需要先进行安装egg-mysql模块。
yarn add egg-mysql
复制代码
- plugin.js中进行配置
/server/config/plugin.js
'use strict';
exports.mysql = {
enable: true,
package: 'egg-mysql'
}
复制代码
- /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 准备阶段
- 建立前端文件夹
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;
复制代码
这里给下目录截图
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 主页显示+中端获取文章数据
- 先在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
}
}
复制代码
- 在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/', // 文章详细页内容接口 ,需要接收参数
复制代码
- 详情页
- 先安装两个模块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
复制代码
- 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 主页面
- 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>
<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}>
<Select defaultValue="Sign Up" size="large">
<Option value="Sign Up">视频教程</Option>
</Select>
</Col> */}
<Col span={4}>
<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> */}
<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地址
复制代码