VBlog项目代码理解之前端
文章目录
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 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:导入依赖
导入使用的外部组件,如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-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:
建多个按钮用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-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>
然后其他的列可以用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>
点击查看详细内容:
会跳到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"
),所以可以使用数据库有的属性,如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-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:文章显示
返回:
向后跳转一个页面,通过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-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-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-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:博客配置
邮箱校验:
主要就是以邮箱校验功能,通过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:栏目管理
到了这里大部分功能上面已经讲过了,也就删除这里有点新东西
- 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 {
@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:员工显示与管理
卡片:
使用el-car
实现,因为有很多卡片,所以还是要用到v-for
,具体要展示的内容在夹层中显示
禁用按钮:
通过el-switch
实现,可以通过active-text
和inactive-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});
}
});
},
角色管理:
-
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-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);
}
},
推荐阅读
-
spring学习之创建项目 Hello Spring实例代码
-
个人项目开源之c++基于epoll实现高并发游戏盒子(服务端+客户端)源代码
-
.NET Core实战项目之CMS 第十一章 开发篇-数据库生成及实体代码生成器开发
-
代码着色之SyntaxHighlighter项目(最流行的代码高亮)
-
优酷项目之 ORM(数据库对象关系映射)代码重写
-
Vue学习之项目部分代码(十八)
-
个人项目开发记录---前端(vue3.0+elementui)之项目搭建及推送远程库
-
Android项目实战之Glide 高斯模糊效果的实例代码
-
深入理解移动前端开发之viewport
-
详解webpack打包nodejs项目(前端代码)