Vue + Spring Boot 項目實戰(十八):博客功能開發

logo


重要鏈接:
「系列文章目錄」

「項目源碼(GitHub)」

前言

各位朋友們你們好哇,隱約感覺我已經鴿了好幾周了,所以雖然快過年了,我還是決定肝出一篇文章出來。前幾篇文章發佈後好多同學表示看不懂了,但我覺得都到第十八篇了,有些問題確實應該讓你們自己嘗試解決一下。不過放心,這篇文章是特地寫給你們找自信的。

博客可以說是技術人的標配了,有個自己運營的博客網站更是可以在小白麪前裝一波逼。網上有很多開源的博客系統,如 WordPress、Hexo 等,功能強大頁面美觀運行穩定,但畢竟不是自己做的。一開始我也沒想在這個項目里加這個模塊,後來做着做着越來越像 CMS,這就好比你和朋友出去玩他想上廁所你陪他到了門口雖然你並沒有什麼庫存但是來都來了放空一下也不是不可以,所以我就花了幾個晚上把這個功能弄了一下。如果你們有興趣真的可以搞個服務器嘗試當博客運營一下,能學到很多東西。

這篇文章除了講解如何搭出一個博客系統,還設計到如下知識點:

  • 如何使用開源編輯器?
  • Vue 如何在不同頁面傳遞參數?
  • Spring Data JPA 分頁功能如何使用?

一、mavon-editor 編輯器

其實項目一開始的時候我就暗搓搓地安裝了這個編輯器,所以如果你們複製過我的 package.json 文件或者直接從 github 上下的源碼,就不用再安裝了。

目前常見的文本編輯器有兩種,即富文本編輯器和 markdown 編輯器,我一直寫作用的都是 markdown,基本不用動鼠標,而且在各個平臺樣式比較統一,方便遷移。一開始可能覺得語法麻煩,其實用的多的就那麼幾個,寫幾篇就熟悉了。

簡單介紹一下,這個 mavon-editor 編輯器應該是最火的國產開源 markdown 編輯器裏最火的一個,github 3.4k star。功能比較齊全,界面比較舒服,作者也很熱情地解決使用者的問題,我用了一下,暫時沒發現什麼 BUG。倉庫地址:

https://github.com/hinesboy/mavonEditor

readme 提供了 API 文檔。

markdown 編輯器的本質是把你的輸入源(按一定語法規則組織的文本)轉換爲 html 代碼,以在瀏覽器上生動形象地展示,這個過程其實類似於「翻譯」或者說「編譯」。同時作爲一個成熟的應用,又需要一些附加的按鈕、快捷鍵等功能,但其實 markdown 本身就是爲了簡化功能的使用,類似加粗、斜體等操作都有相對應的語法,完全可以直接鍵入,不必要過分使用快捷鍵或按鈕。

二、功能設計

博客功能大概可以分爲三個部分,分別是文章展示文章管理編輯器,文章展示又可以劃分爲文章列表與文章詳情兩個部分。
在這裏插入圖片描述
雖然編輯器提供預覽功能,但一般我們在前臺只不需要向用戶展示 markdown 原文,所以最好還是單獨編寫一個文章詳情頁面渲染出 html。有兩種思路:

  • 第一種,在數據庫中僅保存 markdown 語法的文本,在需要使用時解析爲 html,並在前臺渲染
  • 第二種,markdown、html 均保存在數據庫中,需要使用時取出 html 並在前臺渲染

第一種的好處就是節省傳輸的數據量與數據庫空間,壞處就是需要自己編寫解析方法,相當於又重寫了一遍編輯器,而且難以保證解析出來的樣式跟原編輯器一致(用一些公開的解析函數也存在這個問題)。

當然,如果編輯器提供瞭解析的 API 那就比較舒服了,但我暫時沒找到相關的內容。作者可能並不想這麼做,而是提供了一個可以傳遞 markdown 與 html 值的 save 方法,因此就這個編輯器而言,我覺得選擇第二種方法方便一些。

