文章目录
VBlog项目代码理解之前端资源配置问题config/index:通过代理解决跨域问题router:页面跳转控制utils/api.js:调用后端方法并接受返回值filter_utils:过滤器main:导入依赖组件Login:登录与权限验证Home:主页面框架ArticleList:显示文章列表BlogTable:具体文章列表BlogDetail:文章显示PostArticle:编辑与发表BlogCfg:博客配置CateMana:栏目管理UserMana:员工显示与管理VBlog项目代码理解之前端
资源
项目地址
前后端交互理解
后端代码理解
推荐:整个项目几乎是只用到了SpringBoot、Vue、Mybatis、ElementUI,没有用到Redis、RabbitMQ等内容,很适合刚学完SpringBoot和Vue的同学练手,感谢作者!帮作者打个广告吧~
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 settingshost: '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 determinedautoOpenBrowser: false,errorOverlay: true,notifyOnErrors: true,poll: false, // /configuration/dev-server/#devserver-watchoptions-/*** Source Maps*/// /configuration/devtool/#developmentdevtool: 'eval-source-map',// debug工具出问题的时候可以试试改为false,可能会有用cacheBusting: true,// 是否生成css、map文件,说可能存在问题,但是没啥必要用这个,出问题了可以控制台// 所以这里于默认不同,设为falsecssSourceMap: 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-pluginproductionGzip: 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用到的时候importexport 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:导入依赖
导入使用的外部组件,如ElementtUI
、VCharts
,还有过滤器也是在这里导入的。
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:登录与权限验证
登录页面是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:主页面框架
登录成功就来到Home。
v-if="!item.hidden":
通过hidden来控制是否显示
slot=“title”:是系统自带的吗,找不到东西
Breadcrumb 面包屑:
el-breadcrumb
作用显示当前页面的路径,快速返回之前的任意页面,之前的页面用el-breadcrumb-item
表示
可以控制最上面的导航,加了点东西就变成下一行了
激活判断:
代码中可以根据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 = '游客';});},
下拉框:
通过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-menudefault-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-menudefault-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:
建多个按钮用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:具体文章列表
模糊搜索:
比较常规的操作
// template部分<div style="display: flex;justify-content: flex-start"><!--搜索输入框,还设置了图标,这里应该是要用到数据库了吧--><!--确实用到了,主要是keywords生效--><el-inputplaceholder="通过标题搜索该分类下的博客..."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-tableref="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-columntype="selection"width="35" align="left" v-if="showEdit || showDelete"></el-table-column>
然后其他的列可以用lable
来设置列名,还可以绑定点击触发事件。
<el-table-columnlabel="标题"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-tableref="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-columnlabel="最近编辑时间" width="140" align="left"><template slot-scope="scope">{{scope.row.editTime | formatDateTime}}</template></el-table-column>
点击查看详细内容:
会跳到blogDetail
页面
// template部分<el-table-columnlabel="标题"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"
),所以可以使用数据库有的属性,如id
、title
。
scope.$index
→拿到每一行的index
,scope.$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)},
分页功能:
翻完了要重新加载一次
// tmeplate部分<!--分页功能--><!--layout中的内容用逗号隔开,内容会依次显示--><!--background可以显示背景颜色--><!--v-show指定展示条件--><!--current-change:页面改动时会触发,这里是重新加载了数据--><el-paginationbackground: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-buttonsize="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-buttonsize="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-buttonsize="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-inputplaceholder="通过标题搜索该分类下的博客..."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-tableref="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-columntype="selection"width="35" align="left" v-if="showEdit || showDelete"></el-table-column><el-table-columnlabel="标题"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-columnlabel="最近编辑时间" width="140" align="left"><template slot-scope="scope">{{scope.row.editTime | formatDateTime}}</template></el-table-column><el-table-columnprop="nickname"label="作者"width="120" align="left"></el-table-column><el-table-columnprop="cateName"label="所属分类"width="120" align="left"></el-table-column><el-table-column label="操作" align="left" v-if="showEdit || showDelete"><template slot-scope="scope"><!--调用了下面的方法, 参数是用来指定自身的吧--><el-buttonsize="mini"@click="handleEdit(scope.$index, scope.row)" v-if="showEdit">编辑</el-button><el-buttonsize="mini"@click="handleRestore(scope.$index, scope.row)" v-if="showRestore">还原</el-button><el-buttonsize="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-paginationbackground: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) {// 回收站+idthis.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:文章显示
返回:
向后跳转一个页面,通过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:编辑与发表
el-header:
使用el-header
将栏目选择、标题、tag三个包裹到一起,不然的话就会变得松散,而且和el-main
混到了一行,如下图:
选择栏目:
使用el-select
完成,在el-option
中做遍历,最终影响的是article.cid
的值。
<!--从数据库获取栏目对应id信息,显示还得看label--><el-select v-model="article.cid" placeholder="请选择文章栏目" style="width: 150px;"><el-optionv-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-if
和v-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-inputclass="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:博客配置
邮箱校验:
主要就是以邮箱校验功能,通过ref="emailValidateForm"
实现(还是说type="email"
?),其他的都是基本操作
<!--这应该是个邮箱校验,像是自己定义的?--><!--也不是,就是撞名了,这么常用的功能被定义很正常啊--><!--条件是有@有.--><el-form :model="emailValidateForm" label-position="top" ref="emailValidateForm"style="color:#20a0ff;font-size: 14px;"><el-form-itemprop="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-itemprop="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:栏目管理
到了这里大部分功能上面已经讲过了,也就删除这里有点新东西
403 (禁止) 服务器拒绝请求。503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
由数据库的关系图可知,栏目与文章有绑定,所以不能乱删。
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 {@AutowiredCategoryService 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:员工显示与管理
卡片:
使用el-car
实现,因为有很多卡片,所以还是要用到v-for
,具体要展示的内容在夹层中显示
禁用按钮:
通过el-switch
实现,可以通过active-text
和inactive-text
来设置激活与未激活时显示的文字,调用了个方法,看不懂有啥用
// template部分<!--开关,这样activate-text的值是什么样的?--><!--应该就是true or false,因为user.enabled就是true or false--><el-switchv-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});}});},
角色管理:
el-popover
为弹出框,即左边的箭头点击触发,其中的placement="right"
控制弹出的方向,trigger
属性用于设置何时触发 Popover,支持四种触发方式:hover
,click
,focus
和manual
;使用具名插槽slot="reference"
来激活popover
,即代码中的el-button
部分,对应上图左边按钮。可在el-popover
中嵌套内容,如展示列表、选择,这里嵌入了el-select
,可修改roles
的值,所以会影响前面tag
的显示,按右边箭头可出现选择,这个选择也自带删除功能。使用v-loading在接口为请求到数据之前,显示加载中,直到请求到数据后消失,这里eploading[index]
里面都是true
orfalse
。代码中有两个函数,saveRoles
和showRole
。showRole
函数的作用是从数据库读取user
的roles
,然后赋值给vue类的变量this.roles
,用这个变量来展示并获取多选的变化,然后saveRoles
用this.roles
替换数据库中user
的roles
。之所以不像el-tag
一样直接使用user.roles
,是因为这一步是要变化的,前端不能直接修改数据库内容,要依靠后端完成。
// template部分<el-tagv-for="role in user.roles":key="role.id"size="mini"style="margin-right: 8px"type="success">{{role.name}}</el-tag><!--placement确定展示方向--><el-popoverplacement="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-optionv-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的rolesvar 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 = [];// 获取数据库中rolesthis.loadRoles(index);for (var i = 0; i < aRoles.length; i++) {this.roles.push(aRoles[i].id);}},