欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

VBlog项目代码理解之前端

程序员文章站 2022-04-19 11:28:27
...

VBlog项目代码理解之前端

资源

项目地址
前后端交互理解
后端代码理解
推荐:整个项目几乎是只用到了SpringBoot、Vue、Mybatis、ElementUI,没有用到Redis、RabbitMQ等内容,很适合刚学完SpringBoot和Vue的同学练手,感谢作者!帮作者打个广告吧~
VBlog项目代码理解之前端
PS:这是本人第一个学习的项目,难免会有错误的地方,哪里有问题烦请指正,感谢!


配置问题

解决前后端交互、跨域、页面跳转等问题

config/index:通过代理解决跨域问题

  • 核心内容就是proxyTable,这块根据SpringBoot的配置来配置,具体内容都在注释里了。
  • config/index.js参数详解
'use strict'
// Template version: 1.2.7
// see http://vuejs-templates.github.io/webpack for documentation.

// 这个不知道有啥用
const path = require('path')

// 话说这边并没有用env: require()来指定环境啊
module.exports = {
  // 管开发的时候,既有前后端交互的跨域配置,也有在前端玩的配置
  dev: {
    // Paths
    // 静态资源子目录和公开地址
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    // 这边是设置代理,解决跨域问题,开发的时候才用
    proxyTable: {
      // 路径啥也不加
      '/': {
        // 改写,相当于把http://localhost:8080改成下面的内容+/
        target: 'http://localhost:8081',
        changeOrigin: true, // 指示是否跨域
        pathRewrite: {
          '^/': ''  // 啥都没有,就等价于http://localhost:8081/
        }
      }
    },

    // 这边是前端自己玩
    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST

    // dev-server的端口号
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-


    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'eval-source-map',


    // debug工具出问题的时候可以试试改为false,可能会有用
    cacheBusting: true,


    // 是否生成css、map文件,说可能存在问题,但是没啥必要用这个,出问题了可以控制台
    // 所以这里于默认不同,设为false
    cssSourceMap: false,

  },
  // 配置build、打包问题
  // 这一套很固定,根本不用动,创建完了就是这样的
  build: {
    // 这块好像都是说运行npm run build之后,生成的文件应在的位置
    // build之后生成的index位置?
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    // 静态资源的根目录
    assetsRoot: path.resolve(__dirname, '../dist'),
    // 静态资源子目录
    assetsSubDirectory: 'static',
    // 静态资源的公开路径,也就是真正的引用路径(引用路径指使用时?)
    assetsPublicPath: '/',

    /**
     * Source Maps
     */
    // 是否生成生产环境的sourcemap,sourcemap用来对编译后的文件进行debug,方法是映射回编译前的文件
    // 编译后的代码人是看不懂的
    productionSourceMap: true,

    // 这应当是一种映射工具
    devtool: '#source-map',


    // 是否在生产环境中压缩代码,如果要压缩必须安装compression-webpack-plugin
    productionGzip: false,
    // 指定要压缩的文件类型
    productionGzipExtensions: ['js', 'css'],


    // 开启编译完成后的报告,只有运行了npm run build --report才有吧
    bundleAnalyzerReport: process.env.npm_config_report
  }
}


router:页面跳转控制

  • 所有的跳转都通过路由控制,在router/index.js文件中配置,path可以通过children设置子路径,如果有多个子模块,就会变成可选模式。
  • 同时每个路径都绑定了component,子路径的component会在父路径的组件的<router-view>位置显示,所以路径的跳转本质上就是组件的不同组合
  • 还可以配置一些信息,如组件名字、是否隐藏、保持**等内容。
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Home from '@/components/Home'
import ArticleList from '@/components/ArticleList'
import CateMana from '@/components/CateMana'
import DataCharts from '@/components/DataCharts'
import PostArticle from '@/components/PostArticle'
import UserMana from '@/components/UserMana'
import BlogDetail from '@/components/BlogDetail'
import Doex from '@/components/Doex'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: '登录',
      hidden: true,
      component: Login
    }, {
      path: '/home',
      name: '',
      component: Home,
      hidden: true
    }, {
      path: '/home',
      component: Home,
      name: '文章管理',
      iconCls: 'fa fa-file-text-o',
      children: [
        {
          path: '/articleList',
          name: '文章列表',
          component: ArticleList,
          meta: {
            keepAlive: true
          }
        }, {
          path: '/postArticle',
          name: '发表文章',
          component: PostArticle,
          meta: {
            keepAlive: false
          }
        }, {
          path: '/blogDetail',
          name: '博客详情',
          component: BlogDetail,
          hidden: true,
          meta: {
            keepAlive: false
          }
        }, {
          path: '/editBlog',
          name: '编辑博客',
          component: PostArticle,
          hidden: true,
          meta: {
            keepAlive: false
          }
        }
      ]
    }, {
      path: '/home',
      component: Home,
      name: '用户管理',
      children: [
        {
          path: '/user',
          iconCls: 'fa fa-user-o',
          name: '用户管理',
          component: UserMana
        }
      ]
    }, {
      path: '/home',
      component: Home,
      name: '栏目管理',
      children: [
        {
          path: '/cateMana',
          iconCls: 'fa fa-reorder',
          name: '栏目管理',
          component: CateMana
        }
      ]
    }, {
      path: '/home',
      component: Home,
      name: '数据统计',
      iconCls: 'fa fa-bar-chart',
      children: [
        {
          path: '/charts',
          iconCls: 'fa fa-bar-chart',
          name: '数据统计',
          component: DataCharts
        }
      ]
    }, {
      path: '/home',
      component: Home,
      name: '身体记录',
      iconCls: 'el-icon-date',
      children: [
        {
          path: '/doex',
          iconCls: 'el-icon-date',
          name: '身体记录',
          component: Doex
        }
      ]
    }
  ]
})


utils/api.js:调用后端方法并接受返回值

关于两种不同的Content-Type:

  • application/x-www-form-urlencoded会对参数进行编码,键值对参数用&连接,空格转换为+,有特殊符号就转换为ASCII HEX值。然后这个类型就是编码格式,也是浏览器默认的编码格式。如果是Get请求,就将参数转化成?key=value&key=value的格式接在url后面。
  • multipart/form-data不会进行编码,使用分割线来相当于&。常用于文件等二进制,也可以用于键值对参数。
  • application/json也经常使用。
  • 两种post接口的解读

四种常用请求含义

  • Get(SELECT):从服务器查询,可以在服务器通过请求的参数区分查询的方式。
  • POST(CREATE):在服务器新建一个资源,调用insert操作。
  • PUT(UPDATE):在服务器更新资源,调用update操作。
  • DELETE(DELETE):从服务器删除资源,调用delete语句。
import axios from 'axios'
// base意义是啥
let base = '';
// 这些要在component用到的时候import
export const postRequest = (url, params) => {
  return axios({
    method: 'post',
    url: `${base}${url}`,
    data: params,
    transformRequest: [function (data) {
      // 这边就是参数传递的标准表达式了
      let ret = ''
      for (let it in data) {
        ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
      }
      return ret
    }],
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  });
}
export const uploadFileRequest = (url, params) => {
  return axios({
    method: 'post',
    url: `${base}${url}`,
    data: params,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
}
export const putRequest = (url, params) => {
  return axios({
    method: 'put',
    url: `${base}${url}`,
    data: params,
    transformRequest: [function (data) {
      let ret = ''
      for (let it in data) {
        ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
      }
      return ret
    }],
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  });
}
export const deleteRequest = (url) => {
  return axios({
    method: 'delete',
    url: `${base}${url}`
  });
}
export const getRequest = (url,params) => {
  return axios({
    method: 'get',
    data:params,
    transformRequest: [function (data) {
      let ret = ''
      for (let it in data) {
        ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
      }
      return ret
    }],
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    url: `${base}${url}`
  });
}


filter_utils:过滤器

  • 在一个单独的文件中定义过滤器,使用方法是{{ 值 | 过滤器函数名}}
    过滤器的定义
import Vue from 'vue'
// 定义全局过滤器,在main.js中导入
Vue.filter("formatDate", function formatDate(value) {
  var date = new Date(value);
  var year = date.getFullYear();
  var month = date.getMonth() + 1;
  var day = date.getDate();
  if (month < 10) {
    month = "0" + month;
  }
  if (day < 10) {
    day = "0" + day;
  }
  return year + "-" + month + "-" + day;
});
Vue.filter("formatDateTime", function formatDateTime(value) {
  var date = new Date(value);
  var year = date.getFullYear();
  var month = date.getMonth() + 1;
  var day = date.getDate();
  var hours = date.getHours();
  var minutes = date.getMinutes();
  if (month < 10) {
    month = "0" + month;
  }
  if (day < 10) {
    day = "0" + day;
  }
  return year + "-" + month + "-" + day + " " + hours + ":" + minutes;
});

main:导入依赖

导入使用的外部组件,如ElementtUIVCharts,还有过滤器也是在这里导入的。

import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// import './styles/element-variables.scss'
import 'font-awesome/css/font-awesome.min.css'
// 再次导入了过滤器
import './utils/filter_utils.js'
import VCharts from 'v-charts'

Vue.use(ElementUI)
Vue.use(VCharts)

Vue.config.productionTip = false;
window.bus = new Vue();
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: {App}
})