下面是各個頁面的初步設計與功能介紹:

文章列表:

展示文章的題目、摘要、封面等信息,提供文章詳情頁面入口。主要是前端設計與分頁功能實現,後期可以擴展分類標籤、檢索、歸檔等功能,還可以在側邊欄加入作者簡介等信息。
筆記本
文章詳情:

這個頁面用於展示文章的具體內容,也就是渲染從數據庫中取出的 html。打碼的部分說明了我是一個遵守平臺規則的老實人。
文章詳情
文章管理:

後臺的管理頁面,提供查看、發佈、修改文章的入口以及刪除功能,需要內容管理權限。
文章管理
編輯器:

核心頁面,在開源編輯器的基礎上,添加了標題、摘要及封面設置功能。
編輯器

三、功能實現

1.數據庫設計

爲了保存文章相關的信息,設計 jotter_article 表如下:
文章表
目前包含的字段是 id、標題、文章 html、md 原文、文章摘要、文章標題和發文日期。

2.編輯器的引入與改造

如果之前沒有安裝過編輯器,可以先在項目 wj-vue 根目錄下執行如下命令:

$ npm install mavon-editor --save

再在 main.js 裏全局註冊一下:

import mavonEditor from 'mavon-editor'
...
...
Vue.use(mavonEditor)

即可在組件中使用。考慮到編輯功能應該向具有內容管理權限的用戶使用,我們在 components/admin/content 文件夾下新建一個組件,命名爲 ArticleEditor.vue。該組件的主體就是 mavon-editor 編輯器,最初的狀態如下:

<template>
  <mavon-editor
    v-model="article.articleContentMd"
    style="height: 100%;"
    ref=md
    @save="saveArticles"
    fontSize="16px">
  </mavon-editor>
</template>

