React(1702H)文章管理-cms系统

一、GIF图

1、效果图

 

 

 

 

 

 

 2、上传图片和创建文章

 

 

 

 

3、编辑文章

 

 

 

 

4、添加带文章的banner

 

 

 

 

二、使用到的npm包

1、pug

参考链接:https://pug.bootcss.com/api/getting-started.html

 

pug示例代码:

const pug = require('pug')
const fs = require('fs')

function editArticle (htmlJson, fileName) {
  const compiledFunction = pug.compileFile('views/index.pug');
  let indexHtml = compiledFunction({
    ...htmlJson,
    // list: [
    //   {
    //     type: 'title-level-1',
    //     text: '活动规则:'
    //   }
    // ]
  })
  fs.writeFile(`./public/article/${fileName}`, indexHtml, function (err) {
    if (err) {
      throw err;
    }
  });
}

//生成添加文章
app.post('/add/article', async function (req, res) {
  let { htmlJson } = req.body
  let fileName = (new Date()).getTime() + '.html'
  editArticle(htmlJson, fileName)

  let articlePath = `http://localhost:8888/article/${fileName}`
  let uid = getID(10)
  let createTime = new Date().getTime()
  let sqlData = await addArticle(
    uid, 
    htmlJson.articleTitle, 
    fileName, 
    articlePath, 
    JSON.stringify(htmlJson), 
    createTime)

  if (sqlData) {
    let data = {
      fileName,
      articlePath
    }
    res.send(({
      code: 200,
      data: data,
      message: '添加文章成功'
    }))
  } else {
    res.send(({
      code: 400,
      message: '添加文章失败'
    }))    
  }
})

//获取文章列表
app.get('/article/list', async function (req, res) {
  const data = await getArticleList()
  res.send(({
    code: 200,
    data: data,
    message: '文章列表'
  }))
})

//通过id获取文章
app.get('/article_detail', async function (req, res) {
  let {id} = req.query
  const data = await getArticleDetail(id)
  res.send(({
    code: 200,
    data: data,
    message: '文章详情'
  }))
})

//编辑文章
app.post('/article_edit', async function (req, res) {
  let {articleId, title, fileName, htmlJson} = req.body
  const data = await editArticleDetail(articleId, title, JSON.stringify(htmlJson))
  editArticle(htmlJson, fileName)
  res.send(({
    code: 200,
    message: '编辑文章成功'
  }))
})

views/index.pug:

doctype html
html
  head
    title=articleTitle
    link(rel="stylesheet" type='text/css' href='/css/index.css')
  body
    div.m-warp
      div.m-hearder-wrap
        img(class="m-header-img" src=headerImagePath)
      div.m-content-wrap
        if list
          each item in list
            case item.type 
              when 'p'
                p.m-paragraph-text=item.text    
              when 'p-strong'
                p.m-paragraph-text-strong=item.text    
              when 'title-level-1'
                div.m-title-level-1=item.text
                div.m-division
              when 'title-level-2'
                div.m-title-level-2=item.text


    script(src='/common/js/jquery.min.js')
    script(src='/js/index.js')

 

三、前端React

前端React部分没有用到新技术,需要具备知识包括:

路由、antd组件(Button, Input, message, Modal, Checkbox,Table)、受控组件、生命周期(componentDidMount)、Scrollbars库、moment库(时间戳转日期)、axios等。

 

 

Article.js:

import React from 'react';
import { withRouter } from 'react-router-dom'
import { Button, Input, message, Modal, Table } from 'antd';
import { Scrollbars } from 'react-custom-scrollbars'
import moment from 'moment'
import Api from '../../api/index.js'
import * as keyCode from '../../api/keyCode.js'
import './index.css'

const { TextArea } = Input;
class Article extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      addArticleModalVisible: false,
      articleTitle: '',
      list: [],
    }
  }
  render() {
    let {
      addArticleModalVisible,
      articleTitle,
      list,
    } = this.state
    let columns = this.renderColumns()
    return (
      <div className="m-content">
        <Scrollbars>
          <div className="m-content-inner">
            <div className="m-article-toolbar">
              <Button onClick={this.handleShowAddArticleModal.bind(this)}>添加文章</Button>
            </div>       
            <div>
              <Table 
                columns={columns} 
                dataSource={list} 
                rowKey="uid"
                scroll={{ x: 900 }}
                ></Table>                     
            </div> 
          </div>
          <Modal
            title="添加文章"
            visible={addArticleModalVisible}
            onOk={this.handleAddArticle.bind(this)}
            onCancel={this.handleHideModal.bind(this)}>
            <div className="m-row">
              <Input 
                type="text" 
                value={articleTitle}
                placeholder="请输入文章标题"
                onChange={this.handleInput.bind(this, 'articleTitle')}></Input>
            </div>         
          </Modal>          
        </Scrollbars>        
			</div>
    );
  }
}

