Vue实战狗尾草博客管理平台第六章

 源码地址:https://github.com/18291907191/gwc_manage

Vue实现狗尾草博客后台管理系统第六章

本章节内容

  1. 文章列表

  2. 文章详情

  3. 草稿箱

  4. 文章发布。

本章节内容呢,开发的很是随意哈,因为多数就是element-ui的使用,熟悉的童鞋,是可以很快完成本章节的内容的。

为啥文章模块会有这么多东西呢?

因为狗尾草想着以后,文章如果是待发布的话就需要一个地方去存放起来,一开始删除的文章呢,也将会被移入到草稿箱中,这样的话,文章就不会被随便的更改啦。

文章列表

先给大家一张效果图

是不是感觉非常轻松,一个table就可以搞定,

这里的代码呢,我就直接贴出来,因为没有什么值得注意的地方都是基础。

article>list.vue

<template>
  <div class="article-wrap">
    <el-table
    :data="articleList"
    height="100%"
    stripe>

      <el-table-column
      prop="id"
      align="center"
      label="文章编号">
      </el-table-column>

      <el-table-column
      prop="create_time"
      align="center"
      label="创建时间">
        <template slot-scope="scope">
          {{$moment(scope.row.create_time).format('YYYY-MM-DD HH:mm')}}
        </template>
      </el-table-column>

      <el-table-column
      prop="tags"
      align="center"
      label="标签">
        <template slot-scope="scope">
          {{$utils.formatTableFont(scope.row.tags)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="title"
      align="center"
      label="标题">
        <template slot-scope="scope">
          {{$utils.formatTableFont(scope.row.title)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="title_image"
      align="center"
      label="标题图片">
        <template slot-scope="scope">
          <img v-if="scope.row.title_image" class="title-img" :src="scope.row.title_image" />
          <span v-else>-</span>
        </template>
      </el-table-column>

      <el-table-column
      prop="reader_number"
      align="center"
      label="阅读数">
        <template slot-scope="scope">
          {{$utils.formatTableData(scope.row.reader_number)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="good_number"
      align="center"
      label="点赞数">
        <template slot-scope="scope">
          {{$utils.formatTableData(scope.row.good_number)}}
        </template>
      </el-table-column>

      <el-table-column
      label="操作"
      align="center"
      fixed="right">
        <template slot-scope="scope">
          <el-button size="mini" @click.stop="$router.push({path:'/article/detail',query:{articleId:scope.row.id,status:1}})">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      articleList: [],
      params: {
        searchParams: '',
        page: 1,
        size: 10,
        status: 1
      },
    }
  },
  methods: {
    //  获取文章列表
    async getArticleList() {
      try {
        const { articleData } = await this.$http.getRequest('/article/api/v1/article_list',this.params);
        this.articleList = articleData;
      } catch(err) {
        throw new Error('获取文章列表失败',err);
      }
    }
  },
  mounted() {
    this.getArticleList();
  }
}
</script>
<style lang="less" scoped>
.article-wrap {
  height: 100%;
  overflow: hidden;
  /deep/.title-img {
    width: 90px;
    height: 90px;
  }
}
</style>

这里呢,接口呢,都已经完成了。这类先不做说明,后面会单独将node.js抽离出来的哈。

不过呢,这里的formatTableData方法呢,是因为我们在做表格数据显示的时候呢,会有没有数据的情景,所以这里。我封装了一个方法,专门的针对表格的数据进行一个处理,在没有数据的时候呢就显示'-',如果是数字类型的呢,这里就显示0

给大家把方法贴出来,至于如果把方法挂在到全局,前面有讲到啦。这里也不需要这样做,因为直接方法utils文件中,utils挂载到全局,是可以直接使用的了。

utils>plugins.js

import * as http from './http';
import VueCookies from 'vue-cookies'
import moment from 'moment';
import utils from './plugins';

const install = (Vue, opts = {}) => {
  if (install.installed) return;
  Vue.prototype.$http = http;
  Vue.prototype.$cookies = VueCookies;
  Vue.prototype.$moment = moment;
  Vue.prototype.$utils = utils;
}

export default install

utils>index.js

/**
 * @description 封装的工具类
 * @author chaizhiyang
 */
class Util {
  /**
   * 保留小数点后两位
   * @param  {Number} data 需要处理的数值
   * @return {Number} 保留两位小数的数值
   * @author Czy 2018-10-25
   */
  returnFloat(data) {
    return data.toFixed(2)
  }

  //el-table表格数据的处理
  formatTableFont(val) {
    //格式化数据,为空或0或null时,显示无
    let formatTableData;
    if (!val) {
      formatTableData = "-";
    } else {
      formatTableData = val;
    }
    return formatTableData;
  };

  //el-table表格数据的处理
  formatTableData(val) {
    //格式化数据,为空或0或null时,显示无
    let formatTableData;
    if (!val) {
      formatTableData = "0";
    } else {
      formatTableData = val;
    }
    return formatTableData;
  };

  // 返回性别
  sexStatus(status) {
    if (!status) return
    switch (status) {
      case 1:
        return '';
        break;
      case 2:
        return '';
        break;
      default:
        return '未知';
        break;
    }
  }

  /**
   * 正则验证
   * @param {Number,String} str 需要验证的内容如:手机号,邮箱等
   * @param {String} type 需要正则验证的类型
   * @return {Boolean} true: 正则通过,输入无误。false: 正则验证失败,输入有误
   * @author Czy 2018-10-25
   */
  checkStr(str, type) {
    switch (type) {
      case 'phone': //手机号码
        return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
      case 'tel': //座机
        return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
      case 'card': //身份证
        return /^\d{15}|\d{18}$/.test(str);
      case 'account': //账号 ,长度4~16之间,只能包含数字,中文,字母和下划线
        return /^(\w|[\u4E00-\u9FA5])*$/.test(str);
      case 'pwd': //密码以字母开头,长度在6~18之间,只能包含字母、数字和下划线
        return /^[a-zA-Z]\w{6,18}$/.test(str);
      case 'postal': //邮政编码
        return /[1-9]\d{5}(?!\d)/.test(str);
      case 'QQ': //QQ号
        return /^[1-9][0-9]{4,9}$/.test(str);
      case 'email': //邮箱
        return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
      case 'money': //金额(小数点2位)
        return /^\d*(?:\.\d{0,2})?$/.test(str);
      case 'URL': //网址
        return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str);
      case 'IP': //IP
        return /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/.test(str);
      case 'date': //日期时间
        return /^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})(?:\:\d{2}|:(\d{2}):(\d{2}))$/.test(str) || /^(\d{4})\-(\d{2})\-(\d{2})$/.test(str);
      case 'number': //数字
        return /^[0-9]$/.test(str);
      case 'english': //英文
        return /^[a-zA-Z]+$/.test(str);
      case 'chinese': //中文
        return /^[\u4E00-\u9FA5]+$/.test(str);
      case 'lower': //小写
        return /^[a-z]+$/.test(str);
      case 'upper': //大写
        return /^[A-Z]+$/.test(str);
      case 'HTML': //HTML标记
        return /<("[^"]*"|'[^']*'|[^'">])*>/.test(str);
      default:
        return true;
    }
  }

  /**
   * 类型判断
   * @param {*} o 进行判断的内容
   * @return {Boolean} true: 是该类型,false: 不是该类型
   * @author Czy 2018-10-25
   */
  isString(o) { //是否字符串
    return Object.prototype.toString.call(o).slice(8, -1) === 'String'
  }

  isNumber(o) { //是否数字
    return Object.prototype.toString.call(o).slice(8, -1) === 'Number'
  }

  isObj(o) { //是否对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Object'
  }

  isArray(o) { //是否数组
    return Object.prototype.toString.call(o).slice(8, -1) === 'Array'
  }

  isDate(o) { //是否时间
    return Object.prototype.toString.call(o).slice(8, -1) === 'Date'
  }

  isBoolean(o) { //是否boolean
    return Object.prototype.toString.call(o).slice(8, -1) === 'Boolean'
  }

  isFunction(o) { //是否函数
    return Object.prototype.toString.call(o).slice(8, -1) === 'Function'
  }

  isNull(o) { //是否为null
    return Object.prototype.toString.call(o).slice(8, -1) === 'Null'
  }

  isUndefined(o) { //是否undefined
    return Object.prototype.toString.call(o).slice(8, -1) === 'Undefined'
  }

  isFalse(o) {
    if (o == '' || o == undefined || o == null || o == 'null' || o == 'undefined' || o == 0 || o == false || o == NaN) {
      return true
    }
    return false
  }

  isTrue(o) {
    return !this.isFalse(o)
  }
}