<script>
  export default {
    name: 'Editor',
    data () {
      return {
        article: {}
      }
    }
</script>

接下來我們需要對它做一些邪惡的事情,把它改造成我們想要的樣子。改造工序是:

  • 第一步,添加標題輸入欄
  • 第二步,插入自定義工具,提供摘要與封面錄入功能
  • 第三步,編寫 save 方法,與後端交互

實現標題的輸入只需要添加一個 <el-input>

      <el-input
        v-model="article.articleTitle"
        style="margin: 10px 0px;font-size: 18px;"
        placeholder="請輸入標題"></el-input>

插入自定義工具,文檔中並沒有相關內容,但是我尋思肯定有人會問,就搜了一下 issues,果然
插槽
作者在 2018 年 8 月份就這個問題提交了一次更新。當然其實也可以直接看源碼,裏面設置了 4 個插槽,對應不同的插入位置。
插槽
爲了保證我們插入的圖標跟原來的圖標樣式一致,需要再瞅一眼 tool-bar 的源碼。裏面的按鈕大概是這樣寫的

<button type="button"
		v-if="toolbars.save"
		@click="$clicks('save')"
		class="op-icon fa fa-mavon-floppy-o"
        aria-hidden="true"
        :title="`${d_words.tl_save} (ctrl+s)`"></button>

我們照葫蘆畫瓢設置一下 type、class 和 title 屬性,弄一個添加摘要和封面的按鈕:

<button type="button"
		class="op-icon el-icon-document"
		:title="'摘要/封面'"
		slot="left-toolbar-after"
		@click="dialogVisible = true"></button>

其實樣式主要是 class 裏的 op-icon 控制的。

添加摘要和封面的表單被我做在彈出框裏了。上傳文章封面複用之前上傳圖書封面的組件。

<el-dialog
  :visible.sync="dialogVisible"
  width="30%">
  <el-divider content-position="left">摘要</el-divider>
  <el-input
    type="textarea"
    v-model="article.articleAbstract"
    rows="6"
    maxlength="255"
    show-word-limit></el-input>
  <el-divider content-position="left">封面</el-divider>
  <div style="margin-top: 20px">
    <el-input v-model="article.articleCover" autocomplete="off" placeholder="圖片 URL"></el-input>
    <img-upload @onUpload="uploadImg" ref="imgUpload" style="margin-top: 5px"></img-upload>
  </div>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">確 定</el-button>
  </span>
</el-dialog>

界面是這個樣子:
封面、摘要
然後我驚奇地發現圖片上傳不了,好像之前也有同學反映過這個問題,我沒當回事。後來折騰半天,發現是圖片上傳這個組件比較狗,需要單獨設置屬性才能帶上 cookie,不帶 cookie 後端就拿不到 sessionId,就會被 shiro 攔截。修改後的組件模板部分如下:

<template>
  <el-upload
    class="img-upload"
    ref="upload"
    action="http://localhost:8443/api/admin/content/books/covers"
    with-credentials
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :before-remove="beforeRemove"
    :on-success="handleSuccess"
    multiple
    :limit="1"
    :on-exceed="handleExceed"
    :file-list="fileList">
    <el-button size="small" type="primary">點擊上傳</el-button>
    <div slot="tip" class="el-upload__tip">只能上傳jpg/png文件,且不超過500kb</div>
  </el-upload>
</template>

相比之前添加了 with-credentials

最後一步編寫保存方法,常規操作,向後端發送數據即可。

組件完整的代碼如下:

<template>
  <div>
    <el-row style="margin: 18px 0px 0px 18px ">
      <el-breadcrumb separator-class="el-icon-arrow-right">
        <el-breadcrumb-item :to="{ path: '/admin/dashboard'}">管理中心</el-breadcrumb-item>
        <el-breadcrumb-item :to="{ path: '/admin/content/book'}">內容管理</el-breadcrumb-item>
        <el-breadcrumb-item :to="{ path: '/admin/content/article'}">文章管理</el-breadcrumb-item>
        <el-breadcrumb-item>編輯器</el-breadcrumb-item>
      </el-breadcrumb>
    </el-row>
    <el-row>
      <el-input
        v-model="article.articleTitle"
        style="margin: 10px 0px;font-size: 18px;"
        placeholder="請輸入標題"></el-input>
    </el-row>
    <el-row style="height: calc(100vh - 140px);">
      <mavon-editor
        v-model="article.articleContentMd"
        style="height: 100%;"
        ref=md
        @save="saveArticles"
        fontSize="16px">
        <button type="button" class="op-icon el-icon-document" :title="'摘要/封面'" slot="left-toolbar-after"
                @click="dialogVisible = true"></button>
      </mavon-editor>
      <el-dialog
        :visible.sync="dialogVisible"
        width="30%">
        <el-divider content-position="left">摘要</el-divider>
        <el-input
          type="textarea"
          v-model="article.articleAbstract"
          rows="6"
          maxlength="255"
          show-word-limit></el-input>
        <el-divider content-position="left">封面</el-divider>
        <div style="margin-top: 20px">
          <el-input v-model="article.articleCover" autocomplete="off" placeholder="圖片 URL"></el-input>
          <img-upload @onUpload="uploadImg" ref="imgUpload" style="margin-top: 5px"></img-upload>
        </div>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="dialogVisible = false">確 定</el-button>
        </span>
      </el-dialog>
    </el-row>
  </div>
</template>

<script>
  import ImgUpload from './ImgUpload'

  export default {
    name: 'Editor',
    components: {ImgUpload},
    data () {
      return {
        article: {},
        dialogVisible: false
      }
    },
    mounted () {
      if (this.$route.params.article) {
        this.article = this.$route.params.article
      }
    },
    methods: {
      saveArticles (value, render) {
        // value 是 md,render 是 html
        this.$confirm('是否保存併發布文章?', '提示', {
          confirmButtonText: '確定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
            this.$axios
              .post('/admin/content/article', {
                id: this.article.id,
                articleTitle: this.article.articleTitle,
                articleContentMd: value,
                articleContentHtml: render,
                articleAbstract: this.article.articleAbstract,
                articleCover: this.article.articleCover,
                articleDate: this.article.articleDate
              }).then(resp => {
              if (resp && resp.status === 200) {
                this.$message({
                  type: 'info',
                  message: '已保存成功'
                })
              }
            })
          }
        ).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消發佈'
          })
        })
      },
      uploadImg () {
        this.article.articleCover = this.$refs.imgUpload.url
      }
    }
  }