//生命周期
Object.assign(Article.prototype, {
  renderColumns () {
    return [
      {
        title: 'ID',
        dataIndex: 'uid',
      },
      {
        title: '标题',
        dataIndex: 'title',
      },       
      {
        title: '文章路径',
        dataIndex: 'path',
        key: 'path',
        render: (text, record) => {
          return <a href={text} target="_blank">{text}</a>
        }
      },
      {
        title: '创建时间',
        dataIndex: 'create_time',
        key: 'create_time',
        render: (text, record) => {
          return <span>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</span>
        }
      },
      {
        title: '操作',
        fixed: 'right',   
        width: 150,
        render: (text, record, index) => {
          return <div>
            <Button onClick={this.handleEditArticle.bind(this, record)}>编辑文章</Button>
          </div>
        }
      }                     
    ]
  },   
  componentDidMount() {
    this.getArticleList()
  }
})

//事件
Object.assign(Article.prototype, {
  handleShowAddArticleModal() {
    this.setState({
      addArticleModalVisible: true,
      articleTitle: ''
    })
  },
  handleHideModal() {
    this.setState({
      addArticleModalVisible: false,
      addHeaderImageModal: false
    })
  },
  handleAddArticle() {
    let {articleTitle} = this.state
    let htmlJson = {
      articleTitle,
      list: [],
    }
    let data = {
      htmlJson
    }
    Api.addArticle(data).then((res) => {
      console.log(res)
      this.getArticleList()
      this.handleHideModal()
    })
  },
  getArticleList() {
    Api.getArticleList().then((res) => {
      if (res.code = keyCode.SUCCESS) {
        this.setState({
          list: res.data.list
        })
      }
    })
  },
  handleEditArticle(record) {
    this.props.history.push(`/management/edit_article/${record.uid}`)
  }
})

//受控组件
Object.assign(Article.prototype, {
  handleInput(field, e) {
    this.setState({
      [field]: e.target.value
    })
  },
})

export default withRouter(Article)

EditArticle.js:

import React from 'react';
import { withRouter } from 'react-router-dom'
import { Button, Input, message, Modal, Checkbox } from 'antd';
import { Scrollbars } from 'react-custom-scrollbars'
import Api from '../../api/index.js'
import * as keyCode from '../../api/keyCode.js'
import './index.css'