export default new Util();

这里直接挂在到全局。使用方法呢就是this.$utils.func就可以了

文章详情

这里的主要功能呢就是根据id去回显该文章的所有信息,并可以进行修改,删除,移入草稿箱等得操作。这里呢,因为详情和发布是相同的,所以呢,这里也就发一份。当然了就有同学问我,为什么不讲两个页面放到一个页面中呢。

这里给大家解释一下哈:

项目初期往往会比较简单,大多人选择将相同的地方进行封装,给前妻开发带来很大方便,但是项目越往后,会发现,在一个页面反复的添加,修改判断。页面的逻辑处理变得十分复杂,便后悔当初没有还不如分开处理。当然,另一个原因就是,这里路由也做了懒加载,我们在不进入另一个路由的同时呢。他是不会被加载的。性能损耗而言呢,也就是多占了份空间,当然,不要为了封装而封装,不要过度封装。适用才是最合适的!

article>publish

<template>
  <section class="wraper">
    <el-form ref="form" :model="form" label-width="92px" :rules="rules">
    <!--S 标题 -->
      <admin-title :title="title.tit1"></admin-title>
      <el-form-item label="Article Title" prop="title">
        <el-col :span="6">
          <el-input v-model="form.title"></el-input>
        </el-col>
      </el-form-item>
      <el-form-item label="Title Image" prop="title_image"
      >
        <el-col :span="6">
          <el-input v-model="form.title_image"></el-input>
        </el-col>
      </el-form-item>
      <admin-title :title="title.tit2"></admin-title>
      <el-form-item label="Article Tags" prop="tags">
        <el-col :span="6">
          <el-select
            style="width: 100%;"
            v-model="form.tags"
            multiple
            filterable
            allow-create
            default-first-option
            placeholder="请选择文章标签">
            <el-option
              v-for="item in tagList"
              :key="item.id"
              :label="item.tag"
              :value="item.id">
            </el-option>
          </el-select>
        </el-col>
      </el-form-item>
      <admin-title :title="title.tit3"></admin-title>
      <el-form-item label="Abstract" prop="describe" align="left">
        <textarea class="abstract" v-bind:maxlength="190" v-model="form.describe" rows="5" cols="100" type="text" name="abstract">
        </textarea>
        <span style="font-size:16px;"><font style="color: #3576e0;">{{190 - form.describe.length}}</font>/190</span>
      </el-form-item>
      <el-form-item label="Content" prop="content">
        <mavon-editor v-model="form.content"/>
      </el-form-item>
      <el-form-item align="left">
        <el-col>
          <el-button type="primary" @click.native="handleSubmit('rules')" :loading="buttonLoading.publishLoading">文章发布</el-button>
          <el-button type="primary" @click.native="handleMoveDraft('rules')" :loading="buttonLoading.draftLoading">保存草稿</el-button>
        </el-col>
      </el-form-item>
    </el-form>
  </section>