</script>

後端部分首先是 pojo、DAO、service 一套,目前沒什麼可說的,不想自己寫可以參考源碼:

https://github.com/Antabot/White-Jotter/tree/master/wj/src/main/java/com/gm/wj

controller 中保存對應的方法如下:

 @PostMapping("api/admin/content/article")
 public Result saveArticle(@RequestBody JotterArticle article) {
     jotterArticleService.addOrUpdate(article);
     return ResultFactory.buildSuccessResult("保存成功");
 }

前端路由寫法參考:

 {
   path: '/admin/content/editor',
   name: 'Editor',
   component: Editor,
   meta: {
     requireAuth: true
   }
 }

由於編輯器不在後臺管理目錄中,所以不用設置動態加載,雖然前端非要訪問也能訪問,但是反正沒有寫的權限,所以無所謂了。

3.文章列表頁面

這個頁面主要涉及到分頁的問題。之前我們圖書館頁面的分頁是純靠前端進行的,這裏我們用後端來實現一下。

Spring Data 提供了 org.springframework.data.domain.Page 類,該類包含了頁碼、頁面尺寸等信息,可以很方便地實現分頁。我們要做的,就是編寫一個傳入頁碼與頁面尺寸參數的方法,這個方法可以寫在 service 層。

public Page list(int page, int size) {
    Sort sort = new Sort(Sort.Direction.DESC, "id");
    return  jotterArticleDAO.findAll(PageRequest.of(page, size, sort));
}

這裏我們構造了一個 PageRequest 類來配合查詢,sort 參數是可選的,如果報錯了可能是版本問題,較新的版本里取消了公共構造方法,而是用靜態工廠方法代替。將語句替換爲

Sort sort = Sort.by(Sort.Direction.DESC, "id")

即可。接下來編寫 controller 對應方法:

@GetMapping("/api/article/{size}/{page}")
public Page listArticles(@PathVariable("size") int size, @PathVariable("page") int page) {
    return jotterArticleService.list(page - 1, size);
}

這裏 page 是 1 的話其實會查詢到第二頁的內容,而前端組件傳入的參數就是當頁頁碼,所以需要 - 1。隨便輸個參數測試一下,可以看到後端查詢出來的數據結構如下:

{"content":[{"id":1,"articleTitle":"涼風有興","articleContentHtml":"涼風有興,秋月無邊,而我思鄉的情緒好比度日如年。雖然我風流倜儻玉樹臨風,但我還是有聰明的頭腦和強健的臂腕。","articleContentMd":"涼風有興,秋月無邊,而我思鄉的情緒好比度日如年。雖然我風流倜儻玉樹臨風,但我還是有聰明的頭腦和強健的臂腕。","articleAbstract":"涼風有興,秋月無邊,而我思鄉的情緒好比度日如年。","articleCover":"https://i.loli.net/2020/01/16/d2ZlKI1WRE4p7XB.png","articleDate":"2020-01-13"}],
"pageable":
{"sort":{"sorted":true,"unsorted":false,"empty":false},"offset":4,"pageSize":4,"pageNumber":1,"paged":true,"unpaged":false},
"totalElements":5,
"totalPages":2,
"last":true,
"number":1,
"size":4,
"sort":{"sorted":true,"unsorted":false,"empty":false},
"numberOfElements":1,
"first":false,
"empty":false}

結合前端,我們實際上需要的內容只有兩個: content,即數據庫中的內容,totalElements,即總數量。

頁面大小交由前端控制即可。前端的分頁組件可以寫成:

<el-pagination
  background
  layout="total, prev, pager, next, jumper"
  @current-change="handleCurrentChange"
  :page-size="pageSize"
  :total="total">
</el-pagination>

頁面變更時觸發的方法爲:

