前端设计流程图:
1. 环境搭建
- 在Node.js官网安装Node.js
- 安装Vue 2,使用npm安装
npm install vue@^2
- 安装命令行工具Vue Cli
npm install -g @vue/cli
- 打开Vue可视化管理工具界面,新建Vue项目:
vue ui
- 在项目根目录下安装element-ui
npm install element-ui --save
- 同样的,安装axios
cnpm install axios --save
- 安装MarkdownIt和md样式
npm install markdown-it --save
npm install github-markdown-css
- 安装mavon-editor
npm install mavon-editor --save
2. 配置文件
- main.js
将上述第三方库导入main.js中
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Element from 'element-ui'
import axios from 'axios'
import mavonEditor from 'mavon-editor';
import 'element-ui/lib/theme-chalk/index.css'
import 'mavon-editor/dist/css/index.css'
import 'github-markdown-css/github-markdown.css'
Vue.use(Element)
Vue.use(mavonEditor)
Vue.config.productionTip = false
Vue.prototype.$axios = axios
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- router.js
按照项目功能要求配置路由文件
import Vue from 'vue'
import VueRouter from 'vue-router'
import Blogs from '../views/Blogs.vue'
import BlogEdit from '../views/BlogEdit.vue'
import Login from '../views/Login.vue'
import BlogDetail from '../views/BlogDetail.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
redirect: {name: "Blogs"}
},
{
path: '/blogs',
name: 'Blogs',
component: Blogs
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/blog/add',
name: 'BlogAdd',
component: BlogEdit,
meta: {
requireAuth: true,
}
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
component: BlogEdit,
meta: {
requireAuth: true,
}
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
- App.vue
配置页面背景色
body {
background: linear-gradient(to right, #65CBF7, #B3A5FC);
width: 100vw;
height: 100vh;
margin: 0;
}
- store.js
全局配置存储
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: '',
userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo
sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
},
REMOVE_INFO: (state) => {
localStorage.setItem("token", '')
sessionStorage.setItem("userInfo", JSON.stringify(''))
state.userInfo = {}
}
},
getters: {
getUser: state => {
return state.userInfo
}
},
actions: {},
modules: {}
})
3. 设计登录组件
HTML+CSS设计样式,配合ElementUI设计登录组件
<template>
<div>
<Header></Header>
<div class="box">
<div class="left">
<img src="./img/Login.jpg" alt="">
</div>
<div class="right">
<h1>Login</h1>
<el-main>
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px"
class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input type="text" maxlength="12" v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-main>
</div>
</div>
</div>
</template>
CSS:
body {
background: linear-gradient(to right, #65CBF7, #B3A5FC);
width: 100vw;
height: 100vh;
margin: 0;
}
.box {
width: 60%;
height: 450px;
box-shadow: 0 5px 15px rgba(0, 0, 0, .8);
display: flex;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.left {
width: 65%;
}
.left > img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.right {
width: 35%;
height: 100%;
background-color: #fff;
box-sizing: border-box;
padding: 0 20px;
}
h1 {
text-align: center;
padding-top: 45px;
margin-top: 0;
}
实现submitForm通过Axios提交表单至后端进行登录验证
import Header from "../components/Header";
export default {
name: 'Login',
components: {Header},
data() {
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
callback();
}
};
return {
ruleForm: {
password: '',
username: ''
},
rules: {
password: [
{validator: validatePass, trigger: 'blur', required: true}
],
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, max: 12, message: '长度在 3 到 12 个字符', trigger: 'blur'}
]
}
};
},
methods: {
submitForm(formName) {
const _this = this
this.$refs[formName].validate((valid) => {
if (valid) {
// 提交逻辑
this.$axios.post('http://localhost:8081/login', this.ruleForm).then((res) => {
const token = res.headers['authorization']
_this.$store.commit('SET_TOKEN', token)
_this.$store.commit('SET_USERINFO', res.data.data)
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
}
4. 设计头部导航栏
HTML+CSS:
<template>
<div>
<header>
<nav>
<div class="logo-contain">
<img class="blogLogo" src="../views/img/LOGO.jpg" alt="">
<div class="navText">YUDATI</div>
</div>
<div class="menu">
<div class="menu-item">
<router-link to="/blogs">Home</router-link>
</div>
<div class="divider">|</div>
<div v-show="!hasLogin" class="menu-item">
<router-link to="/login">Login</router-link>
</div>
<div v-show="hasLogin" class="menu-item">
<router-link to="/blog/add">AddBlog</router-link>
</div>
<div v-show="hasLogin" class="divider">|</div>
<div v-show="hasLogin" class="menu-item">
<a @click="logout">Logout</a>
</div>
</div>
</nav>
</header>
</div>
</template>
CSS:
header {
margin: 0 auto;
width: 100vw;
background: #ffffff;
}
nav {
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between; /* Adjust spacing between logo and menu items */
padding: 10px 20px; /* Add padding to nav container */
}
.menu {
display: flex;
align-items: center;
}
.logo-contain {
display: flex;
align-items: center;
}
.menu-item {
color: #ffffff;
padding: 10px 20px;
position: relative;
text-align: center;
border-bottom: 3px solid transparent;
display: flex;
transition: 0.4s;
}
.menu-item.active,
.menu-item:hover {
background-color: #ffffff;
border-bottom-color: #7f2978;
}
.menu-item a {
font-size: 22px;
color: #7f2978;
text-decoration: none;
}
.block {
position: relative;
/* font-weight: bold; */
font-size: 22px;
color: #7f2978;
text-decoration: none;
}
.navText {
/* margin-right: 500px; */
position: relative;
font-weight: bold;
font-size: 30px;
color: #65CBF7;
text-decoration: none;
}
.blogLogo {
position: relative;
width: 50px;
height: 50px;
border-radius: 50px;
}
实现Logout登出,在全局配置中清空userInfo,created初始化导航栏监控用户信息:
export default {
name: "Header",
data() {
return {
hasLogin: false,
user: {
username: '',
avatar: ''
},
blogs: {},
currentPage: 1,
total: 0
}
},
methods: {
logout() {
const _this = this;
this.hasLogin = false;
this.$axios.get('http://localhost:8081/logout', {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then((res) => {
_this.$store.commit('REMOVE_INFO')
_this.$router.push('/login')
});
}
},
created() {
if (this.$store.getters.getUser.username) {
this.user.username = this.$store.getters.getUser.username
this.user.avatar = this.$store.getters.getUser.avatar
this.hasLogin = true
}
}
}
5. 设计博客主页
ElementUI时间线组件创建博客列表:
<template>
<div>
<Header></Header>
<div class="block">
<el-timeline>
<el-timeline-item class="summary" v-bind:timestamp="blog.created" placement="top" v-for="blog in blogs">
<el-card>
<router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}"><h2 class="mtitle">{{ blog.title }}</h2>
</router-link>
<p class="describe">{{ blog.description }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<el-pagination class="mpage"
background
layout="prev, pager, next"
:current-page=currentPage
:page-size=pageSize
@current-change=page
:total="total">
</el-pagination>
</div>
</template>
CSS:
.block {
margin: 0 auto;
max-width: 1080px;
}
.summary::v-deep .el-timeline-item__timestamp {
font-family: 'Consola', sans-serif;
font-size: 16px;
color: #ffffff !important;
}
.mpage {
margin: 0 auto;
text-align: center;
}
.describe {
font: 'Consolas';
}
.mtitle {
margin: 0px auto;
font-size: 20px;
font: 'Consola';
font-weight: bolder;
text-decoration: none !important;
}
page()通过Axios从后端获取页面信息并展示:
import Header from "@/components/Header";
export default {
name: "Blogs",
components: {Header},
data() {
return {
blogs: {},
currentPage: 1,
total: 0,
pageSize: 5
}
},
methods: {
page(currentPage) {
const _this = this
this.$axios.get('http://localhost:8081/blogs?currentPage=' + currentPage).then((res) => {
console.log(res.data.data.records)
_this.blogs = res.data.data.records
_this.currentPage = res.data.data.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
mounted() {
this.page(1);
}
}
6. 设计博客内容详情页
HTML+CSS
<template>
<div>
<Header></Header>
<div class="box">
<div class="mblog">
<h2>{{ blog.title }}</h2>
<router-link :to="{name: 'BlogEdit', params: {blogId: blog.id}}">
<el-link icon="el-icon-edit" v-if="ownBlog">编辑</el-link>
</router-link>
<el-divider></el-divider>
<div class="content markdown-body" v-html="blog.content"></div>
</div>
</div>
</div>
</template>
CSS:
.box {
background-color: aliceblue;
margin-top: 20px;
margin-left: 15%;
width: 70%;
min-height: 700px;
box-shadow: 0 5px 15px rgba(0, 0, 0, .8);
display: flex;
}
.mblog {
margin: 30px;
}
.mtitle {
margin: 10px;
text-align: center;
}
getBlog()获取博客信息,MarkdownIt渲染
import 'github-markdown-css/github-markdown.css' // 然后添加样式markdown-body
import Markdown from '@/components/Markdown'
import Header from "@/components/Header";
export default {
name: "BlogDetail",
components: {
Header, Markdown,
},
data() {
return {
blog: {
userId: null,
title: "",
description: "",
content: ""
},
ownBlog: false
}
},
methods: {
getBlog() {
const blogId = this.$route.params.blogId
const _this = this
this.$axios.get('/blog/' + blogId).then((res) => {
console.log(res)
console.log(res.data.data)
_this.blog = res.data.data
var MarkdownIt = require('markdown-it'),
md = new MarkdownIt();
var result = md.render(_this.blog.content);
_this.blog.content = result
// 判断是否是自己的文章,能否编辑
_this.ownBlog = (_this.blog.userId === _this.$store.getters.getUser.id)
});
}
},
created() {
this.getBlog()
}
}
7. 设计博客编辑页
ElementUI表单组件+mavon-editor构成组件:
<template>
<div>
<Header></Header>
<div class="box">
<div class="m-content">
<el-form ref="editForm" status-icon :model="editForm" :rules="rules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="editForm.title"></el-input>
</el-form-item>
<el-form-item label="摘要" prop="description">
<el-input type="textarea" v-model="editForm.description"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<mavon-editor v-model="editForm.content"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm()">Submit</el-button>
<router-link to="/">
<el-button>Cancel</el-button>
</router-link>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
CSS:
.box {
background-color: aliceblue;
margin-top: 20px;
margin-left: 12.5%;
width: 75%;
min-height: 700px;
box-shadow: 0 5px 15px rgba(0, 0, 0, .8);
display: flex;
}
.m-content {
margin: 30px;
margin-right: 60px;
}
submitForm提交表单至后端
import Header from "@/components/Header";
export default {
name: "BlogEdit",
components: {Header},
data() {
return {
editForm: {
id: null,
title: '',
description: '',
content: ''
},
rules: {
title: [
{required: true, message: '请输入标题', trigger: 'blur'},
{min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur'}
],
description: [
{required: true, message: '请输入摘要', trigger: 'blur'}
]
}
}
},
created() {
const blogId = this.$route.params.blogId
const _this = this
if (blogId) {
this.$axios.get('/blog/' + blogId).then((res) => {
const blog = res.data.data
_this.editForm.id = blog.id
_this.editForm.title = blog.title
_this.editForm.description = blog.description
_this.editForm.content = blog.content
});
}
},
methods: {
submitForm() {
const _this = this
this.$refs.editForm.validate((valid) => {
if (valid) {
this.$axios.post('/blog/edit', this.editForm, {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then((res) => {
_this.$alert('操作成功', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
});
} else {
console.log('error submit!!');
return false;
}
})
}
}
}
8. 配置拦截
aixos.js:
import axios from 'axios'
import Element from "element-ui";
import store from "./store";
import router from "./router";
axios.defaults.baseURL = 'http://localhost:8081'
axios.interceptors.request.use(config => {
console.log("前置拦截")
// 可以统一设置请求头
return config
})
axios.interceptors.response.use(response => {
const res = response.data;
console.log("后置拦截")
// 当结果的code是否为200的情况
if (res.code === 200) {
return response
} else {
// 弹窗异常信息
Element.Message({
message: response.data.msg,
type: 'error',
duration: 2 * 1000
})
// 直接拒绝往下面返回结果信息
return Promise.reject(response.data.msg)
}
},
error => {
console.log('err' + error)// for debug
if (error.response.data) {
error.message = error.response.data.msg
}
// 根据请求状态觉得是否登录或者提示其他
if (error.response.status === 401) {
store.commit('REMOVE_INFO');
router.push({
path: '/login'
});
error.message = '请重新登录';
}
if (error.response.status === 403) {
error.message = '权限不足,无法访问';
}
Element.Message({
message: error.message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
})
permission.js:
import router from "./router";
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) {
const token = localStorage.getItem("token");
console.log("--------------" + token);
if (token) {
if (to.path == '/login') {
} else {
next();
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
在main.js中导入
import "./axios"
import "./permission.js"