组件

  • 最核心的部分,通过各种组件的组合、跳转来实现页面的展示。

Login:登录与权限验证

VBlog项目代码理解之前端

  • 登录页面是Login,两个输入框绑定rules校验规则,绑定方法是el-form:rules,然后通过prop关联rules中的性质。
  • 然后登录按钮绑定了方法,传递地址为后端的spring security的/login/{username, password},执行后判断

v:onclick.nativa.prevent:“方法名”:

  • 给vue组件绑定事件时候,必须加上native ,否则会认为监听的是来自Item组件自定义的事件
  • 但父组件想在子组件上监听自己的click的话,需要加上native修饰符,故写法就像上面这样。
  • prevent 是用来阻止默认的 ,相当于原生的event.preventDefault()

auto-complete=“off”:
autocomplete 属性规定输入字段是否应该启用自动完成功能。自动完成允许浏览器预测对字段的输入。当用户在字段开始键入时,浏览器基于之前键入过的值,应该显示出在字段中填写的选项。
注意:autocomplete 属性适用于 <form>,以及下面的 <input> 类型:text, search, url, telephone, email, password, datepickers, range 以及 color。

完整代码

<template>
  <!--v-bind:rules,是绑定一些数据?答:是绑定校验规则,通过prop的方法使用rules的校验规则-->
  <!--所有的class都可以在下面设置style-->
  <el-form :rules="rules" class="login-container" label-position="left"
           label-width="0px" v-loading="loading">
    <h3 class="login_title">系统登录</h3>
    <el-form-item prop="account">
      <!--输入用户名,关闭了自动提示。-->
      <el-input type="text" v-model="loginForm.username" auto-complete="off" placeholder="账号"></el-input>
    </el-form-item>
    <!--注意这个prop,这是用来设置校验规则的,-->
    <el-form-item prop="checkPass">
      <el-input type="password" v-model="loginForm.password" auto-complete="off" placeholder="密码"></el-input>
    </el-form-item>
    <!--这个左右对齐好像没差别-->
    <el-checkbox class="login_remember" v-model="checked" label-position="left">记住密码</el-checkbox>
    <el-form-item style="width: 100%">
      <!--v:on绑定自定义方法-->
      <el-button type="primary" @click.native.prevent="submitClick" style="width: 100%">登录</el-button>
    </el-form-item>
  </el-form>
</template>
<!--调用utils/api里面的HTTP传递方法-->
<script>
  import {postRequest} from '../utils/api'
  import {putRequest} from '../utils/api'
  export default{
    data(){
      // 下面这些值会传递给上边
      // 这个return是要传给后端码?
      return {
        // rules:规则,校验规则
        rules: {
          // 跟上面的prop绑定的,触发器为失去焦点
          account: [{required: true, message: '请输入用户名', trigger: 'blur'}],
          checkPass: [{required: true, message: '请输入密码', trigger: 'blur'}]
        },
        // 默认被选择
        checked: true,
        // 默认文本内容,因为由v-model,所以会显示
        loginForm: {
          username: 'sang',
          password: '123'
        },
        loading: false
      }
    },
    methods: {
      submitClick: function () {
        var _this = this;
        // 改变参数
        this.loading = true;
        // 方法里用的axios,第一个参数是url,第二个是参数
        // 方法通往Spring security的权限管理
        postRequest('/login', {
          username: this.loginForm.username,
          password: this.loginForm.password
        }).then(resp=> {
          _this.loading = false;
          if (resp.status == 200) {
            //成功
            var json = resp.data;
            if (json.status == 'success') {
              _this.$router.replace({path: '/home'});
            } else {
              // 第一个参数是内容,第二个参数是标题
              _this.$alert('登录失败!', '失败!');
            }
          } else {
            //失败
            _this.$alert('登录失败!', '失败!');
          }
        }, resp=> {
          _this.loading = false;
          _this.$alert('找不到服务器⊙﹏⊙∥!', '失败!');
        });
      }
    }
  }
</script>
<!--给绑定的class设置style-->
<style>
  .login-container {
    border-radius: 15px;
    background-clip: padding-box;
    margin: 180px auto;
    width: 350px;
    padding: 35px 35px 15px 35px;
    background: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
  }

  .login_title {
    margin: 0px auto 40px auto;
    text-align: center;
    color: #505458;
  }

  .login_remember {
    margin: 0px 0px 35px 0px;
    text-align: left;
  }
</style>


Home:主页面框架

VBlog项目代码理解之前端
登录成功就来到Home。

v-if="!item.hidden"
通过hidden来控制是否显示

slot=“title”:是系统自带的吗,找不到东西

Breadcrumb 面包屑
el-breadcrumb作用显示当前页面的路径,快速返回之前的任意页面,之前的页面用el-breadcrumb-item表示
可以控制最上面的导航,加了点东西就变成下一行了
VBlog项目代码理解之前端

VBlog项目代码理解之前端

**判断
代码中可以根据keep-alive参数来决定子组件显示的位置

          <!--根据**状态判断在哪里显示-->
          <keep-alive>
            <div>
              {{'**'}}
            </div>
            <router-view v-if="this.$route.meta.keepAlive"></router-view>
          </keep-alive>
          <div>
            {{'未**'}}
          </div>
          <router-view v-if="!this.$route.meta.keepAlive"></router-view>

通过:index来绑定路径path

              <!--但是这个怎么跳转呢?-->
              <!--index这里绑定了path, 为啥index绑定了就能跳?是因为el-menu-item的特性?-->
              <el-menu-item :index="item.children[0].path">
                <i :class="item.children[0].iconCls"></i>
                <span slot="title">{{item.children[0].name}}</span>
              </el-menu-item>

登录声明
通过一个钩子函数触发,当转到该页面的时候,会调用

    // 钩子函数,转到的时候触发
    mounted: function () {
      this.$alert('为了确保所有的小伙伴都能看到完整的数据演示,数据库只开放了查询权限和部分字段的更新权限,其他权限都不具备,完整权限的演示需要大家在自己本地部署后,换一个正常的数据库用户后即可查看,这点请大家悉知!', '友情提示', {
        confirmButtonText: '确定',
        callback: action => {
        }
      });
      var _this = this;
      getRequest("/currentUserName").then(function (msg) {
        _this.currentUserName = msg.data;
      }, function (msg) {
        _this.currentUserName = '游客';
      });
    },

下拉框
VBlog项目代码理解之前端
通过el-dropdown来实现下拉功能,其中分割符号在el-dropdown-item中添加divided实现。

// template部分
    <!--下拉框,里面的都是固定操作-->
    <el-dropdown @command="handleCommand">
    <span class="el-dropdown-link home_userinfo">
      <!--left,right还是没区别-->
      <!--这里有的参数-->
      {{currentUserName}}<i class="el-icon-arrow-down el-icon--right home_userinfo"></i>
    </span>
        <el-dropdown-menu slot="dropdown">
          <!--command可以在后面做判断的时候使用,算是一个注释-->
          <!--除了退出登录,其他功能都没有实现-->
          <el-dropdown-item command="sysMsg">系统消息</el-dropdown-item>
          <el-dropdown-item command="MyArticle">我的文章</el-dropdown-item>
          <el-dropdown-item command="MyHome">个人主页</el-dropdown-item>
          <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>