handleCurrentChange (page) {
  var _this = this
  this.$axios.get('/article/' + this.pageSize + '/' + page).then(resp => {
    if (resp && resp.status === 200) {
      _this.articles = resp.data.content
      _this.total = resp.data.totalElements
    }
  })
}

打開頁面時默認加載第一頁,查詢的方法可以寫成:

loadArticles () {
  var _this = this
  this.$axios.get('/article/' + this.pageSize + '/1').then(resp => {
    if (resp && resp.status === 200) {
      _this.articles = resp.data.content
      _this.total = resp.data.totalElements
    }
  })
}

這樣這個頁面的核心部分就完成了。組件 Articles.vue 可以放在 component/jotter 文件夾中,完整代碼如下:

<template>
  <div style="margin-top: 40px">
    <!--<el-button @click="addArticle()">添加文章</el-button>-->
    <div class="articles-area">
      <el-card style="text-align: left">
        <div v-for="article in articles" :key="article.id">
          <div style="float:left;width:85%;height: 150px;">
            <router-link class="article-link" :to="{path:'jotter/article',query:{id: article.id}}"><span style="font-size: 20px"><strong>{{article.articleTitle}}</strong></span></router-link>
            <el-divider content-position="left">{{article.articleDate}}</el-divider>
            <router-link class="article-link" :to="{path:'jotter/article',query:{id: article.id}}"><p>{{article.articleAbstract}}</p></router-link>
          </div>
          <el-image
            style="margin:18px 0 0 30px;width:100px;height: 100px"
            :src="article.articleCover"
            fit="cover"></el-image>
          <el-divider></el-divider>
        </div>
      </el-card>
    </div>
    <el-pagination
      background
      layout="total, prev, pager, next, jumper"
      @current-change="handleCurrentChange"
      :page-size="pageSize"
      :total="total">
    </el-pagination>
  </div>
</template>

<script>

  export default {
    name: 'Articles',
    data () {
      return {
        articles: [],
        pageSize: 4,
        total: ''
      }
    },
    mounted () {
      this.loadArticles()
    },
    methods: {
      loadArticles () {
        var _this = this
        this.$axios.get('/article/' + this.pageSize + '/1').then(resp => {
          if (resp && resp.status === 200) {
            _this.articles = resp.data.content
            _this.total = resp.data.totalElements
          }
        })
      },
      handleCurrentChange (page) {
        var _this = this
        this.$axios.get('/article/' + this.pageSize + '/' + page).then(resp => {
          if (resp && resp.status === 200) {
            _this.articles = resp.data.content
            _this.total = resp.data.totalElements
          }
        })
      }
    }
  }
</script>

<style scoped>
  .articles-area {
    width: 990px;
    height: 750px;
    margin-left: auto;
    margin-right: auto;
  }

  .article-link {
    text-decoration: none;
    color: #606266;
  }

  .article-link:hover {
    color: #409EFF;
  }
</style>

最後一個要說的地方是向詳情頁面傳入參數,以查詢指定的文章內容。

通過 vue 的 router 傳遞參數有兩種方式。第一種稱爲命名路由傳參,即使用 params,形式如下:

this.$router.push({ name: 'editor', params: { id: 1 }})

這種方式裏面 name 是指在路由中定義的那個 name,而不是頁面路徑。跳轉到的頁面路由不會包含相關參數信息,所以刷新後就丟失了。我們可能會想要把某一篇文章分享給別人,如果採用這種方式,就無法實現鏈接分享,所以這裏採用第二種,成爲查詢傳參,形式如下:

this.$router.push({ path: 'jotter/article', query: { id: 1 }});

結合超鏈接,即可實現點擊標題或摘要傳遞參數到文章詳情頁面並跳轉功能,只要在詳情頁面獲取到該參數並向數據庫發送請求即可。獲取參數的形式如下:

this.$route.query.id

記得修改對應路由信息,之前我們的 /jotter 路由對應的是其它組件,被我改成文章列表了:

 {
   path: '/jotter',
   name: 'Jotter',
   component: Articles
 }

