【笔记】Vue Element+Node.js开发企业通用管理后台系统——电子书列表页面开发


电子书编辑 | 「小慕读书」管理后台


一、电子书编辑

1.表单规则校验

修改src\views\book\components\Detail.vue

  • 新增:rules="rules,为可编辑表单项新增prop
<template>
  <el-form ref="postForm" :model="postForm" :rules="rules">
    ...
    <div class="detail-container">
      <el-row>
        ...
        <el-col :span="24">
          <el-form-item style="margin-bottom: 40px;" prop="title">
            ...
          </el-form-item>
          <div>
            <el-row>
              <el-col :span="12" class="form-item-author">
                <el-form-item prop="author" :label-width="labelWidth" label="作者:">
                  ...
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item prop="publisher" :label-width="labelWidth" label="出版社:">
                  ...
                </el-form-item>
              </el-col>
            </el-row>
            <el-row>
              <el-col :span="12">
                <el-form-item prop="language" :label-width="labelWidth" label="语言:">
                 ...
                </el-form-item>
              </el-col>
              ...
            </el-row>
            ...
          </div>
        </el-col>
      </el-row>
    </div>
  </el-form>
</template>

创建表单规则校验逻辑:

  const validateRequire = (rule, value, callback) => {
    if (value === '') {
      this.$message({
        message: rule.field + '为必传项',
        type: 'error'
      })
      callback(new Error(rule.field + '为必传项'))
    } else {
      callback()
    }
  }
  const validateSourceUri = (rule, value, callback) => {
    if (value) {
      if (validURL(value)) {
        callback()
      } else {
        this.$message({
          message: '外链url填写不正确',
          type: 'error'
        })
        callback(new Error('外链url填写不正确'))
      }
    } else {
      callback()
    }
  }
  return {
    postForm: Object.assign({}, defaultForm),
    rules: {
      image_uri: [{ validator: validateRequire }],
      title: [{ validator: validateRequire }],
      content: [{ validator: validateRequire }],
      source_uri: [{ validator: validateSourceUri, trigger: 'blur' }]
    }
  }
}

2.提交表单

提交表单时需要提供两个接口,createBookupdateBook
新建src\api\book.js

import request from '../utils/request'

export function createBook(book) {
  return request({
    url: '/book/create',
    method: 'post',
    data: book
  })
}

修改src\views\book\components\Detail.vue的submitForm方法:

  submitForm() {
    this.$refs.postForm.validate(valid => {
      if (valid) {
        this.loading = true
        const book = Object.assign({}, this.postForm)
        delete book.contents
        if (!this.isEdit) {
          createBook(book).then(response => {
            this.loading = false
            this.$notify({
              title: '成功',
              message: response.msg,
              type: 'success',
              duration: 2000
            })
            this.toDefault()
          }).catch(() => {
            this.loading = false
          })
        } else {
          updateBook(book).then(response => {
            console.log('updateBook', response)
            this.loading = false
            this.$notify({
              title: '成功',
              message: response.msg,
              type: 'success',
              duration: 2000
            })
          }).catch(() => {
            this.loading = false
          })
        }
      } else {
        return false
      }
    })
  }

router\book.js新建api:create:

const { decode } = require('../utils')
...
router.post(
  '/create',
  function(req, res, next) {
    const decode = decoded(req)
    if (decode && decode.username) {
      req.body.username = decode.username
    }
    const book = new Book(null, req.body)
  }
)

utils\constant.js中新增:

UPDATE_TYPE_FROM_WEB: 1

models\Book.js中新增:

createBookFromData(data) {
    this.fileName = data.fileName
    this.cover = data.coverPath
    this.title = data.title
    this.author = data.author
    this.publisher = data.publisher
    this.bookId = data.fileName
    this.language = data.language
    this.rootFile = data.rootFile
    this.originalName = data.originalName
    this.path = data.path || data.filePath
    this.filePath = data.path || data.filePath
    this.unzipPath = data.unzipPath
    this.coverPath = data.coverPath
    this.createUser = data.username
    this.createDt = new Date().getTime()
    this.updateDt = new Date().getTime()
    this.updateType = data.updateType === 0 ? data.updateType : UPDATE_TYPE_FROM_WEB
    this.contents = data.contents
    this.category = data.category || 99
    this.categoryText = data.categoryText || '自定义'
}