const { TextArea } = Input;
class EditArticle extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      articleId: '',
      fileName: '',
      articlePath: '',
      articleTitle: '',
      headerImagePath: '',
      htmlJson: {
      },
      articleTextArea: '',
      paragraph: '',
      isParagraphStrong: false,
      paragraphTitleLevelFirst: '',
      paragraphTitleLevelSecond: '',
      addHeaderImageModal: false,
      addParagraphModal:false,
      addParagraphTitleLevelFirstModal: false,
      addParagraphTitleLevelSecondModal: false,
    }
  }
  render() {
    let {
      articleId,
      fileName,
      articlePath,
      articleTitle,
      headerImagePath,
      articleTextArea,
      paragraph,
      isParagraphStrong,
      paragraphTitleLevelFirst,
      paragraphTitleLevelSecond,
      addHeaderImageModal,
      addParagraphModal,
      addParagraphTitleLevelFirstModal,
      addParagraphTitleLevelSecondModal,
    } = this.state
    return (
      <div className="m-content">
        <Scrollbars>
          <div className="m-content-inner">
            <div>
              <Button onClick={this.handleGoBack.bind(this)}>返回文章列表</Button>
            </div>
            <div className="m-edit-article-title">编辑文章</div>
            <div>
              <div>文章ID: {articleId}</div>
              <div>文件名: {fileName}</div>
              <div>文章链接: <a href={articlePath} target="_blank">{articlePath}</a></div>
            </div>
            
            <div className="m-article-toolbar">
              <Button className="m-toolbar-btn" onClick={this.handleShowAddHeaderImageModal.bind(this)}>标题和顶部图片</Button>
              <Button className="m-toolbar-btn" onClick={this.handleShowAddParagraphModal.bind(this)}>添加段落文本</Button>
              <Button className="m-toolbar-btn" onClick={this.handleShowAddParagraphTitleLevelFirstModal.bind(this)}>添加一级段落标题</Button>
              <Button className="m-toolbar-btn" onClick={this.handleShowAddParagraphTitleLevelSecondModal.bind(this)}>添加二级段落标题</Button>
            </div>    
            <div className="m-article-textarea-wrap">
              <TextArea 
                rows={10} 
                value={articleTextArea}
                onChange={this.handleInput.bind(this, 'articleTextArea')}
                />
            </div>   
            <div className="m-login-row">
              <Button onClick={this.handleEditArticle.bind(this)}>保存</Button>
            </div>    
          </div>        
          <div>
            <Modal
              title="修改标题和顶部图片"
              visible={addHeaderImageModal}
              onOk={this.handleAddHeaderImage.bind(this)}
              onCancel={this.handleHideModal.bind(this)}>
              <div className="m-row">
                <span className="m-input-label">
                  文章标题
                </span>
                <Input 
                  className="m-input"
                  type="text" 
                  value={articleTitle}
                  placeholder="请输入文章标题"
                  onChange={this.handleInput.bind(this, 'articleTitle')}></Input>
              </div>                
              <div className="m-row">
                <span className="m-input-label">
                  顶部图片链接
                </span>                
                <Input 
                  className="m-input"
                  type="text" 
                  value={headerImagePath}
                  placeholder="请输入顶部图片地址"
                  onChange={this.handleInput.bind(this, 'headerImagePath')}></Input>
              </div>          
            </Modal>  
            <Modal
              title="添加段落"
              visible={addParagraphModal}
              onOk={this.handleAddParagraph.bind(this)}
              onCancel={this.handleHideModal.bind(this)}>
              <div className="m-row">
                <span className="m-input-label">
                  文本是否加粗
                </span>
                <Checkbox 
                  className="m-checkbox"
                  checked={isParagraphStrong} 
                  onChange={this.handleCheckbox.bind(this, 'isParagraphStrong')}>加粗</Checkbox>
              </div>
              <div className="m-row">
                <span className="m-input-label">
                  段落文本
                </span>                
                <TextArea 
                  className="m-input"
                  type="text" 
                  rows={6}
                  value={paragraph}
                  placeholder="请输入段落文本"
                  onChange={this.handleInput.bind(this, 'paragraph')}></TextArea>
              </div>          
            </Modal> 
            <Modal
              title="添加一级段落标题"
              visible={addParagraphTitleLevelFirstModal}
              onOk={this.handleAddParagraphTitleLevelFirst.bind(this)}
              onCancel={this.handleHideModal.bind(this)}>
              <div className="m-row">
                <span className="m-input-label">
                  一级段落标题
                </span>                
                <Input 
                  className="m-input"
                  type="text" 
                  value={paragraphTitleLevelFirst}
                  placeholder="请输入一级段落标题"
                  onChange={this.handleInput.bind(this, 'paragraphTitleLevelFirst')}></Input>
              </div>          
            </Modal>  
            <Modal
              title="添加二级段落标题"
              visible={addParagraphTitleLevelSecondModal}
              onOk={this.handleAddParagraphTitleLevelSecond.bind(this)}
              onCancel={this.handleHideModal.bind(this)}>
              <div className="m-row">
                <span className="m-input-label">
                  二级段落标题
                </span>                
                <Input 
                  className="m-input"
                  type="text" 
                  value={paragraphTitleLevelSecond}
                  placeholder="请输入二级段落标题"
                  onChange={this.handleInput.bind(this, 'paragraphTitleLevelSecond')}></Input>
              </div>          
            </Modal>                         
          </div>         
        </Scrollbars>        
			</div>
    );
  }
}

//生命周期
Object.assign(EditArticle.prototype, { 
  componentDidMount() {
    this.getArticleById()
  }
})