</template>
<script>
import AdminTitle  from '@/components/commons/Title';

export default {
  components: {
    AdminTitle,
  },
  watch: {
    'form.describe'(curVal, oldVal) {
      if (curVal.length > this.textNum) {
        this.textareaValue = String(curVal).slice(0, this.textNum);
      }
    }
  },
  data() {
    return {
      title: {
        tit1: '文章标题',
        tit2: '文章标签',
        tit3: '文章摘要',
      }, //标题
      form: {
        title: '',
        tags: [],
        title_image: '',
        describe: '',
        content: '',
        status: 1,
      }, //提交数据
      tagList: [], //标签选择器
      textNum: 200,
      previewMarkdown: '<h1>测试</h1>',
      buttonLoading: {
        publishLoading: false,
        draftLoading: false
      },
      rules: {
        title: [
          { required: true, message: '请输入文章标题', trigger: 'blur'}
        ],
        title_img: [
          { required: false, message: '请输入标题图片', trigger: 'blur'}
        ],
        tags: [
          { required: false, message: '请选择文章标签', trigger: 'change'}
        ],
        describe: [
          { required: true, message: '请输入文章摘要', trigger: ['change','blur']}
        ],
        content: [
          { required: true, message: '请输入文章内容', trigger: ['blur','change']}
        ]
      },  //  表单规则校验
    }
  },
  methods: {
    //发布文章
    async handleSubmit() {
      let isOk = this.validata();
      if(!isOk) {
        return ;
      }
      this.form.status = 1;
      this.publishLoading = true;
      try {
        const result = await this.$http.postRequest('/article/api/v1/article_add',this.form);
        this.publishLoading = false;
        this.$message({
          type: 'success',
          message: '文章发布成功!'
        })
        this.$router.push({
          path: '/article/list'
        })
      } catch(err) {
        throw new Error('文章更新失败',err);
        this.publishLoading = false;
      }
    },
    // 保存草稿
    async handleMoveDraft() {
      this.form.status = 2;
      this.publishLoading = true;
      try {
        const result = await this.$http.postRequest('/article/api/v1/article_add',this.form);
        this.publishLoading = false;
        this.$message({
          type: 'success',
          message: '保存草稿箱成功!'
        })
        this.$router.push({
          path: '/article/draft'
        })
      } catch(err) {
        this.publishLoading = false;
        throw new Error('保存草稿失败',err);
      }
    },
    // 表单校验
    validata() {   
      let isForm;
      this.$refs.form.validate(valid => {
        isForm = valid;
      });
      if (!isForm) {
        return false;
      }
      return true;
    },
    //  获取文章所有标签
    getTags() {
      let hash  = {};
      let arr = [];
      axios.get('/article/api/v1/articleTags')
      .then(res => {
        arr = res.reduce((item,next) => {
          hash[next.tag] ? '' : hash[next.tag] = true && item.push(next);
          return item;
        },[]);
        this.tagList = arr;  
      })
    }
  },
}
</script>
<style lang="less" scoped>
.wraper {
  width: 100%;
  height: 100%;
  .abstract {
    padding: 10px;
    font-size: 14px;
  }
  /deep/.el-form-item__label {
    text-align: left;
    padding-right: 0;
  }
}

