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”属性,以确保它们只应用于该组件的元素。

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