新建services\book.js

const Book = require('../models/Book')
const db = require('../db')

function exists(book) {
  return false
}

function removeBook(book) {}

function insertContents(book) {}

function insertBook(book) {
  return new Promise(async (resolve, reject) => {
    try {
      if (book instanceof Book) {
        const result = await exists(book)
        if (result) {
          await removeBook(book)
          reject(new Error('电子书已存在'))
        } else {
          await db.insert(book, 'book')
          await insertContents(book)
          resolve()
        }
      } else {
        reject(new Error('添加的图书对象不合法'))
      }
    } catch (e) {
      reject(e)
    }
  })
}

module.exports = {
  insertBook
}

修改router\book.js

const bookService = require('../services/book')
...
router.post(
  '/create',
  function(req, res, next) {
    ...
    bookService.insertBook(book).then(() => {
      new Result(book, '上传电子书成功').success(res)
    }).catch(err => {
      next(boom.badImplementation(err)) // 500, Internal Server Error
    })
  }
)

db\index.js中新增insert方法:

function insert(model, tableName) {
  return new Promise((resolve, reject) => {
    if (!isObject(model)) {
      reject(new Error('插入数据库失败,插入数据非对象'))
    } else {
      const keys = []
      const values = []
      Object.keys(model).forEach(key => {
        if (model.hasOwnProperty(key)) { // 检查model自身是否包含key这个属性(继承来的不算)
          keys.push(`\`${key}\``) // 避免key与sql的关键字重复
          values.push(`'${model[key]}'`)
        }
      })
      if (keys.length > 0 && values.length > 0) {
        let sql = `INSERT INTO \`${tableName}\` (`
        const keysString = keys.join(',')
        const valuesString = values.join(',')
        sql = `${sql}${keysString}) VALUES (${valuesString})`
        debug && console.log(sql)
        const conn = connect()
        try {
          conn.query(sql, (err, result) => {
            if (err) {
              reject(err)
            } else {
              resolve(result)
            }
          })
        } catch (e) {
          reject(e)
        } finally {
          conn.end() // 注意一定要加,否则会造成内存泄漏
        }
      } else {
        reject(new Error('插入数据库失败,对象中没有任何属性'))
      }
    }
  })
}

类型判断小技巧:
在这里插入图片描述

models\Book.js中新增方法toDb,以免在插入数据库过程中book对象相对book表出现冗余项:

toDb() {
  return {
    fileName: this.fileName,
    cover: this.cover,
    title: this.title,
    author: this.author,
    publisher: this.publisher,
    bookId: this.bookId,
    updateType: this.updateType,
    language: this.language,
    rootFile: this.rootFile,
    originalName: this.originalName,
    filePath: this.path,
    unzipPath: this.unzipPath,
    coverPath: this.coverPath,
    createUser: this.createUser,
    createDt: this.createDt,
    updateDt: this.updateDt,
    category: this.category,
    categoryText: this.categoryText
  }
}

修改services\book.js:(await db.insert(book.toDb(), 'book')):

function insertBook(book) {
  return new Promise(async (resolve, reject) => {
    try {
      if (book instanceof Book) {
        ...
        if (result) {...} else {
          await db.insert(book.toDb(), 'book')
          await insertContents(book)
          resolve()
        }
      } else {...}
    } catch (e) {...}
  })
}

重启服务端,上传一本电子书,点击新增电子书,在数据库中便可看到新增数据:
在这里插入图片描述


目前的问题是,新增电子书之后需要表单清空,调用toDefault只是清空了表单内容,校验结果还保留:
在这里插入图片描述
查看官网说明:https://element.eleme.cn/#/zh-CN/component/form
在这里插入图片描述
toDefault()中加入this.$refs.postForm.resetFields(),去掉this.postForm = Object.assign({}, defaultForm),并为每个表单项(el-form-item)新增prop