</style>

这里有一个重点,需要大家着重记一下的是,表单的校验,我们在添加好了以后,往往在需要提交的时候去进行判断,将不符合规则的表单给提示出来。不会使用的人,便会去写很多的if判断,this.$message({type:'error',message:'xxx'})的方法给出来,

但是element_ui明明已经给了一个合理的解决方案了。大家就要学会去使用,给我们带来便捷!

  // 表单校验
    validata() {   
      let isForm;
      this.$refs.form.validate(valid => {
        isForm = valid;
      });
      if (!isForm) {
        return false;
      }
      return true;
    },

这块的表单校验,通过给 form表单起一个名称,在提交的时候,调用validate方法就可以方便的达到校验表单的效果。根据返回结果去判断是否继续往下执行就可以啦。get到了有木有。

封装的一个subtitle组件

compoents/commons/Title.vue

<template>
  <p class="title">{{title}}</p>
</template>

<script>
export default {
  name: "AdminTitle",
  props: {
    title: String
  },
  data () {
    return {
      
    };
  }
};
</script>

<style lang="less">
.title {
    display: flex;
    align-items: center;
    margin: 20px 0;
    color: #333;
    position: relative;
    &::before {
      content: "";
      display: inline-block;
      position: absolute;
      left: -15px;
      width: 2px;
      height: 13px;
      background-color: #3576e0;
      border-radius: 1px;
    }
}
</style>

这里呢,狗尾草选择使用了<mavon-editor v-model="form.content"/>富文本编辑器,富文本编辑器很多哈。这里就不做特殊说明,有使用遇到坎坷的童鞋呢,可以留言咨询哦。(大家可以查看后期我的react前端文章详情的回显效果)

草稿箱

这里的草稿箱呢,其实表面上看和列表页是一样的。但是呢。文章没有写完的依旧可以放在草稿箱中。待发布的也可以放在草稿箱中,这也就是像个完全不同功能的模块了。

article>draft.vue