// script部分
      handleCommand(command){
        var _this = this;
        if (command == 'logout') {
          this.$confirm('注销登录吗?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(function () {
            getRequest("/logout")
            _this.currentUserName = '游客';
            _this.$router.replace({path: '/'});
          }, function () {
            //取消
          })
        }
      }
    },

this.$router.replace
跳转到指定URL,替换history栈中最后一个记录,点击后退会返回至上上一个页面
参考

左侧菜单中最后一个router参数的作用
是否使用vue-router的模式,启用该模式会在**导航时以index作为path进行路由跳转。

        <el-menu
          default-active="0"
          class="el-menu-vertical-demo" style="background-color: #ECECEC" router>

完整代码

<template>
  <el-container class="home_container">
    <!--头不-->
    <el-header>
      <div class="home_title">V部落博客管理平台</div>
      <div class="home_userinfoContainer">
        <!--下拉框,里面的都是固定操作-->
        <el-dropdown @command="handleCommand">
        <span class="el-dropdown-link home_userinfo">
          <!--left,right还是没区别-->
          <!--这里有的参数-->
          {{currentUserName}}<i class="el-icon-arrow-down el-icon--right home_userinfo"></i>
        </span>
            <el-dropdown-menu slot="dropdown">
              <!--command可以在后面做判断的时候使用,算是一个注释-->
              <!--除了退出登录,其他功能都没有实现-->
              <el-dropdown-item command="sysMsg">系统消息</el-dropdown-item>
              <el-dropdown-item command="MyArticle">我的文章</el-dropdown-item>
              <el-dropdown-item command="MyHome">个人主页</el-dropdown-item>
              <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
      </div>
    </el-header>
    <!--主要内容-->
    <el-container>
      <el-aside width="200px">
        <!--左侧菜单-->
        <el-menu
          default-active="0"
          class="el-menu-vertical-demo" style="background-color: #ECECEC" router>
          <!--上面最后的router是干嘛的?答:控制使用index来跳转路由path的-->
          <!--对routes里面的内容遍历,但是不显示隐藏的-->
          <!--this.$router.options.routes是如何绑定的?-->
          <template v-for="(item,index) in this.$router.options.routes" v-if="!item.hidden">
            <!--如果孩子大于1,就显示子菜单-->
            <!--对应这个if-->
            <el-submenu :index="index+''" v-if="item.children.length>1" :key="index">
              <template slot="title">
                <!--根据自带的属性设置格式-->
                <i :class="item.iconCls"></i>
                <span>{{item.name}}</span>
              </template>
              <!--给子标签也显示出来-->
              <!--那为啥不是对应这个if?因为这个是在组件的里面?-->
              <el-menu-item v-for="child in item.children" v-if="!child.hidden" :index="child.path" :key="child.path">
                {{child.name}}
              </el-menu-item>
            </el-submenu>

            <!--这个else-->
            <template v-else>
              <!--但是这个怎么跳转呢?-->
              <!--index这里绑定了path, 为啥index绑定了就能跳?是因为el-menu-item的特性?-->
              <el-menu-item :index="item.children[0].path">
                <i :class="item.children[0].iconCls"></i>
                <span slot="title">{{item.children[0].name}}</span>
              </el-menu-item>
            </template>
          </template>
        </el-menu>
      </el-aside>
      <!--内容页面-->
      <el-container>
        <el-main>
          <!--面包屑,就最上面那层-->
          <!--这个class是管图标分隔符的-->
          <el-breadcrumb separator-class="el-icon-arrow-right">
            <!--path指的是路由的路径,这个跟面包屑没啥关系吧,就是基础语法-->
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <!--如果text中的内容存在,就也会显示,否则不显示;这个也没有点击功能-->
            <el-breadcrumb-item v-text="this.$router.currentRoute.name"></el-breadcrumb-item>

          </el-breadcrumb>
          <!--根据**状态判断在哪里显示-->
          <keep-alive>
            <div>
              {{'**'}}
            </div>
            <router-view v-if="this.$route.meta.keepAlive"></router-view>
          </keep-alive>
          <div>
            {{'未**'}}
          </div>
          <router-view v-if="!this.$route.meta.keepAlive"></router-view>
        </el-main>
      </el-container>
    </el-container>
  </el-container>
</template>
<script>
  import {getRequest} from '../utils/api'
  export default{
    methods: {
      handleCommand(command){
        var _this = this;
        if (command == 'logout') {
          this.$confirm('注销登录吗?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(function () {
            getRequest("/logout")
            _this.currentUserName = '游客';
            _this.$router.replace({path: '/'});
          }, function () {
            //取消
          })
        }
      }
    },
    // 钩子函数,转到的时候触发
    mounted: function () {
      this.$alert('为了确保所有的小伙伴都能看到完整的数据演示,数据库只开放了查询权限和部分字段的更新权限,其他权限都不具备,完整权限的演示需要大家在自己本地部署后,换一个正常的数据库用户后即可查看,这点请大家悉知!', '友情提示', {
        confirmButtonText: '确定',
        callback: action => {
        }
      });
      var _this = this;
      getRequest("/currentUserName").then(function (msg) {
        _this.currentUserName = msg.data;
      }, function (msg) {
        _this.currentUserName = '游客';
      });
    },
    // 最后就返回了给名字
    data(){
      return {
        currentUserName: ''
      }
    }
  }
</script>
<style>
  .home_container {
    height: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
  }

  .el-header {
    background-color: #20a0ff;
    color: #333;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .el-aside {
    background-color: #ECECEC;
  }

  .el-main {
    background-color: #fff;
    color: #000;
    text-align: center;
  }

  .home_title {
    color: #fff;
    font-size: 22px;
    display: inline;
  }

  .home_userinfo {
    color: #fff;
    cursor: pointer;
  }

  .home_userinfoContainer {
    display: inline;
    margin-right: 20px;
  }
</style>


ArticleList:显示文章列表

el-main
el-main感觉就是好看的,来将内容收到一起

建多Tage
VBlog项目代码理解之前端
建多个按钮用el-tabs,可以在“全部文章”、“已发表”、“草稿箱”等tag中切换。el-tabs可以给下面不同的el-tab-pane设置不同的lable(作用是显示的名字不同),然后调用组件blog_table,传递参数过去,用props接收,这个组件的作用就是根据参数来控制该显示哪些内容,本质上所有的tag的内容都在一个组件中,但是不会全部都显示,根据每个tag设置的参数来控制显示内容,就达到了不同的效果

关于el-tabs的点击响应,这里应是一种自动配置的方法,框架已经帮做好了:

// template中
  <el-tabs v-model="activeName" @tab-click="handleClick" type="card">
