VUE.JS和NODE.JS構建一個簡易的前後端分離靜態博客系統(二)

後臺管理頁面,需要配合NODE.JS搭建的EXPRESS服務器使用。

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { 
  Button,
  Input,
  Form,
  Link,
  Divider,
  Upload,
  Dialog,
  Card,
  Popover,
  MessageBox,
  Message,
  Loading,
  Breadcrumb,
  BreadcrumbItem,
  Select,
  Option,
  Table,
  TableColumn,
  Avatar,
  Pagination,
  Checkbox,
  CheckboxGroup,
} from 'element-ui';

// 局部引入必須這麼做才能正常使用
Vue.prototype.$message = Message
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$loading = Loading.service

Vue.use(Button)
Vue.use(Input)
Vue.use(Form)
Vue.use(Link)
Vue.use(Divider)
Vue.use(Upload)
Vue.use(Dialog)
Vue.use(Card)
Vue.use(Popover)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Select)
Vue.use(Option)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Avatar)
Vue.use(Pagination)
Vue.use(Checkbox)
Vue.use(CheckboxGroup)

Vue.config.productionTip = false

Vue.prototype.$url_posts = "http://localhost:8081/posts"
Vue.prototype.$url_categories = "http://localhost:8081/categories"

new Vue({
  render: h => h(App),
  router,
}).$mount('#app')

這段代碼主要執行了以下操作:

  1. 導入所需的庫和組件:

    • 導入 Vue 核心庫。
    • 導入根組件 App.vue。
    • 導入路由配置文件。
    • 從 Element UI 庫中導入了一系列 UI 組件,如 Button、Input、Form 等。
  2. 配置 Vue 全局屬性:

    • 將 Message、MessageBox 和 Loading 服務掛載到 Vue 原型對象上,這樣可以在整個應用中直接使用它們,例如 this.$messagethis.$confirmthis.$loading
  3. 註冊 UI 組件:

    • 使用 Vue.use() 方法註冊導入的 Element UI 組件,使其可以在 Vue 應用中全局使用。
  4. 關閉 Vue 生產環境的提示信息:

    • 設置 Vue.config.productionTip = false,以關閉生產環境下的提示信息。
  5. 定義全局 API 地址:

    • 在 Vue 原型對象上定義了 $url_posts$url_categories 兩個屬性,分別用於存儲 post 和 category 相關 API 的 URL。這樣在整個應用中都可以方便地訪問這兩個 URL。
  6. 創建 Vue 實例並掛載:

    • 創建一個新的 Vue 實例,指定渲染函數以將根組件 App.vue 渲染到頁面上,並傳入路由配置。最後將 Vue 實例掛載到頁面的 '#app' 元素上。

簡言之,這段代碼的主要作用是:

  • 導入所需的庫和組件。
  • 配置 Vue 全局屬性和 UI 組件。
  • 定義全局 API 地址。
  • 創建 Vue 實例並將其掛載到頁面上。

App.vue

<template>
  <div id="app">
    <header>
      <div class="header-left">
        <ul>
          <li
            :class="{ active: activeRoute === 'Management' }"
            class="nav-btn clickable"
            @click="jump2('Management')"
          >管理
          </li>
          <li
            :class="{ active: activeRoute === 'Edit' }"
            class="nav-btn clickable"
            @click="jump2('Edit')"
          >新隨筆
          </li>
          <li
            :class="{ active: activeRoute === 'Edit2' }"
            class="nav-btn auto"
          >編輯
          </li>
          <li
            :class="{ active: activeRoute === 'Category' }"
            class="nav-btn clickable"
            @click="jump2('Category')"
          >分類
          </li>
        </ul>
      </div>
      <div class="header-right">
        <ul>
          <li>
            <div id="user" slot="reference">
              <el-avatar
                icon="el-icon-user-solid"
                style="display: inline-block"
              ></el-avatar>
            </div>
          </li>
        </ul>
      </div>
    </header>
    <main>
      <router-view></router-view>
    </main>
  </div>
</template>

<script>
export default {
  name: "App",
  computed: {
    activeRoute() {
      return this.$route.name
    }
  },
  methods: {
    jump2(router_name) {
      if (this.activeRoute !== router_name) {
        this.$router.push({ name: router_name });
      }  
    },
    switch2management() {
      // 棄用,待修改
      if (this.activeRoute !== 'Management') {
        if (this.activeRoute === 'Edit2') {
          this.$confirm('跳轉頁面將丟失未保存的修改內容,確認跳轉?', '提示', {
            confirmButtonText: '確定',
            cancelButtonText: '取消',
            type: 'warning',
          }).then(() => {
            this.$router.push({ name: "Management" });
          })          
        } else {
          this.$router.push({ name: "Management" });
        }     
      }  
    },
  },
};
</script>