//事件
Object.assign(EditArticle.prototype, {
  handleGoBack() {
    this.props.history.push('/management/article')
  },
  getArticleById() {
    let {match} = this.props
    let articleId = match.params.id
    this.setState({
      articleId
    })
    Api.getArticleDetail(`?id=${articleId}`).then((res) => {
      console.log(res)
      if (res.code === keyCode.SUCCESS) {
        let articleTextArea = JSON.stringify(res.data[0].content, null, 2)
        this.setState({
          fileName: res.data[0].file_name,
          articlePath: res.data[0].path,
          articleTextArea,
          htmlJson: res.data[0].content
        })
      }
    })
  },
  handleEditArticle() {
    let {articleId, fileName, articleTextArea} = this.state
    let htmlJson
    try {
      htmlJson = JSON.parse(articleTextArea)
    } catch (err) {
      console.log(err)
      message.info('文本框里输入的json格式不对!')
      return
    }
    
    let title = ''
    if (htmlJson.articleTitle) {
      title = htmlJson.articleTitle
    }
    let data = {
      articleId,
      title,
      fileName,
      htmlJson,
    }
    Api.editArticle(data).then((res) => {
      console.log(res)
      if (res.code === keyCode.SUCCESS) {
        this.setState({
          htmlJson
        })
        message.info('编辑成功')
      }
    })
  }
})

//对话框相关
Object.assign(EditArticle.prototype, {
  handleShowAddHeaderImageModal() {
    let {htmlJson} = this.state
    this.setState({
      addHeaderImageModal: true,
      articleTitle: htmlJson.articleTitle,
      headerImagePath: htmlJson.headerImagePath,
    })
  },
  handleShowAddParagraphModal(){
    this.setState({
      addParagraphModal: true,
      paragraph: '',
      isParagraphStrong: false,
    })
  },
  handleShowAddParagraphTitleLevelFirstModal() {
    this.setState({
      addParagraphTitleLevelFirstModal: true,
      paragraphTitleLevelFirst: '',
    })
  },
  handleShowAddParagraphTitleLevelSecondModal() {
    this.setState({
      addParagraphTitleLevelSecondModal: true,
      paragraphTitleLevelSecond: '',
    })
  },
  handleHideModal() {
    this.setState({
      addHeaderImageModal: false,
      addParagraphModal: false,
      addParagraphTitleLevelFirstModal: false,
      addParagraphTitleLevelSecondModal: false,
    })
  },
})



//顶部图片、段落、段落一级标题、段落二级标题
Object.assign(EditArticle.prototype, {
  handleAddHeaderImage() {
    let {htmlJson, articleTitle, headerImagePath} = this.state
    htmlJson.headerImagePath = headerImagePath
    htmlJson.articleTitle = articleTitle
    this.setState({
      htmlJson,
    })
    this.formatTextAreaString(htmlJson)
    this.handleHideModal()
  },
  handleAddParagraph() {
    let {htmlJson, paragraph, isParagraphStrong } = this.state
    if (!htmlJson.list) {
      htmlJson.list = []
    }
    htmlJson.list.push({
      type: isParagraphStrong ? 'p-strong' : 'p',
      text: paragraph
    })    

    this.setState({
      htmlJson
    })
    this.formatTextAreaString(htmlJson)
    this.handleHideModal()
  },
  handleAddParagraphTitleLevelFirst() {
    let {htmlJson, paragraphTitleLevelFirst } = this.state
    if (!htmlJson.list) {
      htmlJson.list = []
    }
    htmlJson.list.push({
      type: 'title-level-1',
      text: paragraphTitleLevelFirst
    })    

    this.setState({
      htmlJson
    })
    this.formatTextAreaString(htmlJson)
    this.handleHideModal()
  },
  handleAddParagraphTitleLevelSecond() {
    let {htmlJson, paragraphTitleLevelSecond } = this.state
    if (!htmlJson.list) {
      htmlJson.list = []
    }
    htmlJson.list.push({
      type: 'title-level-2',
      text: paragraphTitleLevelSecond
    })    

    this.setState({
      htmlJson
    })
    this.formatTextAreaString(htmlJson)
    this.handleHideModal()
  }
})

//工具
Object.assign(EditArticle.prototype, {
  formatTextAreaString(htmlJson) {
    let articleTextArea = JSON.stringify(htmlJson, null, 2)
    this.setState({
      articleTextArea,
    })
  }
})

//受控组件
Object.assign(EditArticle.prototype, {
  handleInput(field, e) {
    this.setState({
      [field]: e.target.value
    })
  },
  handleCheckbox(field, e) {
    this.setState({
      [field]: e.target.checked
    })
  },
})

export default withRouter(EditArticle)

 

 

 

 

 

 

 

posted @ 2019-07-29 09:09  徐同保  阅读(273)  评论(0编辑  收藏  举报