4.文章詳情頁面

這個頁面的需要解決的問題主要是正確渲染 html。上面說過渲染的結果最好與原編輯器中預覽的效果一致,但作者又沒有提供相關接口,沒辦法,只能把編輯器的 css 文件給整到本地了。

文件可以自己從模塊裏找,也可以從

https://github.com/Antabot/White-Jotter/blob/master/wj-vue/src/styles/markdown.css

拷貝,放在 /src/styles/ 文件夾下即可。接着我們在 component/jotter 文件夾中新建 ArticleDetails 組件,在 style 中引入 css:

  @import "../../styles/markdown.css";

然後在渲染 html 的上一層加上 class="markdown-body 即可。

該頁面完整的代碼如下:

<template>
  <div class="articles-area">
    <el-card style="text-align: left;width: 990px;margin: 35px auto 0 auto">
      <div>
        <span style="font-size: 20px"><strong>{{article.articleTitle}}</strong></span>
        <el-divider content-position="left">{{article.articleDate}}</el-divider>
        <div class="markdown-body">
          <div v-html="article.articleContentHtml"></div>
        </div>
      </div>
    </el-card>
  </div>
</template>

<script>
  export default {
    name: 'ArticleDetails',
    data () {
      return {
        article: []
      }
    },
    mounted () {
      this.loadArticle()
    },
    methods: {
      loadArticle () {
        var _this = this
        this.$axios.get('/article/' + this.$route.query.id).then(resp => {
          if (resp && resp.status === 200) {
            _this.article = resp.data
          }
        })
      }
    }
  }
</script>

<style scoped>
  @import "../../styles/markdown.css";
</style>

使用 vue 的 v-html 屬性即可方便地渲染。後端只需要編寫一個對應的方法查詢指定 id 的記錄即可:

@GetMapping("/api/article/{id}")
public JotterArticle getOneArticle(@PathVariable("id") int id) {
    return jotterArticleService.findById(id);
}

5.文章管理頁面

這個頁面在圖書管理頁面的基礎上,需要調整如下內容:

  • 【寫文章】按鈕,跳轉到編輯器頁面
  • 查看操作,跳轉到指定文章詳情頁面
  • 編輯操作,跳轉到編輯器頁面,並傳入文章內容參數
  • 添加分頁組件

新建組件 ArticleManagement 放在 ArticleEditor 同級目錄,代碼如下:

<template>
  <div>
    <el-row style="margin: 18px 0px 0px 18px ">
      <el-breadcrumb separator-class="el-icon-arrow-right">
        <el-breadcrumb-item :to="{ path: '/admin/dashboard' }">管理中心</el-breadcrumb-item>
        <el-breadcrumb-item>內容管理</el-breadcrumb-item>
        <el-breadcrumb-item>文章管理</el-breadcrumb-item>
      </el-breadcrumb>
    </el-row>
    <el-link href="/admin/content/editor" :underline="false" target="_blank" class="add-link">
      <el-button type="success">寫文章</el-button>
    </el-link>
    <el-card style="margin: 18px 2%;width: 95%">
      <el-table
        :data="articles"
        stripe
        style="width: 100%"
        :max-height="tableHeight">
        <el-table-column
          type="selection"
          width="55">
        </el-table-column>
        <el-table-column type="expand">
          <template slot-scope="props">
            <el-form label-position="left" inline>
              <el-form-item>
                <span>{{ props.row.articleAbstract }}</span>
              </el-form-item>
            </el-form>
          </template>
        </el-table-column>
        <el-table-column
          prop="articleTitle"
          label="題目(展開查看摘要)"
          fit>
        </el-table-column>
        <el-table-column
          prop="articleDate"
          label="發佈日期"
          width="200">
        </el-table-column>
        <el-table-column
          fixed="right"
          label="操作"
          width="180">
          <template slot-scope="scope">
            <el-button
              @click.native.prevent="viewArticle(scope.row.id)"
              type="text"
              size="small">
              查看
            </el-button>
            <el-button
              @click.native.prevent="editArticle(scope.row)"
              type="text"
              size="small">
              編輯
            </el-button>
            <el-button
              @click.native.prevent="deleteArticle(scope.row.id)"
              type="text"
              size="small">
              移除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div style="margin: 20px 0 50px 0">
        <el-pagination
          background
          style="float:right;"
          layout="total, prev, pager, next, jumper"
          @current-change="handleCurrentChange"
          :page-size="pageSize"
          :total="total">
        </el-pagination>
      </div>
    </el-card>
  </div>