<style>
/* 全局樣式在這裏設置,其它一律scoped */
body {
  margin: 0;
}

#app {
  /* font-family: Avenir, Helvetica, Arial, sans-serif; */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* text-align: center; */
  /* color: #2c3e50; */
  /* margin-top: 60px; */
}
</style>

<style scoped>
  #app {
    background: white;
    /* 設定最大寬度,並且居中 */
    max-width: 1380px;
    min-width: 450px;
    margin: 0 auto;
    padding: 0 15px;

    /* 用下面的grid配置在打開有element表格的頁面有一個神奇的無限往右延長的BUG */
    /* display: grid;
    grid-template: 65px 1fr / 25px 1fr 25px; */

    display: flex;
    flex-direction: column;

    font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微軟雅黑",Arial,sans-serif;    
  }

  header {
    /* grid-column: 2 / 3; */
    margin: 10px 0;

    display: flex;
    justify-content: space-between;
    align-items: center;

    /* 下面這個線畫到在main上畫 */
    /* border-bottom: 1px solid #dcdfe6;    */
  }

  ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
  }

  li {
    display: inline-block;
    box-sizing: border-box;
    /* li之間互相應該有一定間隙 */
    margin: 5px;

    user-select: none;
  }

  .nav-btn {
    /* 按鈕靜默狀態樣式 */

    /* 上下10px 左右22px */
    padding: 10px 22px; 
        
    /* 字體影響非常非常大................ */
    font-size: 18px;
    font-family: 'Courier New', Courier, monospace;

    /* 特效之靜默狀態 */
    opacity: 0.5;
    border: 1.3px solid #292b2d56;
    border-radius: 5%; 
  }

  .nav-btn.clickable {
    cursor: pointer;
    /* 保持與element一致 */
    color: #409EFF;
    transition: 0.3s ease-in-out;
  }

  .nav-btn.clickable:hover, .nav-btn.clickable.active {  
    /* 鼠標懸浮時亮起,0字體改變,1是變成不透明,2是邊框顏色改變,4懸浮 */
    color: #409EFF;

    opacity: 1;
    border: 1.3px solid #409EFF;
    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);

    transition: 0.3s ease-in-out;
  }

  .nav-btn.auto {
    color: #67C23A;
  }

  .nav-btn.auto.active {
    color: #67C23A;

    opacity: 1;
    border: 1.3px solid #67C23A;
    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);

    transition: 0.3s ease-in-out;
  }

  div#user {
    cursor: pointer;
  }

  main {
    border-top: 1px solid #dcdfe6;   
  }
</style>

這段代碼是一個Vue.js的單文件組件,包括了一個template、一個script和兩個style塊。這個組件表示了一個頁面的佈局和導航,其中包括一個頭部和一個主體。頭部包括了一個左側的導航欄和一個右側的用戶頭像。左側導航欄包括了四個導航按鈕,分別是“管理”、“新隨筆”、“編輯”和“分類”。主體部分是一個router-view組件,用於顯示路由對應的內容。

在script部分,該組件的名字是“App”,定義了兩個方法:jump2和switch2management,以及一個computed屬性activeRoute。activeRoute返回當前路由的名稱,jump2方法根據傳入的路由名稱進行跳轉,而switch2management方法目前被棄用了。

在style部分,第一個style塊是全局樣式的設置,第二個style塊是局部樣式,它們都是通過scoped屬性限定了作用域。該組件的整體樣式是一個白色背景,最大寬度爲1380像素,居中顯示。頭部採用了flex佈局,左側導航欄使用了inline-block佈局,右側用戶頭像使用了slot插槽。導航按鈕有靜默狀態和懸浮狀態,點擊後會有對應的路由跳轉。主體部分有一個上邊框,用於分隔頭部和主體。

vue-my-cnblog\src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Management from '@/page/Management.vue'
import Edit from '@/page/Edit.vue'
import Edit2 from '@/page/Edit2.vue'
import PostInfo from '@/page/PostInfo.vue'
import Category from '@/page/Category.vue'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      name: 'Management',
      component: Management,
    },
    {
        path: '/edit',
        name: 'Edit',
        component: Edit,
        // 這個新隨筆頁面,用戶可以主動進入
    },
    {
      path: '/edit2',
      name: 'Edit2',
      component: Edit2,
      // 真 · 編輯頁面,用戶不能主動進入
    },
    {
      path: '/postinfo',
      name: 'PostInfo',
      component: PostInfo,
    },
    {
      path: '/category',
      name: 'Category',
      component: Category,
    },
  ],
})