// script中
    methods: {
      // 这个啥也不做吗
      // 可能是自动做了
      handleClick(tab, event) {
       // console.log(tab, event);
      }

完整代码:

<template>
  <el-container class="article_list">
    <!--感觉main的作用就是分的更开了,格式好看了-->
    <el-main class="main">
      <!--点击了总的有个响应的地方吧,响应呢?-->
      <!--答:使用了组件,blog_table和blog_cfg-->
      <el-tabs v-model="activeName" @tab-click="handleClick" type="card">
        <!--根据绑定的方法,点击后自动把label传给activateName-->
        <el-tab-pane label="全部文章" name="all">
          <!--使用组件的同时传递参数-->
          <blog_table state="-1" :showEdit="false" :showDelete="false" :showRestore="false" :activeName="activeName"></blog_table>
        </el-tab-pane>
        <el-tab-pane label="已发表" name="post">
          <blog_table state="1" :showEdit="true" :showDelete="true" :showRestore="false" :activeName="activeName"></blog_table>
        </el-tab-pane>
        <el-tab-pane label="草稿箱" name="draft">
          <blog_table state="0" :showEdit="true" :showDelete="true" :showRestore="false" :activeName="activeName"></blog_table>
        </el-tab-pane>
        <el-tab-pane label="回收站" name="dustbin">
          <blog_table state="2" :showEdit="false" :showDelete="true" :showRestore="true" :activeName="activeName"></blog_table>
        </el-tab-pane>
        <el-tab-pane label="博客管理" name="blogmana" v-if="isAdmin">
          <blog_table state="-2" :showEdit="false" :showDelete="true" :showRestore="false" :activeName="activeName"></blog_table>
        </el-tab-pane>
        <el-tab-pane label="博客配置" name="blogcfg">
          <blog_cfg></blog_cfg>
        </el-tab-pane>
      </el-tabs>
    </el-main>
  </el-container>
</template>
<script>
  import BlogTable from '@/components/BlogTable'
  import BlogCfg from '@/components/BlogCfg'
  import {postRequest} from '../utils/api'
  import {putRequest} from '../utils/api'
  import {deleteRequest} from '../utils/api'
  import {getRequest} from '../utils/api'
  export default {
    mounted: function () {
      var _this = this;
      getRequest("/isAdmin").then(resp=> {
        if (resp.status == 200) {
          _this.isAdmin = resp.data;
        }
      })
    },
    data() {
      return {
        // 初始化标签
        activeName: 'post',
        isAdmin: false
      };
    },
    methods: {
      // 这个啥也不做吗
      // 可能是自动做了
      handleClick(tab, event) {
       // console.log(tab, event);
      }
    },
    components: {
      'blog_table': BlogTable,
      'blog_cfg': BlogCfg
    }
  };
</script>
<style>
  .article_list > .header {
    background-color: #ececec;
    margin-top: 10px;
    padding-left: 5px;
    display: flex;
    justify-content: flex-start;
  }

  .article_list > .main {
    /*justify-content: flex-start;*/
    display: flex;
    flex-direction: column;
    padding-left: 0px;
    background-color: #fff;
    padding-top: 0px;
    margin-top: 8px;
  }
</style>


BlogTable:具体文章列表

VBlog项目代码理解之前端

模糊搜索
比较常规的操作

// template部分
    <div style="display: flex;justify-content: flex-start">
      <!--搜索输入框,还设置了图标,这里应该是要用到数据库了吧-->
      <!--确实用到了,主要是keywords生效-->
      <el-input
        placeholder="通过标题搜索该分类下的博客..."
        prefix-icon="el-icon-search"
        v-model="keywords" style="width: 400px" size="mini">
      </el-input>
      <!--设置图标挺秀的-->
      <el-button type="primary" icon="el-icon-search" size="mini" style="margin-left: 3px" @click="searchClick">搜索
      </el-button>
    </div>


// script部分
      searchClick(){
        this.loadBlogs(1, this.pageSize);
      }


      loadBlogs(page, count){
        var _this = this;
        var url = '';
        if (this.state == -2) {
          url = "/admin/article/all" + "?page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        } else {
          url = "/article/all?state=" + this.state + "&page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        }
        getRequest(url).then(resp=> {
          _this.loading = false;
          if (resp.status == 200) {
            _this.articles = resp.data.articles;
            _this.totalCount = resp.data.totalCount;
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }, resp=> {
          _this.loading = false;
          if (resp.response.status == 403) {
            _this.$message({type: 'error', message: resp.response.data});
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }).catch(resp=> {
          //try catch?
          //压根没见到服务器
          _this.loading = false;
          _this.$message({type: 'error', message: '数据加载失败!'});
        })
      }

建表

    <el-table
      ref="multipleTable"
      :data="articles"
      tooltip-effect="dark"
      style="width: 100%;overflow-x: hidden; overflow-y: hidden;"
      max-height="390"
      @selection-change="handleSelectionChange" v-loading="loading">

多table的操作是建一个<el-table>, 然后指定ref="multipleTable",在夹层中el-table中添加一些el-table-column,这些就是表的列,即每个元素的通用属性。要注意有一个el-table-column是用来作为选择框的,不需要有什么内容但要设置type="selection",如下图所示:

      <el-table-column
        type="selection"
        width="35" align="left" v-if="showEdit || showDelete">
      </el-table-column>

VBlog项目代码理解之前端
然后其他的列可以用lable来设置列名,还可以绑定点击触发事件。

      <el-table-column
        label="标题"
        width="400" align="left">
        <!--这里scope.row,应该就是数据库数据了,来自:data="article"-->
        <template slot-scope="scope"><span style="color: #409eff;cursor: pointer" @click="itemClick(scope.row)">{{ scope.row.title}}</span>
        </template>
      </el-table-column>

多选框
在建表的el-table中添加@selection-change="handleSelectionChange"属性,这个可以获取到选择的id的变化,并生成一个数组送到方法handleSelectionChange

// template部分
    <el-table
      ref="multipleTable"
      :data="articles"
      tooltip-effect="dark"
      style="width: 100%;overflow-x: hidden; overflow-y: hidden;"
      max-height="390"
      @selection-change="handleSelectionChange" v-loading="loading">
// script部分
      handleSelectionChange(val) {
        // 可获取多选的内容
        this.selItems = val;
        // console.log(val);
      },

时间与过滤器
编辑时间什么的都用过滤器过一下,vue的过滤格式就是{{ 值 | 过滤器函数名}}

      <el-table-column
        label="最近编辑时间" width="140" align="left">
        <template slot-scope="scope">{{ scope.row.editTime | formatDateTime}}</template>
      </el-table-column>

点击查看详细内容
VBlog项目代码理解之前端

会跳到blogDetail页面

// template部分
      <el-table-column
        label="标题"
        width="400" align="left">
        <!--这里scope.row,应该就是数据库数据了,来自:data="article"-->
        <template slot-scope="scope"><span style="color: #409eff;cursor: pointer" @click="itemClick(scope.row)">{{ scope.row.title}}</span>
        </template>
      </el-table-column>

// script部分
      itemClick(row){
        // row.id为什么是数据库里对应的id?
        // 答:因为row这东西就是数据库来的
        this.$router.push({path: '/blogDetail', query: {aid: row.id}})
      },

这个slot-scope="scope"我感觉也是自带的,然后点击触发事件。其中scope.row就是表单这一行的属性,在这里就是一条数据库的完整对象(应是来自:data="articles"),所以可以使用数据库有的属性,如idtitle
scope.$index→拿到每一行的indexscope.$row→拿到每一行的数据。

批量删除
之前把选中的id都存放到了selItems对象中,可以根据能不能显示、选没选中来控制按钮的展示。
这里是把id都放入了dustbinData数组中,删除的时候就把这个数组传到MySQL中做foreach删除,删除完了再把dustbinData数组清空。

// template部分
      <!--批量删除,文章数不为0可显示,选中的元素为0时不可选,点击后调用deleteMany方法-->
      <el-button type="danger" size="mini" style="margin: 0px;" v-show="this.articles.length>0 && showDelete"
                 :disabled="this.selItems.length==0" @click="deleteMany">批量删除
      </el-button>

// script部分
      deleteMany(){
        var selItems = this.selItems;
        for (var i = 0; i < selItems.length; i++) {
          this.dustbinData.push(selItems[i].id)
        }
        // 随便取一个state指定就行,因为是在一个页面选择的,所以state都相同
        this.deleteToDustBin(selItems[0].state)
      },

分页功能
VBlog项目代码理解之前端
翻完了要重新加载一次

// tmeplate部分
      <!--分页功能-->
      <!--layout中的内容用逗号隔开,内容会依次显示-->
      <!--background可以显示背景颜色-->
      <!--v-show指定展示条件-->
      <!--current-change:页面改动时会触发,这里是重新加载了数据-->
      <el-pagination
        background
        :page-size="pageSize"
        layout="prev, pager, next"
        :total="totalCount" @current-change="currentChange" v-show="this.articles.length>0">
      </el-pagination>

// script部分
      //翻页
      currentChange(currentPage){
        this.currentPage = currentPage;
        this.loading = true;
        this.loadBlogs(currentPage, this.pageSize);
      },

      loadBlogs(page, count){
        var _this = this;
        var url = '';
        if (this.state == -2) {
          url = "/admin/article/all" + "?page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        } else {
          url = "/article/all?state=" + this.state + "&page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        }
        getRequest(url).then(resp=> {
          _this.loading = false;
          if (resp.status == 200) {
            _this.articles = resp.data.articles;
            _this.totalCount = resp.data.totalCount;
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }, resp=> {
          _this.loading = false;
          if (resp.response.status == 403) {
            _this.$message({type: 'error', message: resp.response.data});
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }).catch(resp=> {
          //try catch?
          //压根没见到服务器
          _this.loading = false;
          _this.$message({type: 'error', message: '数据加载失败!'});
        })
      },

编辑
按钮与方法,参数还传入了scope.$index,主要还是跳转同时传递参数,参数是通过钩子函数获取的(this.$route.query.变量)。

// template部分
          <el-button
            size="mini"
            @click="handleEdit(scope.$index, scope.row)" v-if="showEdit">编辑
          </el-button>

// scripte部分
      handleEdit(index, row) {
        this.$router.push({path: '/editBlog', query: {from: this.activeName,id:row.id}});
      },

删除
按钮与方法,删除的时候会使用再次确定功能this.$confirm根据state判断是送到回收站还是彻底删除,会走不同的url地址,删除完了dustbinData会清零。

// template部分
          <el-button
            size="mini"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)" v-if="showDelete">删除
          </el-button>

// script部分
      deleteToDustBin(state){
        var _this = this;
        // state=2是管理页面,删了后进入回收站;否则的话就是回收站了,回收站的删除是永久删除
        this.$confirm(state != 2 ? '将该文件放入回收站,是否继续?' : '永久删除该文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          _this.loading = true;
          var url = '';
          if (_this.state == -2) {
            url = "/admin/article/dustbin";
          } else {
            url = "/article/dustbin";
          }
          // dustbinData是这次要删除的,删完了就清零
          putRequest(url, {aids: _this.dustbinData, state: state}).then(resp=> {
            if (resp.status == 200) {
              var data = resp.data;
              _this.$message({type: data.status, message: data.msg});
              if (data.status == 'success') {
                window.bus.$emit('blogTableReload')//通过选项卡都重新加载数据
              }
            } else {
              _this.$message({type: 'error', message: '删除失败!'});
            }
            _this.loading = false;
            _this.dustbinData = []
          }, resp=> {
            _this.loading = false;
            _this.$message({type: 'error', message: '删除失败!'});
            _this.dustbinData = []
          });
        }).catch(() => {
          _this.$message({
            type: 'info',
            message: '已取消删除'
          });
          _this.dustbinData = []
        });
      }

还原
从回收站还原,然后也有一个再次确定的验证,和删除差不多,都是state的改变,完了后重新加载一下。

// template部分
          <el-button
            size="mini"
            @click="handleRestore(scope.$index, scope.row)" v-if="showRestore">还原
          </el-button>

// script部分
      handleRestore(index, row) {
        let _this = this;
        this.$confirm('将该文件还原到原处,是否继续?','提示',{
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        } ).then(() => {
          _this.loading = true;
          // id参数传给后端
          putRequest('/article/restore', {articleId: row.id}).then(resp=> {
            if (resp.status == 200) {
              // 获取后端信息,实际上是获取了后端返回的RespBean类
              var data = resp.data;
              // 出现信息框,表示状态
              _this.$message({type: data.status, message: data.msg});
              if (data.status == 'success') {
                // 出发事件,方法在钩子中定义了
                window.bus.$emit('blogTableReload')//通过选项卡都重新加载数据
              }
            } else {
              _this.$message({type: 'error', message: '还原失败!'});
            }
            _this.loading = false;
          });
        }).catch(() => {
          _this.$message({
            type: 'info',
            message: '已取消还原'
          });
        });
      },

完整代码

<style type="text/css">
  .blog_table_footer {
    display: flex;
    box-sizing: content-box;
    padding-top: 10px;
    padding-bottom: 0px;
    margin-bottom: 0px;
    justify-content: space-between;
  }
</style>
<template>
  <div>
    <div style="display: flex;justify-content: flex-start">
      <!--搜索输入框,还设置了图标,这里应该是要用到数据库了吧-->
      <!--确实用到了,主要是keywords生效-->
      <el-input
        placeholder="通过标题搜索该分类下的博客..."
        prefix-icon="el-icon-search"
        v-model="keywords" style="width: 400px" size="mini">
      </el-input>
      <!--设置图标挺秀的-->
      <el-button type="primary" icon="el-icon-search" size="mini" style="margin-left: 3px" @click="searchClick">搜索
      </el-button>
    </div>
    <!--<div style="width: 100%;height: 1px;background-color: #20a0ff;margin-top: 8px;margin-bottom: 0px"></div>-->
    <!--这个表还蛮复杂的-->
    <el-table
      ref="multipleTable"
      :data="articles"
      tooltip-effect="dark"
      style="width: 100%;overflow-x: hidden; overflow-y: hidden;"
      max-height="390"
      @selection-change="handleSelectionChange" v-loading="loading">
      <el-table-column
        type="selection"
        width="35" align="left" v-if="showEdit || showDelete">
      </el-table-column>
      <el-table-column
        label="标题"
        width="400" align="left">
        <!--这里scope.row,应该就是数据库数据了,来自:data="article"-->
        <template slot-scope="scope"><span style="color: #409eff;cursor: pointer" @click="itemClick(scope.row)">{{ scope.row.title}}</span>
        </template>
      </el-table-column>
      <el-table-column
        label="最近编辑时间" width="140" align="left">
        <template slot-scope="scope">{{ scope.row.editTime | formatDateTime}}</template>
      </el-table-column>
      <el-table-column
        prop="nickname"
        label="作者"
        width="120" align="left">
      </el-table-column>
      <el-table-column
        prop="cateName"
        label="所属分类"
        width="120" align="left">
      </el-table-column>
      <el-table-column label="操作" align="left" v-if="showEdit || showDelete">
        <template slot-scope="scope">
          <!--调用了下面的方法, 参数是用来指定自身的吧-->
          <el-button
            size="mini"
            @click="handleEdit(scope.$index, scope.row)" v-if="showEdit">编辑
          </el-button>
          <el-button
            size="mini"
            @click="handleRestore(scope.$index, scope.row)" v-if="showRestore">还原
          </el-button>
          <el-button
            size="mini"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)" v-if="showDelete">删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!--这个class指定了左对齐-->
    <div class="blog_table_footer">
      <!--批量删除,文章数不为0可显示,选中的元素为0时不可选,点击后调用deleteMany方法-->
      <el-button type="danger" size="mini" style="margin: 0px;" v-show="this.articles.length>0 && showDelete"
                 :disabled="this.selItems.length==0" @click="deleteMany">批量删除
      </el-button>
      <span>{{'底部,啥也没写'}}</span>
      <!--分页功能-->
      <!--layout中的内容用逗号隔开,内容会依次显示-->
      <!--background可以显示背景颜色-->
      <!--v-show指定展示条件-->
      <!--current-change:页面改动时会触发,这里是重新加载了数据-->
      <el-pagination
        background
        :page-size="pageSize"
        layout="prev, pager, next"
        :total="totalCount" @current-change="currentChange" v-show="this.articles.length>0">
      </el-pagination>
    </div>
  </div>
</template>

<script>
  import {putRequest} from '../utils/api'
  import {getRequest} from '../utils/api'
//  import Vue from 'vue'
//  var bus = new Vue()

  export default{
    data() {
      return {
        articles: [],
        selItems: [],
        loading: false,
        currentPage: 1,
        totalCount: -1,
        pageSize: 6,
        keywords: '',
        dustbinData: []
      }
    },
    mounted: function () {
      var _this = this;
      this.loading = true;
      this.loadBlogs(1, this.pageSize);
      var _this = this;
      window.bus.$on('blogTableReload', function () {
        _this.loading = true;
        _this.loadBlogs(_this.currentPage, _this.pageSize);
      })
    },
    methods: {
      searchClick(){
        this.loadBlogs(1, this.pageSize);
      },
      itemClick(row){
        // row.id为什么是数据库里对应的id?
        // 答:因为row这东西就是数据库来的
        this.$router.push({path: '/blogDetail', query: {aid: row.id}})
      },
      deleteMany(){
        var selItems = this.selItems;
        for (var i = 0; i < selItems.length; i++) {
          this.dustbinData.push(selItems[i].id)
        }
        // 随便取一个state指定就行,因为是在一个页面选择的,所以state都相同
        this.deleteToDustBin(selItems[0].state)
      },
      //翻页
      currentChange(currentPage){
        this.currentPage = currentPage;
        this.loading = true;
        this.loadBlogs(currentPage, this.pageSize);
      },
      loadBlogs(page, count){
        var _this = this;
        var url = '';
        if (this.state == -2) {
          url = "/admin/article/all" + "?page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        } else {
          url = "/article/all?state=" + this.state + "&page=" + page + "&count=" + count + "&keywords=" + this.keywords;
        }
        getRequest(url).then(resp=> {
          _this.loading = false;
          if (resp.status == 200) {
            _this.articles = resp.data.articles;
            _this.totalCount = resp.data.totalCount;
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }, resp=> {
          _this.loading = false;
          if (resp.response.status == 403) {
            _this.$message({type: 'error', message: resp.response.data});
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }).catch(resp=> {
          //try catch?
          //压根没见到服务器
          _this.loading = false;
          _this.$message({type: 'error', message: '数据加载失败!'});
        })
      },
      handleSelectionChange(val) {
        // 可获取多选的内容
        this.selItems = val;
        // console.log(val);
      },
      handleEdit(index, row) {
        this.$router.push({path: '/editBlog', query: {from: this.activeName,id:row.id}});
      },
      handleDelete(index, row) {
        // 回收站+id
        this.dustbinData.push(row.id);
        this.deleteToDustBin(row.state);
      },
      handleRestore(index, row) {
        let _this = this;
        this.$confirm('将该文件还原到原处,是否继续?','提示',{
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        } ).then(() => {
          _this.loading = true;
          // id参数传给后端
          putRequest('/article/restore', {articleId: row.id}).then(resp=> {
            if (resp.status == 200) {
              // 获取后端信息,实际上是获取了后端返回的RespBean类
              var data = resp.data;
              // 出现信息框,表示状态
              _this.$message({type: data.status, message: data.msg});
              if (data.status == 'success') {
                // 出发事件,方法在钩子中定义了
                window.bus.$emit('blogTableReload')//通过选项卡都重新加载数据
              }
            } else {
              _this.$message({type: 'error', message: '还原失败!'});
            }
            _this.loading = false;
          });
        }).catch(() => {
          _this.$message({
            type: 'info',
            message: '已取消还原'
          });
        });
      },
      deleteToDustBin(state){
        var _this = this;
        // state=2是管理页面,删了后进入回收站;否则的话就是回收站了,回收站的删除是永久删除
        this.$confirm(state != 2 ? '将该文件放入回收站,是否继续?' : '永久删除该文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          _this.loading = true;
          var url = '';
          if (_this.state == -2) {
            url = "/admin/article/dustbin";
          } else {
            url = "/article/dustbin";
          }
          // dustbinData是这次要删除的,删完了就清零
          putRequest(url, {aids: _this.dustbinData, state: state}).then(resp=> {
            if (resp.status == 200) {
              var data = resp.data;
              _this.$message({type: data.status, message: data.msg});
              if (data.status == 'success') {
                window.bus.$emit('blogTableReload')//通过选项卡都重新加载数据
              }
            } else {
              _this.$message({type: 'error', message: '删除失败!'});
            }
            _this.loading = false;
            _this.dustbinData = []
          }, resp=> {
            _this.loading = false;
            _this.$message({type: 'error', message: '删除失败!'});
            _this.dustbinData = []
          });
        }).catch(() => {
          _this.$message({
            type: 'info',
            message: '已取消删除'
          });
          _this.dustbinData = []
        });
      }
    },
    props: ['state', 'showEdit', 'showDelete', 'activeName', 'showRestore']
  }
</script>


BlogDetail:文章显示

VBlog项目代码理解之前端

返回
向后跳转一个页面,通过this.$router.go(参数)方法实现的,参数为正则向前,为负则向后,数值决定跳几步

// template部分
    <!--向后跳转一个页面-->
    <el-button type="text" icon="el-icon-back" @click="goBack" style="padding-bottom: 0px;">返回</el-button>

// script部分
      goBack(){
        // 正为向后跳转,负为向后跳转
        this.$router.go(-1);
      }

浏览量
这个功能是在后端完成的,当点击该文章的时候,钩子函数会把该文章的id传送给后端,显示文章的同时浏览量+1。同时也注意这里的钩子函数是如何获取前面的query参数的。

// template部分
      <span style="color: #20a0ff;margin-right:20px;font-size: 12px;">浏览 {{article.pageView==null?0:article.pageView}}</span>

// script部分
    mounted: function () {
      var aid = this.$route.query.aid;
      // 这个an变量没见到呀?
      this.activeName = this.$route.query.an;
      console.log(aid);
      // 确实不存在
      console.log(this.activeName);
      var _this = this;
      this.loading = true;
      getRequest("/article/" + aid).then(resp=> {
        if (resp.status == 200) {
          _this.article = resp.data;
        }
        _this.loading = false;
      }, resp=> {
        _this.loading = false;
        _this.$message({type: 'error', message: '页面加载失败!'});
      });
    },

Tag
使用el-tag来完成,对所有的tags做遍历展示

      <!--添加了tag,还挺好看-->
      <el-tag type="success" v-for="(item,index) in article.tags" :key="index" size="small"
              style="margin-left: 8px">{{item.tagName}}
      </el-tag>

内容展示
直接调用数据库显示即可,注意html格式的内容用v-html获取

    <el-col>
      <!--内容, 应也是来自数据库属性, 名字就是htmlContent-->
      <div style="text-align: left" v-html="article.htmlContent">
      </div>
    </el-col>

完整代码

<template>
  <!--整个一大行?-->
  <el-row v-loading="loading">
    <el-col :span="24">
      <div style="text-align: left;">
        <!--向后跳转一个页面-->
        <el-button type="text" icon="el-icon-back" @click="goBack" style="padding-bottom: 0px;">返回</el-button>
      </div>
    </el-col>
    <el-col :span="24">
      <div>
        <div><h3 style="margin-top: 0px;margin-bottom: 0px">{{article.title}}</h3></div>
        <div style="width: 100%;margin-top: 5px;display: flex;justify-content: flex-end;align-items: center">
          <div style="display: inline; color: #20a0ff;margin-left: 50px;margin-right:20px;font-size: 12px;">
            {{article.nickname}}
          </div>
          <!--或许pageView是在后端++-->
          <!--:没错,就是在后端加的,直接在数据库里改的-->
          <span style="color: #20a0ff;margin-right:20px;font-size: 12px;">浏览 {{article.pageView==null?0:article.pageView}}</span>
          <span style="color: #20a0ff;margin-right:20px;font-size: 12px;"> {{article.editTime | formatDateTime}}</span>
          <!--添加了tag,还挺好看-->
          <el-tag type="success" v-for="(item,index) in article.tags" :key="index" size="small"
                  style="margin-left: 8px">{{item.tagName}}
          </el-tag>
          <span style="margin:0px 50px 0px 0px"></span>
        </div>
      </div>
    </el-col>
    <el-col>
      <!--内容, 应也是来自数据库属性, 名字就是htmlContent-->
      <div style="text-align: left" v-html="article.htmlContent">
      </div>
    </el-col>
  </el-row>
</template>
<script>
  import {getRequest} from '../utils/api'
  export default{
    methods: {
      goBack(){
        // 正为向后跳转,负为向后跳转
        this.$router.go(-1);
      }
    },
    mounted: function () {
      var aid = this.$route.query.aid;
      // 这个an变量没见到呀?
      this.activeName = this.$route.query.an;
      console.log(aid);
      // 确实不存在
      console.log(this.activeName);
      var _this = this;
      this.loading = true;
      getRequest("/article/" + aid).then(resp=> {
        if (resp.status == 200) {
          _this.article = resp.data;
        }
        _this.loading = false;
      }, resp=> {
        _this.loading = false;
        _this.$message({type: 'error', message: '页面加载失败!'});
      });
    },
    data(){
      return {
        article: {},
        loading: false,
        activeName: ''
      }
    }
  }
</script>


PostArticle:编辑与发表

VBlog项目代码理解之前端

el-header:
使用el-header将栏目选择、标题、tag三个包裹到一起,不然的话就会变得松散,而且和el-main混到了一行,如下图:
VBlog项目代码理解之前端

选择栏目
使用el-select完成,在el-option中做遍历,最终影响的是article.cid的值。

      <!--从数据库获取栏目对应id信息,显示还得看label-->
      <el-select v-model="article.cid" placeholder="请选择文章栏目" style="width: 150px;">
        <el-option
          v-for="item in categories"
          :key="item.id"
          :label="item.cateName"
          :value="item.id">
        </el-option>
      </el-select>

标题输入
基本操作。

      <el-input v-model="article.title" placeholder="请输入标题..." style="width: 400px;margin-left: 10px"></el-input>

显示tag

  • 虽然这块显示只有标签本身和添加标签按钮,但是实现由三部分组成,依次是显示、输入、按钮,其中输入与按钮是不能同时存在的,因为点击了按钮就会变成输入,输入完了就会变成按钮,所以此处用来v-ifv-else实现。
  • el-tag的显示是个简单的v-for方法,同时具有closable属性,就是点击叉号可删除该标签,还可以用:disable-transitions来设置渐变动画
// template部分
      <!--已经添加的先在前面显示-->
      <!--closable是可移除标签-->
      <!--:disable-transitions="false" 开启渐变动画(是一个翻转的效果)-->
      <!--调用handleClose移除标签-->
      <el-tag
        :key="tag"
        v-for="tag in article.dynamicTags"
        closable
        :disable-transitions="false"
        @close="handleClose(tag)" style="margin-left: 10px">
        {{tag}}
      </el-tag>

// script部分
      handleClose(tag) {
        this.article.dynamicTags.splice(this.article.dynamicTags.indexOf(tag), 1);
      },

输入与按钮
@keyup.enter.native@blur就是回车和失去焦点都会触发。

// template部分
      <!--这个也在前面,是点击了button之后才展示-->
      <!--可以通过回车键确定,也可以失去焦点确定-->
      <el-input
        class="input-new-tag"
        v-if="tagInputVisible"
        v-model="tagValue"
        ref="saveTagInput"
        size="small"
        @keyup.enter.native="handleInputConfirm"
        @blur="handleInputConfirm">
      </el-input>
      <!--这一套都是固定的-->
      <el-button v-else class="button-new-tag" type="primary" size="small" @click="showInput">+Tag</el-button>

// script部分
      showInput() {
        this.tagInputVisible = true;
        this.$nextTick(_ => {
          this.$refs.saveTagInput.$refs.input.focus();
        });
      },
      handleInputConfirm() {
        let tagValue = this.tagValue;
        if (tagValue) {
          this.article.dynamicTags.push(tagValue);
        }
        this.tagInputVisible = false;
        this.tagValue = '';
      }

markdown输入
这里是调用的库mavon-editor,但是图像上传和删除是自己写的方法。

// template部分
      <div id="editor">
        <!--用的库,但是图像的两个方法是自己定义的-->
        <mavon-editor style="height: 100%;width: 100%;" ref=md @imgAdd="imgAdd"
                      @imgDel="imgDel" v-model="article.mdContent"></mavon-editor>
      </div>

// script部分
      imgAdd(pos, $file){
        var _this = this;
        // 第一步.将图片上传到服务器.
        var formdata = new FormData();
        formdata.append('image', $file);
        uploadFileRequest("/article/uploadimg", formdata).then(resp=> {
          var json = resp.data;
          if (json.status == 'success') {
           // _this.$refs.md.$imgUpdateByUrl(pos, json.msg)
            _this.$refs.md.$imglst2Url([[pos, json.msg]])
          } else {
            _this.$message({type: json.status, message: json.msg});
          }
        });
      },
      // 删除就是啥也不做?
      imgDel(pos){

      },

撤销修改与保存发布
撤销操作还是用的起那面的this.$router.go(-1)功能,保存就是把内容插入or更新到数据库中,调用后端方法。

// template部分
        <!--这是修改才有的button-->
        <el-button @click="cancelEdit" v-if="from!=undefined">放弃修改</el-button>
        <!--如果是新的或者是草稿-->
        <template v-if="from==undefined || from=='draft'">
          <el-button @click="saveBlog(0)">保存到草稿箱</el-button>
          <el-button type="primary" @click="saveBlog(1)">发表文章</el-button>
        </template>
        <template v-else="from==post">
          <el-button type="primary" @click="saveBlog(1)">保存修改</el-button>
        </template>

// script部分
      cancelEdit(){
        this.$router.go(-1)
      },
      saveBlog(state){
        if (!(isNotNullORBlank(this.article.title, this.article.mdContent, this.article.cid))) {
          this.$message({type: 'error', message: '数据不能为空!'});
          return;
        }
        // 将所有的信息都重新整一遍,估计后端那边接收的是一个Article类
        var _this = this;
        _this.loading = true;
        postRequest("/article/", {
          id: _this.article.id,
          title: _this.article.title,
          mdContent: _this.article.mdContent,
          htmlContent: _this.$refs.md.d_render,
          cid: _this.article.cid,
          state: state,
          dynamicTags: _this.article.dynamicTags
        }).then(resp=> {
          _this.loading = false;
          if (resp.status == 200 && resp.data.status == 'success') {
            _this.article.id = resp.data.msg;
            _this.$message({type: 'success', message: state == 0 ? '保存成功!' : '发布成功!'});
//            if (_this.from != undefined) {
            window.bus.$emit('blogTableReload')
//            }
            if (state == 1) {
              _this.$router.replace({path: '/articleList'});
            }
          }
        }, resp=> {
          _this.loading = false;
          _this.$message({type: 'error', message: state == 0 ? '保存草稿失败!' : '博客发布失败!'});
        })
      },

BlogCfg:博客配置

VBlog项目代码理解之前端

邮箱校验
主要就是以邮箱校验功能,通过ref="emailValidateForm"实现(还是说type="email"?),其他的都是基本操作

        <!--这应该是个邮箱校验,像是自己定义的?-->
        <!--也不是,就是撞名了,这么常用的功能被定义很正常啊-->
        <!--条件是有@有.-->
        <el-form :model="emailValidateForm" label-position="top" ref="emailValidateForm"
                 style="color:#20a0ff;font-size: 14px;">
          <el-form-item
            prop="email"
            label="开启博客评论通知"
            :rules="[{type: 'email', message: '邮箱格式不对哦!'}]">
            <el-input type="email" v-model.email="emailValidateForm.email" auto-complete="off" style="width: 300px"
                      placeholder="请输入邮箱地址..." size="mini"></el-input>
            <el-button type="primary" @click="submitForm('emailValidateForm')" size="mini">确定</el-button>
          </el-form-item>
        </el-form>

完整代码

<template>
  <el-card style="width: 500px" v-loading="loading">
    <div>
      <div style="text-align: left">
        <!--这应该是个邮箱校验,像是自己定义的?-->
        <!--也不是,就是撞名了,这么常用的功能被定义很正常啊-->
        <!--条件是有@有.-->
        <el-form :model="emailValidateForm" label-position="top" ref="emailValidateForm"
                 style="color:#20a0ff;font-size: 14px;">
          <el-form-item
            prop="email"
            label="开启博客评论通知"
            :rules="[{type: 'email', message: '邮箱格式不对哦!'}]">
            <el-input type="email" v-model.email="emailValidateForm.email" auto-complete="off" style="width: 300px"
                      placeholder="请输入邮箱地址..." size="mini"></el-input>
            <el-button type="primary" @click="submitForm('emailValidateForm')" size="mini">确定</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
  </el-card>
</template>
<script>
  import {getRequest} from '../utils/api'
  import {putRequest} from '../utils/api'
  export default{
    data(){
      return {
        emailValidateForm: {
          email: ''
        },
        loading: false
      }
    },
    mounted: function () {
      var _this = this;
      getRequest("/currentUserEmail").then(resp=> {
        if (resp.status == 200) {
          _this.emailValidateForm.email = resp.data;
        }
      });
    },
    methods: {
      submitForm(formName) {
        var _this = this;
        this.$refs[formName].validate((valid) => {
          if (valid) {
            _this.loading = true;
            putRequest("/updateUserEmail", {email: _this.emailValidateForm.email}).then(resp=> {
              _this.loading = false;
              if (resp.status == 200) {
                _this.$message({type: resp.data.status, message: resp.data.msg});
              } else {
                _this.$message({type: 'error', message: '开启失败!'});
              }
            }, resp=> {
              _this.loading = false;
              _this.$message({type: 'error', message: '开启失败!'});
            });
          } else {
            _this.$message({type: 'error', message: '邮箱格式不对哦!'})
            return false;
          }
        });
      }
    }
  }
</script>

CateMana:栏目管理

VBlog项目代码理解之前端
到了这里大部分功能上面已经讲过了,也就删除这里有点新东西

  • 403 (禁止) 服务器拒绝请求。
  • 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。

由数据库的关系图可知,栏目与文章有绑定,所以不能乱删。
VBlog项目代码理解之前端

      deleteCate(ids){
        var _this = this;
        this.loading = true;
        //删除
        deleteRequest("/admin/category/" + ids).then(resp=> {
          var json = resp.data;
          _this.$message({
            type: json.status,
            message: json.msg
          });
          _this.refresh();
        }, resp=> {
          _this.loading = false;
          if (resp.response.status == 403) {
            _this.$message({
              type: 'error',
              message: resp.response.data
            });
          } else if (resp.response.status == 500) {
            _this.$message({
              type: 'error',
              message: '该栏目下尚有文章,删除失败!'
            });
          }
        })
      },

完整代码

package org.sang.controller;

import org.sang.bean.Category;
import org.sang.bean.RespBean;
import org.sang.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 超级管理员专属Controller
 */
@RestController
@RequestMapping("/admin/category")
public class CategoryController {
    @Autowired
    CategoryService categoryService;

    // 输出到外面,所以外面get?
    @RequestMapping(value = "/all", method = RequestMethod.GET)
    public List<Category> getAllCategories() {
        return categoryService.getAllCategories();
    }

    @RequestMapping(value = "/{ids}", method = RequestMethod.DELETE)
    public RespBean deleteById(@PathVariable String ids) {
        boolean result = categoryService.deleteCategoryByIds(ids);
        if (result) {
            return new RespBean("success", "删除成功!");
        }
        return new RespBean("error", "删除失败!");
    }

    // 根据method匹配上了,话说为啥是Post?从外面Post进来的?
    @RequestMapping(value = "/", method = RequestMethod.POST)
    public RespBean addNewCate(Category category) {

        if ("".equals(category.getCateName()) || category.getCateName() == null) {
            return new RespBean("error", "请输入栏目名称!");
        }

        int result = categoryService.addCategory(category);

        if (result == 1) {
            return new RespBean("success", "添加成功!");
        }
        return new RespBean("error", "添加失败!");
    }

    @RequestMapping(value = "/", method = RequestMethod.PUT)
    public RespBean updateCate(Category category) {
        int i = categoryService.updateCategoryById(category);
        if (i == 1) {
            return new RespBean("success", "修改成功!");
        }
        return new RespBean("error", "修改失败!");
    }
}


UserMana:员工显示与管理

VBlog项目代码理解之前端

卡片
使用el-car实现,因为有很多卡片,所以还是要用到v-for,具体要展示的内容在夹层中显示

禁用按钮
通过el-switch实现,可以通过active-textinactive-text来设置**与未**时显示的文字,调用了个方法,看不懂有啥用

// template部分
        <!--开关,这样activate-text的值是什么样的?-->
        <!--应该就是true or false,因为user.enabled就是true or false-->
        <el-switch
          v-model="user.enabled"
          active-text="启用"
          active-color="#13ce66"
          @change="enabledChange(user.enabled,user.id,index)"
          inactive-text="禁用" style="font-size: 12px">
        </el-switch>

// script部分
      enabledChange(enabled, id, index){
        var _this = this;
        _this.cardloading.splice(index, 1, true)
        putRequest("/admin/user/enabled", {enabled: enabled, uid: id}).then(resp=> {
          if (resp.status != 200) {
            _this.$message({type: 'error', message: '更新失败!'})
            _this.loadOneUserById(id, index);
            return;
          }
          _this.cardloading.splice(index, 1, false)
          _this.$message({type: 'success', message: '更新成功!'})
        }, resp=> {
          _this.$message({type: 'error', message: '更新失败!'})
          _this.loadOneUserById(id, index);
        });
      },
      loadOneUserById(id, index){
        var _this = this;
        getRequest("/admin/user/" + id).then(resp=> {
          _this.cardloading.splice(index, 1, false)
          if (resp.status == 200) {
            _this.users.splice(index, 1, resp.data);
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }, resp=> {
          _this.cardloading.splice(index, 1, false)
          if (resp.response.status == 403) {
            var data = resp.response.data;
            _this.$message({type: 'error', message: data});
          }
        });
      },

角色管理
VBlog项目代码理解之前端

  • el-popover为弹出框,即左边的箭头点击触发,其中的placement="right"控制弹出的方向,trigger属性用于设置何时触发 Popover,支持四种触发方式:hoverclickfocusmanual;使用具名插槽slot="reference"来**popover,即代码中的el-button部分,对应上图左边按钮。
  • 可在el-popover中嵌套内容,如展示列表、选择,这里嵌入了el-select,可修改roles的值,所以会影响前面tag的显示,按右边箭头可出现选择,这个选择也自带删除功能。
  • 使用v-loading在接口为请求到数据之前,显示加载中,直到请求到数据后消失,这里eploading[index]里面都是true or false
  • 代码中有两个函数,saveRolesshowRoleshowRole函数的作用是从数据库读取userroles,然后赋值给vue类的变量this.roles,用这个变量来展示并获取多选的变化,然后saveRolesthis.roles替换数据库中userroles。之所以不像el-tag一样直接使用user.roles,是因为这一步是要变化的,前端不能直接修改数据库内容,要依靠后端完成。
// template部分
            <el-tag
              v-for="role in user.roles"
              :key="role.id"
              size="mini"
              style="margin-right: 8px"
              type="success">
              {{role.name}}
            </el-tag>
            <!--placement确定展示方向-->
            <el-popover
              placement="right"
              title="角色列表"
              width="200"
              :key="index+''+user.id"
              @hide="saveRoles(user.id,index)"
              trigger="click" v-loading="eploading[index]">
              <!--trigger是触发器设置-->
              <!--这里嵌套了一个选择器,是在<></>中夹着的-->
              <!--roles是一个数组,multiple决定了多选,多选的结果保存到roles-->
              <!--但是显示和删除是怎么做到的?答:这个选择自带的-->
              <el-select v-model="roles" :key="user.id" multiple placeholder="请选择" size="mini">
                <el-option
                  v-for="(item,index) in allRoles"
                  :key="user.id+'-'+item.id"
                  :label="item.name"
                  :value="item.id">
                </el-option>
              </el-select>
              <!--点击这个button会出现框-->
              <el-button type="text" icon="el-icon-more" style="padding-top: 0px" slot="reference"
                         @click="showRole(user.roles,user.id,index)"></el-button>
            </el-popover>

// script部分
      saveRoles(id, index){
        // 获取变化的roles来改变数据库中user的roles
        var selRoles = this.roles;
        if (this.cpRoles.length == selRoles.length) {
          for (var i = 0; i < this.cpRoles.length; i++) {
            for (var j = 0; j < selRoles.length; j++) {
              if (this.cpRoles[i].id == selRoles[j]) {
                selRoles.splice(j, 1);
                break;
              }
            }
          }
          if (selRoles.length == 0) {
            return;
          }
        }
        var _this = this;
        _this.cardloading.splice(index, 1, true)
        putRequest("/admin/user/role", {rids: this.roles, id: id}).then(resp=> {
          if (resp.status == 200 && resp.data.status == 'success') {
            _this.$message({type: resp.data.status, message: resp.data.msg});
            _this.loadOneUserById(id, index);
          } else {
            _this.cardloading.splice(index, 1, false)
            _this.$message({type: 'error', message: '更新失败!'});
          }
        }, resp=> {
          _this.cardloading.splice(index, 1, false)
          if (resp.response.status == 403) {
            var data = resp.response.data;
            _this.$message({type: 'error', message: data});
          }
        });
      },
      showRole(aRoles, id, index){
        this.cpRoles = aRoles;
        this.roles = [];
        // 获取数据库中roles
        this.loadRoles(index);
        for (var i = 0; i < aRoles.length; i++) {
          this.roles.push(aRoles[i].id);
        }
      },
相关标签: 项目经验