</template>

<script>
  export default {
    name: 'ArticleManagement',
    data () {
      return {
        articles: [],
        pageSize: 10,
        total: ''
      }
    },
    mounted () {
      this.loadArticles()
    },
    computed: {
      tableHeight () {
        return window.innerHeight - 320
      }
    },
    methods: {
      loadArticles () {
        var _this = this
        this.$axios.get('/article/' + this.pageSize + '/1').then(resp => {
          if (resp && resp.status === 200) {
            _this.articles = resp.data.content
            _this.total = resp.data.totalElements
          }
        })
      },
      handleCurrentChange (page) {
        var _this = this
        this.$axios.get('/article/' + this.pageSize + '/' + page).then(resp => {
          if (resp && resp.status === 200) {
            _this.articles = resp.data.content
            _this.total = resp.data.totalElements
          }
        })
      },
      viewArticle (id) {
        let articleUrl = this.$router.resolve(
          {
            path: '../../jotter/article',
            query: {
              id: id
            }
          }
        )
        window.open(articleUrl.href, '_blank')
      },
      editArticle (article) {
        this.$router.push(
          {
            name: 'Editor',
            params: {
              article: article
            }
          }
        )
      },
      deleteArticle (id) {
        this.$confirm('此操作將永久刪除該文章, 是否繼續?', '提示', {
          confirmButtonText: '確定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
            this.$axios
              .delete('/admin/content/article/' + id).then(resp => {
              if (resp && resp.status === 200) {
                this.loadArticles()
              }
            })
          }
        ).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消刪除'
          })
        })
      }
    }
  }
</script>

<style scoped>
  .add-link {
    margin: 18px 0 15px 10px;
    float: left;
  }
</style>

這裏由於想要實現在新窗口打開文章詳情頁,又要傳入參數,所以需要寫成如下形式:

 viewArticle (id) {
   let articleUrl = this.$router.resolve(
     {
       path: '../../jotter/article',
       query: {
         id: id
       }
     }
   )
   window.open(articleUrl.href, '_blank')
 }

而編輯方法則使用 params 傳入了參數,畢竟不用分享編輯器鏈接給別人。這個頁面在管理模塊,別忘了往數據庫的 menu 表中新增一條記錄。
表
後端新增了一個刪除指定文章的方法:

@DeleteMapping("/api/admin/content/article/{id}")
public Result deleteArticle(@PathVariable("id") int id) {
    jotterArticleService.delete(id);
    return ResultFactory.buildSuccessResult("刪除成功");
}

這樣這個頁面也就完事兒了。

OK,到這裏我要講的東西就結束了。還有很多組件你們可以自己往上加,比如點贊、評論、統計之類。另外有一些查詢方法也可以優化一下,比如文章列表頁面沒有必要查詢全量信息。

下一步

接下來我想着手對項目作一些優化,一直在往上堆東西,網頁加載速度已經慢的不行了。

終於要回家了,雖然我一年休了不少次假,但上次回家還是 7 月份的時候。不瞞你們說,我要工作到初三才能放假,這還是人生頭一次不能在家過年,雖然早就做了心理準備,但到跟前還是有些失落。現在又是這麼個情況,我心裏其實慌的一批。

這次不皮了,希望武漢挺住,全國人民挺住。

上一篇:Vue + Spring Boot 項目實戰(十七):後臺角色、權限與菜單分配

下一篇:Vue + Spring Boot 項目實戰(十九):Web 項目優化解決方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章