export default router

這段代碼使用了 Vue.js 和 Vue Router 插件,定義了一個路由器實例 router,並指定了多個路由對象作爲它的配置項,每個路由對象包含了路徑、名稱和組件。其中:

  • '/' 表示默認路徑,對應的組件是 Management.vue。
  • '/edit' 路徑表示進入編輯頁面,對應的組件是 Edit.vue。
  • '/edit2' 路徑表示真正的編輯頁面,對應的組件是 Edit2.vue。
  • '/postinfo' 路徑表示文章信息頁面,對應的組件是 PostInfo.vue。
  • '/category' 路徑表示文章分類頁面,對應的組件是 Category.vue。

路由器實例通過 Vue.use(VueRouter) 進行初始化,並使用 new VueRouter(options) 來創建,其中 options 包含了多個路由配置。這個路由器實例最終會被導出並在其他模塊中使用。

需要注意的是,Edit2 路徑對應的組件是不可以直接通過 URL 訪問的,只能在代碼中被調用。

Management.vue

<template>
  <div id="management">
    <main>
      <table>
          <thead>
            <tr>
              <th>標題</th>
              <th>發佈時間</th>
              <th>發佈狀態</th>
              <th>操作1</th>
              <th>操作2</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="post in posts" :key="post.id">
              <td><div class="post_title" @click="postInfo(post)">{{ post.title }}</div></td>
              <td>{{ post.pubDate }}</td>
              <td>{{ post.state }}</td>
              <td><div class="btn" @click="editPost(post)">編輯</div></td>
              <td><div class="btn" @click="removePost(post)">刪除</div></td>
            </tr>
          </tbody>      
      </table>      
    </main>
    <footer>
      <el-pagination
        background
        layout="prev, pager, next"
        :current-page.sync="currentPage"
        :page-size="5"
        :total="total">
      </el-pagination>
    </footer>
  </div>
</template>

<script>
import axios from 'axios'
// import qs from 'qs'

const PAGE_SIZE = 5

export default {
  name: 'Management',
  data() {
    return {
        users: [],
        title: '',
        content: '',
        currentPage: 1,
        allPosts: [],
    }
  },
  computed: {
    total() {
      return this.allPosts.length;
    },
    start() {
      return (this.currentPage - 1) * PAGE_SIZE
    },
    end() {
      return this.currentPage * PAGE_SIZE
    },
    posts() {
      // 變化,例如說,刪除一條,那麼allposts變化導致,total和posts變化
      return this.allPosts.slice(this.start, this.end)
    }
  },
  created() {    
    console.log('Management created')
    this.reloadPosts()
  },
  methods: {
    reloadPosts() {
      axios.get(this.$url_posts)
        .then(resp => {         
          // console.log(resp.data)
          // 接受到數據後進行格式化,每當數據更新應該調用該方法
          if (resp.data) {
            this.allPosts = resp.data.map(post => {
              return {
                id: post.id,
                title: post.title,
                createTime: new Date(post.createTime).toLocaleString(),
                category: [], 
                state: post.state, 
                pubDate: new Date(post.pubDate).toLocaleString(),                   
              }
            })
          }
        })
        .catch(err => {
          console.log(err)
        })        
    },
    postInfo(post) {
      const POST_ID = post.id
      if (this.$route.name !== 'PostInfo') {
        this.$router.push({ 
          name: "PostInfo",
          query: {
            post_id: POST_ID,
          },
        });
      }  
    },
    removePost(post) {
      const POST_ID = post.id
      console.log('removePost: ' + POST_ID)
      // 確認一下
      this.$confirm('此操作將永久刪除該文件, 是否繼續?', '提示', {
        confirmButtonText: '確定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        // 把屏幕鎖了防止亂點
        const LOADING = this.$loading({
          lock: true,
          text: '正在刪除',
          // spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });  
        // 發送刪除文件的請求
        axios.delete(this.$url_posts + `/${POST_ID}`)
          .then(() => {                          
            setTimeout(() => {
              // 刷新 
              this.reloadPosts()
              // 至少鎖1秒才解除
              LOADING.close();
              this.$message({
                type: 'success',
                message: '刪除成功!',
              });                                              
            }, 1000);                         
          })
          .catch(err => {
            console.log(err)
          })  
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消刪除'
        });          
      });         
    },
    editPost(post) {
      const POST_ID = post.id
      // 跳轉到對應的文件詳情頁
      if (this.$route.name !== 'Edit2') {
        this.$router.push({ 
          name: "Edit2",
          query: {
            post_id: POST_ID,
          },
        });
      }            
    },
    editContent(id) {
      axios.get(`http://localhost:8081/users/${id}`)
        .then(resp => {          
          this.content = resp.data
        })
        .catch(err => {
          console.log(err)
        })      
    },
  },
}
</script>