此时再点新增电子书之后表单就会直接清空。


在服务端的models\Book.js 中新增getContents方法:

getContents() {
  return this.contents
}

3.将目录存入数据库

操作之前我们需要先去掉前端中src\views\book\components\Detail.vuesubmitForm方法中的这一句:delete book.contents
在这里插入图片描述
由于直接从文件中获取的目录数据有多个冗余字段,因此在存入数据库之前需要将其冗余字段去掉,为了方便使用我们在服务端安装lodash

cnpm i -S lodash

Lodash 中文文档 | Lodash 中文网
axios中文文档

services\book.js中引入lodash并新建insertContents方法:

async function insertContents(book) {
  const contents = book.getContents()
  if (contents && contents.length > 0) {
    for (let i = 0; i < contents.length; i++) {
      // 项目内容有可能包含单引号,给它替换成空格,以免影响插入数据库
      for (const key in contents[i]) {
        contents[i][key] = contents[i][key].toString().replace('\'', ' ')
      }
      const content = contents[i]
      // 剔除冗余项
      const _content = _.pick(content, [
        'fileName',
        'id',
        'text',
        'href',
        'order',
        'level',
        'label',
        'pid',
        'navId'
      ])
      console.log('_content', _content)
      await db.insert(_content, 'contents')
    }
  }
}

lodash.pick

调试过程中,可能会遇到目录项比较多的书,会产生报错:413 Payload Too Large,解决方法:在服务端的app.js中修改如下两句(新增limit: '50mb'):

app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }))
app.use(bodyParser.json({limit: '50mb'}))

数据量大那肯定会有超时问题(timeout of 5000ms exceeded),因此修改src\utils\request.js的如下部分(timeout: 20 * 1000):

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 20 * 1000
})

这样便可成功。对比控制台输出的content和经过处理的_content,发现冗余字段已经清理干净
在这里插入图片描述

在这里插入图片描述
点击新增电子书之后查看数据库已经成功插入数据


二、电子书在数据库中查重

判断电子书在数据库中是否已经存在,需要从三方面着手:书名、作者、出版社。在services\book.js中修改如下内容:

function exists(book) {
  const { title, author, publisher } = book
  const sql = `select * from book where title='${title}' and author='${author}' and publisher='${publisher}'`
  return db.queryOne(sql)
}

如果电子书在数据库中已存在,从book表和contents表中移除相关内容:

async function removeBook(book) {
  if (book) {
    book.reset()
    if (book.fileName) {
      const removeBookSql = `delete from book where fileName='${book.fileName}'`
      const removeContentsSql = `delete from contents where fileName='${book.fileName}'`
      await db.querySql(removeBookSql)
      await db.querySql(removeContentsSql)
    }
  }
}

上面代码中用到了book对象的reset方法(models\Book.js)用来删除服务端刚刚上传的已重复的电子书相关资源(epubcoverunzip),同时在models\Book.js中新增pathExists方法搭配fs.existsSync用来判断文件路径是否存在:

reset() {
    if (this.path && Book.pathExists(this.path)) {
      fs.unlinkSync(Book.genPath(this.path))
    }
    if (this.filePath && Book.pathExists(this.filePath)) {
      fs.unlinkSync(Book.genPath(this.filePath))
    }
    if (this.coverPath && Book.pathExists(this.coverPath)) {
      fs.unlinkSync(Book.genPath(this.coverPath))
    }
    if (this.unzipPath && Book.pathExists(this.unzipPath)) {
      // 注意node低版本将不支持第二个属性(迭代删除)
      fs.rmdirSync(Book.genPath(this.unzipPath), { recursive: true })
    }
  }

  static pathExists(path) {
    if (path.startsWith(UPLOAD_PATH)) {
      return fs.existsSync(path)
    } else {
      return fs.existsSync(Book.genPath(path))
    }
  }
  • fs.unlinkSync用来删除对应路径的文件
  • fs.rmdirSync用来删除对应路径的文件夹