<template>
  <div class="article-wrap">
    <el-table
    :data="articleList"
    height="100%"
    stripe>

      <el-table-column
      prop="id"
      align="center"
      label="文章编号">
      </el-table-column>

      <el-table-column
      prop="create_time"
      align="center"
      label="创建时间">
        <template slot-scope="scope">
          {{$moment(scope.row.create_time).format('YYYY-MM-DD HH:mm')}}
        </template>
      </el-table-column>

      <el-table-column
      prop="tags"
      align="center"
      label="标签">
        <template slot-scope="scope">
          {{$utils.formatTableFont(scope.row.tags)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="title"
      align="center"
      label="标题">
        <template slot-scope="scope">
          {{$utils.formatTableFont(scope.row.title)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="title_image"
      align="center"
      label="标题图片">
        <template slot-scope="scope">
          <img v-if="scope.row.title_image" class="title-img" :src="scope.row.title_image" />
          <span v-else>-</span>
        </template>
      </el-table-column>

      <el-table-column
      prop="reader_number"
      align="center"
      label="阅读数">
        <template slot-scope="scope">
          {{$utils.formatTableData(scope.row.reader_number)}}
        </template>
      </el-table-column>

      <el-table-column
      prop="good_number"
      align="center"
      label="点赞数">
        <template slot-scope="scope">
          {{$utils.formatTableData(scope.row.good_number)}}
        </template>
      </el-table-column>

      <el-table-column
      label="操作"
      align="center"
      fixed="right">
        <template slot-scope="scope">
          <el-button size="mini" @click.stop="$router.push({path:'/article/detail',query:{articleId:scope.row.id,status:2}})">编辑</el-button>
          <el-button size="mini" type="danger" @click.stop="handleDeleteDraft(scope.row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      articleList: [],
      params: {
        searchParams: '',
        page: 1,
        size: 10,
        status: 2
      }
    }
  },
  methods: {
    //获取文章列表
    async getArticleList() {
      try {
        const { articleData } = await this.$http.getRequest('/article/api/v1/article_list',this.params);
        this.articleList = articleData;
      } catch(err) {
        throw new Error('获取文章列表失败',err);
      }
    },
    handleDeleteDraft(id) {
      this.$confirm('此操作将永久删除该文章,不可复原, 是否继续?', '删除提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(async () => {
        try {
          const result = await this.$http.postRequest('/article/api/v1/article_delete',{ id });
          this.$message({
            type: 'success',
            message: '文章已删除!'
          })
          this.getArticleList();
        } catch(err) {
          throw new Error('删除草稿失败',err);
        }
        this.$message({
          type: 'success',
          message: '删除成功!'
        });
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });          
      });
    }
  },
  mounted() {
    this.getArticleList();
  }
}
</script>
<style lang="less" scoped>
.article-wrap {
  height: 100%;
  overflow: hidden;
  /deep/.title-img {
    width: 90px;
    height: 90px;
  }
}
</style>

这里给大家理一下这里的思路哈。

文章列表可编辑,编辑时,可选择将文章进行更新发布或者移入草稿箱。发布没有啥说的,移入草稿箱呢,其实也就是将该文章的状态进行更改。

在草稿箱中,主需要根据状态去查询文章即可。 但是草稿箱中的删除操作也就会将文章彻底的删除。

最后呢,附上更改后的路由

router>index.js

import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)

const _import = file => () => import('@/pages/' + file + '.vue');
const _import_ = file => () => import('@/components/' + file + '.vue');

const asyncRouterMap = [];

const constantRouterMap = [
  {
    path: '/login',
    name: 'Login',
    component: _import('login/index'),
  },
  {
    path: '/',
    name: '概况',
    component: _import_('commons/Layout'),
    redirect: '/index',
    children: [
      {
        path: '/index',
        name: '总览',
        component: _import('home/index'),
        meta: {
          isAlive: false,
          auth: true,
          title: '概况数据'
        }
      }
    ]
  },
  {
    path: '/article',
    name: '文章',
    component: _import_('commons/Layout'),
    redirect: '/article/publish',
    children: [
      {
        path: '/article/publish',
        name: '文章发布',
        component: _import('article/publish'),
        meta: {
          auth: true,
          isAlive: true,
          isFooter: false,
          title: '文章发布'
        }
      },
      {
        path: '/article/list',
        name: '列表',
        component: _import('article/list'),
        meta: {
          auth: true,
          isAlive: false,
          isFooter: true,
          title: '列表'
        }
      },
      {
        path: '/article/draft',
        name: '草稿箱',
        component: _import('article/draft'),
        meta: {
          auth: true,
          isAlive: false,
          isFooter: true,
          title: '草稿箱'
        }
      },
      {
        path: '/article/detail',
        name: '文章详情',
        component: _import('article/detail'),
        meta: {
          auth: true,
          isAlive: false,
          isFooter: false,
          title: '文章详情'
        }
      }
    ]
  },
  {
    path: '/404',
    name: '404',
    component: _import('error/index'),
    meta: {
      title: "请求页面未找到",
      auth: false
    },
  },
  {
    path: '*',
    meta: {
      title: "请求页面未找到",
      auth: false
    },
    redirect: '/404'
  }
];

const router = new Router({
  mode: 'history',
  routes: constantRouterMap,
  linkActiveClass: "router-link-active",
});

export default router

总结

1.表单提交时的校验。

2.不要为了封装而封装。避免过度封装。适用才是王道。

下一章节

Vuex的进阶使用

posted @ 2019-08-13 09:48  狗尾草的博客  阅读(561)  评论(0编辑  收藏  举报