<style scoped>
  table {
    width: 100%;
    table-layout: fixed;
    /* fixed之後默認平均分配位置,不會按內容分配 */
    border-collapse: collapse;
    /* collapse只有設置border1時會把那個空餘的地方變成線,其它時候好像沒啥用 */
  }

  thead, tbody {
    /* font-size: 14px;     */
    font-family:'Courier New', Courier, monospace;
    text-align: center;
  }

  th, td {
    padding: 10px;
    /* 看上去寬鬆點 */
    border: 1px solid black;
  }

  th {
    letter-spacing: 2px;
    /* 和td區分一下 */
  }

  thead th:nth-child(1) {
    width: 50%;
  }  

  div.post_title {
    height: 50px;

    overflow-y: hidden;

    display: flex;
    justify-content: center;
    align-items: center;

    cursor: pointer;
    user-select: none;
  }

  div.post_title:hover {
    overflow-y: visible;

    color: #409EFF;
    text-decoration: underline;
  }

  div.btn {
    cursor: pointer;
    border: 1px solid black;
    /* 調整按鈕大小 */
    padding: 5px;
    border-radius: 10px;
  }

  div.btn:hover {
    color: #409EFF;
    border-color: #c6e2ff;
    background-color: #ecf5ff;
  }

  footer {
    height: 50px;

    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>

這段代碼是一個Vue.js組件,包括一個HTML模板和一個JavaScript腳本以及一個局部的CSS樣式。

該組件創建了一個管理界面,包括一個帶有標題的表格和一個分頁組件。表格包含了標題、發佈時間、發佈狀態以及兩個操作按鈕。分頁組件用於分頁顯示所有的文章。

數據部分包括一個名爲“allPosts”的數組,其中存儲了所有文章的信息。在組件創建後,它使用Axios庫從服務器中獲取文章列表,並將其格式化爲所需的格式。在點擊“編輯”或“刪除”按鈕時,將使用Axios庫向服務器發送請求來執行相應的操作。

HTML模板包含一個帶有唯一ID“management”的div元素,其中包含一個主區域和一個頁腳區域。主區域包含一個帶有標題的表格,表格頭包含標題、發佈時間、發佈狀態和兩個操作按鈕。表格主體使用Vue的v-for指令將文章列表中的每一篇文章都渲染爲一個表格行。頁腳區域包含一個分頁組件,可以用於分頁顯示所有文章。

JavaScript腳本定義了一個Vue.js組件,名稱爲“Management”。它包含了一些數據屬性、計算屬性和方法。其中數據屬性包括一個名爲“users”的空數組、一個名爲“title”的空字符串、一個名爲“content”的空字符串、一個名爲“currentPage”的數字1和一個名爲“allPosts”的空數組。計算屬性包括一個名爲“total”的方法,該方法返回文章列表的總數;一個名爲“start”的方法,該方法計算當前頁的起始索引;一個名爲“end”的方法,該方法計算當前頁的結束索引;一個名爲“posts”的方法,該方法使用數組切片從“allPosts”中提取當前頁的文章列表。組件生命週期鉤子函數中的“created”鉤子調用了“reloadPosts”方法,該方法使用Axios庫從服務器中獲取文章列表並將其格式化。

方法包括“reloadPosts”方法,該方法使用Axios庫從服務器中獲取文章列表,並將其格式化爲所需的格式。在點擊“編輯”或“刪除”按鈕時,將使用Axios庫向服務器發送請求來執行相應的操作。其他方法包括“postInfo”方法,該方法在單擊文章標題時用於打開文章詳細信息頁面;“removePost”方法,該方法在單擊“刪除”按鈕時用於刪除文章;“editPost”方法,該方法在單擊“編輯”按鈕時用於打開文章編輯頁面;以及“editContent”方法,該方法使用Axios庫從服務器中獲取文章內容。

CSS樣式定義了一些用於佈局和樣式化表格、標題、按鈕和分頁組件的樣式規則。其中一些規則使用了Vue.js的“scoped”屬性,以確保它們只應用於該組件的元素。

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