Vue實戰狗尾草博客管理平臺第六章

 

Vue實現狗尾草博客後臺管理系統第六章

本章節內容前端

  1. 文章列表vue

  2. 文章詳情node

  3. 草稿箱react

  4. 文章發佈。ios

本章節內容呢,開發的非常隨意哈,由於多數就是element-ui的使用,熟悉的童鞋,是能夠很快完成本章節的內容的。vue-router

爲啥文章模塊會有這麼多東西呢?element-ui

由於狗尾草想着之後,文章若是是待發布的話就須要一個地方去存放起來,一開始刪除的文章呢,也將會被移入到草稿箱中,這樣的話,文章就不會被隨便的更改啦。axios

文章列表

先給你們一張效果圖api

是否是感受很是輕鬆,一個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的進階使用

相關文章
相關標籤/搜索