上传电子书,新增电子书,成功,再次上传同一本,上传成功,新增电子书,新增失败,报错电子书已存在。


三、电子书查询

在前端项目中修改src\router\index.js,在路由/book/edit后新加参数/:fileName

export const asyncRoutes = [
  {
    ....
    children: [	
    ...
      {
        path: '/book/edit/:fileName',	// 访问路径
        ...
      },
      ...
	]
  }
]

此时,路由/book/edit无法直接访问(404),必须填入参数fileName

src\views\book\components\Detail.vue中新增生命周期函数created

  created() {
    if (this.isEdit) {
      const fileName = this.$route.params.fileName
      this.getBookData(fileName)
    }
  },

创建方法getBookData

    getBookData(fileName) {
      getBook(fileName).then(res => {
        this.setData(res.data)
      })
    }

src\api\book.js中新增:

export function getBook(fileName) {
  return request({
    url: '/book/get',
    method: 'get',
    params: { fileName }
  })
}

postdata传参,get使用params

此时访问http://localhost:9527/#/book/edit/f138cd4a0b5fb137b21dcbab021fe799,显示接口不存在,请求地址是:https://book.aimooc.top:18082/book/get?fileName=f138cd4a0b5fb137b21dcbab021fe799

在服务端的router\book.js添加:

router.get('/get', function(req, res, next) {
  const { fileName } = req.query
  if (!fileName) {
    next(boom.badRequest(new Error('参数fileName不能为空')))
  } else {
    bookService.getBook(fileName).then(book => {
      new Result(book, '获取图书信息成功').success(res)
    }).catch(err => {
      next(boom.badImplementation(err))
    })
  }
})

router\book.js中添加:

function getBook(fileName) {
  return new Promise(async (resolve, reject) => {
    const bookSql = `select * from book where fileName='${fileName}'`
    const contentsSql = `select * from contents where fileName= '${fileName} order by \`order\`'`
    const book = await db.queryOne(bookSql)
    const contents = await db.querySql(contentsSql)
    resolve(book)
  })
}

重启服务端,刷新浏览页,其他信息查询成功,只差封面和目录:
在这里插入图片描述
utils\constant.js中新增OLD_UPLOAD_URL,用来兼容之前项目的cover url

const OLD_UPLOAD_URL = env === 'dev' ? 'https://book.aimooc.top/book/res/img' : 'https://book.gaowenda.cn/book/res/img'

models\Book.js新增:

static genCoverUrl(book) {
    console.log('genCoverUrl', book)
    if (+book.updateType === 0) {
      const { cover } = book
      if (cover) {
        if (cover.startsWith('/')) {
          return `${OLD_UPLOAD_URL}${cover}`
        } else {
          return `${OLD_UPLOAD_URL}/${cover}`
        }
      } else {
        return null
      }
    } else {
      if (book.cover) {
        if (book.cover.startsWith('/')) {
          return `${UPLOAD_URL}${book.cover}`
        } else {
          return `${UPLOAD_URL}/${book.cover}`
        }
      } else {
        return null
      }
    }
  }

router\book.jsgetBook中添加:

function getBook(fileName) {
  return new Promise(async (resolve, reject) => {
    ...
    const contents = await db.querySql(contentsSql)
    if (book) { // new
      book.cover = Book.genCoverUrl(book) // new
      book.contentsTree = Book.genContentsTree(contents) // new
      resolve(book) // 有修改
    } else { // new
      reject(new Error('电子书不存在')) // new
    } // new
  })
}

models\Book.js新增:

static genContentsTree(contents ) {
    if (contents) {
      const contentsTree = []
      // 将目录转化为树状结构
      contents.forEach(c => {
        c.children = []
        if (c.pid === '') {
          contentsTree.push(c)
        } else {
          const parent = contents.find(_ => _.navId === c.pid)
          parent.children.push(c)
        }
      })
      return contentsTree
    }
  }

修改models\Book.jsparseContents方法的如下内容:

chapters.forEach(c => {
  c.children = []
  if (c.pid === '') {
    chapterTree.push(c)
  } else {
    const parent = chapters.find(_ => _.navId === c.pid)
    parent.children.push(c)
  }
}) // 将目录转化为树状结构

修改为:

const chapterTree = Book.genContentsTree(chapters)

由于我们是直接从数据库中拿到的contentsTree而不是contents,因此需要修改前端src\views\book\components\Detail.vue的目录展示部分(postForm.contents修改为contentsTree ):

<el-row>
  <el-col :span="24">
    <el-form-item prop="contents" label-width="60px" label="目录:">
      <div
        v-if="contentsTree && contentsTree.length > 0"
        class="contents-wrapper"
      >
        <el-tree :data="contentsTree" @node-click="onContentClick" />
      </div>
      <span v-else></span>
    </el-form-item>
  </el-col>
</el-row>

这样目录便可以显示出来了,点击也可以

四、更新电子书

先来修改前端:

src\api\book.js中新增如下内容(用来向服务端发起电子书更新请求):

export function updateBook(book) {
  return request({
    url: '/book/update',
    method: 'post',
    data: book
  })
}

src\views\book\components\Detail.vuesubmitForm方法中新增如下内容:

submitForm() {
  ...
  this.$refs.postForm.validate(valid => {
    if (valid) {
      ...
      if (!this.isEdit) {
        createBook(book).then(res => {...}).catch(e => {...})
      } else {
        updateBook(book).then(res => { // new
          console.log('updateBook', res) // new
          this.$notify({ // new
            title: '更新成功', // new
            message: res.msg, // new
            type: 'success', // new
            duration: 2000 // new
          }) // new
          this.loading = false // new
        }).catch(e => { // new
          console.log('更新失败', e) // new
          this.loading = false // new
        }) // new
      }
    } else {...}
  })
},

接下来修改服务端:

router\book.js中新增API路由:

router.post(
  '/update',
  function(req, res, next) {
    const decode = decoded(req)
    if (decode && decode.username) {
      req.body.username = decode.username
    }
    const book = new Book(null, req.body) // 从data中拿到book对象的内容
    bookService.updateBook(book).then(book => {
      new Result('更新电子书成功').success(res)
    }).catch(err => {
      next(boom.badImplementation(err)) // 500, Internal Server Error
    })
  }
)

services\book.js中新增电子书更新逻辑:

function updateBook(book) {
  return new Promise(async (resolve, reject) => {
    try {
      if (book instanceof Book) {
        const result = await getBook(book.fileName)
        if (result) {
          const model = book.toDb()
          if (+result.updateType === 0) {
            reject(new Error('内置图书不能编辑'))
          } else {
            await db.update(model, 'book', `where fileName='${book.fileName}'`)
            resolve()
          }
        } else {
          reject(new Error('电子书不存在'))
        }
      } else {
        reject(new Error('添加的图书对象不合法'))
      }
    } catch (e) {
      reject(e)
    }
  })
}

db\index.js中新增数据库更新逻辑:

// 更新
function update(model, tableName, where) {
  return new Promise((resolve, reject) => {
    if (!isObject(model)) {
      reject(new Error('插入数据库失败,插入数据非对象'))
    } else {
      const entry = []
      Object.keys(model).forEach(key => {
        if (model.hasOwnProperty(key)) {
          entry.push(`\`${key}\`='${model[key]}'`)
        }
      })
      if (entry.length > 0) {
        let sql = `UPDATE \`${tableName}\` SET`
        sql = `${sql} ${entry.join(',')} ${where}`
        // debug && console.log(sql)
        const conn = connect()
        try {
          conn.query(sql, (err, result) => {
            if (err) {
              reject(err)
            } else {
              resolve(result)
            }
          })
        } catch (e) {
          reject(e)
        } finally {
          conn.end()
        }
      }
    }
  })
}

完成之后,访问http://localhost:9527/#/book/edit/6bc72db04f7c9c97cab00e3c400849d2即可更新对应已上传电子书的信息(6bc72db04f7c9c97cab00e3c400849d2为后台上传过程中自动生成的电子书文件名)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序边界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值