原文网址:前后端分离简单项目--蚂蚁博客--前端部分_IT利刃出鞘的博客-CSDN博客
简介
说明
** **本文介绍我从0开发的前后端分离的简单项目--蚂蚁博客。本博文介绍前端部分。
本项目是一个全栈项目,使用主流、前沿的技术栈开发,项目虽小,五脏俱全。
后期我会出一个视频,详细讲解本项目。视频录完后会将链接贴到本文。
项目介绍
见:前后端分离简单项目--蚂蚁博客--简介_IT利刃出鞘的博客-CSDN博客
项目源码
gitee地址:ant_blog: 项目名:蚂蚁博客。前后端分离,后端采用SpringBoot,前端采用Vue。
ant_backend目录是前端部分的代码。
技术栈
- vue(^2.6.11)
- vue-router(^3.2.0)
- vuex(^3.4.0)
- element-uI(^2.15.0)
- github-markdown-css(^4.0.0)
- markdown-it(^12.0.4)
- mavon-editor(^2.9.1)
项目结构
概述
- 业务部分(views文件夹):按模块进行划分;
- 公共部分(axio.js, permission.js):全局请求拦截、全局响应拦截,权限处理(是否加token)
- 组件部分(components文件夹):侧边栏、顶栏
- 插件部分(router、store文件夹):vue-router插件、vuex插件
项目概览
依赖及配置
依赖
package.json
{
"name": "ant-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"element-ui": "^2.15.0",
"github-markdown-css": "^4.0.0",
"markdown-it": "^12.0.4",
"mavon-editor": "^2.9.1",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-service": "^4.5.0",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
配置
开发服务器配置
vue.config.js
module.exports = {
devServer: {
proxy: {
'/': {
target: 'http://localhost:9000/',
// ws: true,
changeOrigin: true
}
}
}
}
业务部分
主页
Home.vue
<template>
<div class="home-container">
<top-header></top-header>
<div class="main-container">
<left-aside class="left">
<div class="blogger">
博主列表
</div>
</left-aside>
<user-list class="middle"></user-list>
</div>
</div>
</template>
<script>
import TopHeader from "@/components/TopHeader";
import LeftAside from "@/components/LeftAside";
import UserList from "@/views/user/UserList";
export default {
name: "Home",
components: {TopHeader, LeftAside, UserList},
}
</script>
<style scoped>
.main-container {
display: flex;
margin: 0 50px 0 50px;
}
.middle {
flex: 1;
}
.blogger {
font-size: 20px;
padding: 10px 10px;
background-color: white;
}
</style>
登录页
Login.vue
<template>
<div>
<el-container>
<el-header>
<img class="logo" src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png" alt="">
</el-header>
<el-main>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="userName">
<el-input v-model="ruleForm.userName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password"></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>
</el-container>
</div>
</template>
<script>
import Auth from "@/common/Auth";
export default {
name: "Login",
data() {
return {
ruleForm: {
userName: 'knife',
password: '222333'
},
rules: {
userName: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur'}
],
password: [
{required: true, message: '请选择密码', trigger: 'change'}
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this;
this.$axios.post('/login', this.ruleForm)
.then(res => {
console.log(res.data);
const jwt = res.headers[Auth.HEADER_NAME];
const userInfo = res.data.data;
// 把数据共享出去
_this.$store.commit("SET_TOKEN", jwt);
_this.$store.commit("SET_USERINFO", userInfo);
// 获取
console.log(_this.$store.getters.getUser);
// _this.$router.push("/blogs");
_this.$router.push({name: "UserHome", params: {userName: userInfo.userName}})
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
/*background-color: #E9EEF3;*/
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
.logo {
height: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
</style>
用户页
UserHome.vue(用户主页)
<template>
<div class="user-home-container">
<top-header></top-header>
<div class="main-container">
<left-aside class="left">
<user-profile :userName="userName"></user-profile>
</left-aside>
<blog-list class="middle" :userName="userName"></blog-list>
</div>
</div>
</template>
<script>
import BlogList from "@/views/blog/BlogList";
import TopHeader from "@/components/TopHeader";
import LeftAside from "@/components/LeftAside";
import UserProfile from "@/views/user/UserProfile";
export default {
name: "Home",
components: {TopHeader, LeftAside, BlogList, UserProfile},
data() {
return {
userName: ""
}
},
created() {
this.userName = this.$route.params.userName;
}
}
</script>
<style scoped>
.main-container {
display: flex;
margin: 0 50px 0 50px;
}
.middle {
flex: 1;
}
</style>
**UserList.vue(用户列表) **
<template>
<div class="user-list-container">
<div class="user-list">
<div class="user-item-box"
v-for="user of users"
@click="toUserHome(user.userName)">
<el-avatar class="avatar" :size="50" :src="user.avatarUrl"></el-avatar>
<div class="nick-name">{{ user.nickName }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UserList",
data() {
return {
users: {},
current: 1,
total: 0,
size: 10
}
},
methods: {
page(current, size) {
const _this = this
this.$axios
.get("/user/page",
{
params: {
current: current,
size: size
}
}
)
.then(res => {
_this.users = res.data.data.records
_this.current = res.data.data.current
_this.total = res.data.data.total
_this.size = res.data.data.size
})
},
toUserHome(userName) {
this.$router.push({name: "UserHome", params: {userName: userName}})
}
},
created() {
this.page(1, 10);
}
}
</script>
<style scoped>
.user-list-container {
margin: 0 50px 0 10px;
background-color: white;
}
.user-item-box {
cursor: pointer;
border-bottom: #c2c7d2 solid 1px;
padding: 3px 0 3px 0;
display: flex;
}
.user-item-box:hover {
background-color: rgb(245, 245, 250);
}
.avatar {
margin: 0 20px 5px 5px;
}
.nick-name {
margin: auto 10px;
}
</style>
UserProfile.vue(用户信息)
<template>
<div class="user-profile-container">
<div class="user-information">
<el-avatar class="avatar"
:size="50"
:src="avatarUrl">
</el-avatar>
<div class="nick-name">
{{nickName}}
</div>
</div>
<div class="data-information">
<dl>
<dt>
{{blogCount}}
</dt>
<dd>博客总数</dd>
</dl>
</div>
</div>
</template>
<script>
export default {
name: "UserProfile",
data() {
return {
nickName: '',
avatarUrl: '',
blogCount: 0
}
},
methods: {
getProfile(userName) {
const _this = this;
this.$axios
.get("/user/profile?userName=" + userName)
.then(user => {
_this.nickName = user.data.data.nickName;
_this.avatarUrl = user.data.data.avatarUrl;
_this.blogCount = user.data.data.blogCount
}
)
}
},
created() {
let userName = this.$route.params.userName;
this.getProfile(userName);
}
}
</script>
<style scoped>
.user-profile-container {
background-color: white;
}
.user-information {
display: flex;
margin: 0 0 20px 10px;
}
.nick-name {
flex: 1;
margin: auto 20px;
}
.data-information {
display: flex;
}
dl {
margin: 10px;
text-align: center;
}
dd {
margin: 0;
}
</style>
博客页
BlogList.vue(博客列表页)
<template>
<div class="blog-page-container">
<div class="blog-list">
<div class="blog-item-box" v-for="blog of blogs">
<h3 class="title" @click="viewBlog(blog.id)">
{{ blog.title }}
</h3>
<!-- <h4 class="title">
<router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}">
{{ blog.title }}
</router-link>
</h4>-->
<p class="description">{{ blog.description }}</p>
<span class="date">{{ blog.createTime }}</span>
<span class="operations" v-show="isOwner">
<el-button @click="editBlog(blog.id)" size="mini">
编辑
</el-button>
<!-- <el-button size="mini">
<router-link :to="{name: 'BlogEdit', params: {blogId: blog.id}}">–>
编辑
</router-link>
</el-button>-->
<el-button @click="deleteBlog(blog.id)" size="mini">
删除
</el-button>
</span>
</div>
<div class="page-block">
<el-pagination
class="page-container"
layout="total, prev, pager, next, jumper"
:current-page="current"
:page-size="size"
:total="total"
@current-change=page>
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import TopHeader from "../../components/TopHeader";
import LeftAside from "@/components/LeftAside";
export default {
name: "BlogList",
components: {LeftAside, TopHeader},
data() {
return {
blogs: {},
current: 1,
total: 0,
size: 10,
isOwner: false,
userName: ''
}
},
methods: {
page(currentPage) {
const _this = this
_this.$axios
.get("/blog/page",
{
params: {
current: currentPage,
size: this.size,
userName: this.userName
}
}
)
.then(res => {
console.log(res)
_this.blogs = res.data.data.records
_this.current = res.data.data.current
_this.total = res.data.data.total
_this.size = res.data.data.size
})
},
viewBlog(blogId) {
this.$router.push({name: "BlogDetail", params: {blogId: blogId}})
},
editBlog(blogId) {
this.$router.push({name: "BlogEdit", params: {blogId: blogId}})
},
deleteBlog(blogId) {
const _this = this
this.$axios
.post("/blog/delete?ids=" + blogId,
{}, {
headers: {
"token": localStorage.getItem("token")
}
})
.then(res => {
console.log(res)
_this.$alert('操作成功', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
})
}
},
created() {
this.userName = this.$route.params.userName;
if (this.$store.getters.getUser != null
&& this.userName === this.$store.getters.getUser.userName) {
this.isOwner = true;
}
this.page(1);
}
}
</script>
<style scoped>
.blog-page-container {
margin: 0 50px 0 10px;
background-color: white;
}
.blog-item-box:hover {
background-color: rgb(245, 245, 250);
}
.blog-list {
text-align: left;
}
.blog-list .blog-item-box {
padding: 16px 24px 13px 24px;
border-bottom: 1px solid rgb(126, 150, 130);
position: relative;
}
.blog-list .blog-item-box p.description {
margin: 8px 0 6px 0;
}
.title {
cursor: pointer;
}
.description {
font-size: 14px;
/*只显示两行*/
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.date {
font-size: 14px;
color: #5f6471;
}
.operations {
position: absolute;
right: 24px;
margin: 2px;
}
.page-block {
padding: 20px 0;
}
.page-container {
width: 100px;
margin: auto;
}
</style>
BlogDetail.vue(博客详情页)
<template>
<div>
<Header></Header>
<div class="mblog">
<h2> {{ blog.title }}</h2>
<el-link icon="el-icon-edit" v-if="ownBlog">
<router-link :to="{name: 'BlogEdit', params: {blogId: blog.id}}">
编辑
</router-link>
</el-link>
<el-divider></el-divider>
<div class="markdown-body" v-html="blog.content"></div>
</div>
</div>
</template>
<script>
import 'github-markdown-css'
import Header from "../../components/TopHeader";
export default {
name: "BlogDetail.vue",
components: {Header},
data() {
return {
blog: {
id: "",
title: "",
content: "",
userId: ""
},
ownBlog: false
}
},
created() {
const blogId = this.$route.params.blogId
// console.log(blogId)
const _this = this
this.$axios
.get('/blog/getThis?id=' + blogId)
.then(res => {
const blog = res.data.data
_this.blog.id = blog.id
_this.blog.title = blog.title
let MarkDownIt = require("markdown-it")
let md = new MarkDownIt()
let result = md.render(blog.content)
_this.blog.content = result
_this.ownBlog = (blog.userId === _this.$store.getters.getUser.id)
})
}
}
</script>
<style scoped>
.mblog {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
width: 100%;
min-height: 700px;
padding: 20px 15px;
}
</style>
BlogEdit.vue(博客编辑页)
<template>
<div>
<Header></Header>
<div class="m-content">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="标题" prop="title">
<el-input v-model="ruleForm.title"></el-input>
</el-form-item>
<el-form-item label="摘要" prop="description">
<el-input type="textarea" v-model="ruleForm.description"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</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>
</div>
</div>
</template>
<script>
import Header from "../../components/TopHeader";
export default {
name: "BlogEdit.vue",
components: {Header},
data() {
return {
ruleForm: {
id: '',
title: '',
description: '',
content: ''
},
rules: {
title: [
{required: true, message: '请输入标题', trigger: 'blur'},
{min: 3, max: 25, message: '长度在 3 到 25 个字符', trigger: 'blur'}
],
description: [
{required: true, message: '请输入摘要', trigger: 'blur'}
],
content: [
{required: true, message: '请输入内容', trigger: 'blur'}
]
},
operation: "add"
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios
.post('/blog/' + this.operation, this.ruleForm)
.then(res => {
console.log(res)
_this.$alert('操作成功', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
created() {
const blogId = this.$route.params.blogId
const _this = this
if (!blogId) {
this.operation = "add"
} else {
this.operation = "edit";
this.$axios.get('/blog/getThis?id=' + blogId).then(res => {
const blog = res.data.data
_this.ruleForm.id = blog.id
_this.ruleForm.title = blog.title
_this.ruleForm.description = blog.description
_this.ruleForm.content = blog.content
});
}
}
}
</script>
<style scoped>
.m-content {
text-align: center;
}
</style>
公共部分
axio.js(请求拦截、响应拦截)
import axios from 'axios'
import Element from 'element-ui'
import router from './router'
import store from './store'
import Auth from "@/common/Auth";
// axios.defaults.baseURL = "http://localhost:8081"
// 请求拦截
axios.interceptors.request.use(config => {
// 统一设置请求参数
if (store.state.token) {
config.headers[Auth.HEADER_NAME] = store.state.token
}
return config;
})
// 响应拦截
axios.interceptors.response.use(response => {
let res = response.data;
if (res.code === 1000) {
return response
} else {
Element.Message.error('失败:' + response.data.msg);
return response;
}
},
error => {
console.log(error)
Element.Message.error('失败:' + error.response.data.message);
if (error.response.data) {
error.message = error.response.data.msg
}
if (error.response.status === 401) {
store.commit("REMOVE_INFO")
router.push("/login")
}
Element.Message.error(error.message)
return Promise.reject(error)
}
)
**permission.js(控制权限(是否传token参数)) **
import router from "./router";
// 路由判断登录 根据路由配置文件的参数
router.beforeEach((to, from, next) => {
if (to.matched.some(record => (!record.meta.notRequireAuth))) { // 判断该路由是否需要登录权限
const token = localStorage.getItem("token")
console.log("token:" + token)
if (token) { // 判断当前的token是否存在; 登录存入的token
if (to.path === '/login') {
} else {
next()
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
组件部分
LeftAside.vue(左侧边栏)
<template>
<div class="left-aside-container">
<slot></slot>
</div>
</template>
<script>
export default {
name: "LeftAside"
}
</script>
<style scoped>
.left-aside-container {
width: 300px;
margin: 0 10px 0 5px;
}
</style>
TopHeader.vue(顶栏)
<template>
<div class="header-container">
<div class="left">
<span class="website-name" @click="toHome()">
蚂蚁博客
</span>
</div>
<div class="middle">
<span class="search">
搜索功能,敬请期待
</span>
</div>
<div class="right">
<span v-show="hasLogin"
@click="toUserHome(user.userName)">
<el-avatar class="avatar"
:size="35"
:src="user.avatarUrl">
</el-avatar>
</span>
<span v-show="hasLogin">
<router-link to="/blog/add">写博客</router-link>
<el-divider direction="vertical"></el-divider>
</span>
<router-link v-show="!hasLogin" to="/login">登录/注册</router-link>
<span v-show="hasLogin">
<el-link type="danger" @click="logout">退出</el-link>
</span>
</div>
</div>
</template>
<script>
import BlogEdit from "@/views/blog/BlogEdit";
export default {
name: "Header",
components: {BlogEdit},
data() {
return {
user: {
userName: '请先登录',
avatarUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
},
hasLogin: false
}
},
methods: {
logout() {
const _this = this
_this.$axios
.post("/logout")
.then(res => {
// _this.hasLogin = false
_this.$store.commit("REMOVE_INFO")
_this.$router.push({name: "Home"})
})
},
toHome() {
this.$router.push({name: "Home"});
},
toUserHome(userName) {
this.$router.push({name: "UserHome", params: {userName: userName}})
}
},
created() {
if (this.$store.getters.getUser !== null
&& this.$store.getters.getUser.userName !== null) {
this.user.userName = this.$store.getters.getUser.userName
this.user.avatarUrl = this.$store.getters.getUser.avatarUrl
this.hasLogin = this.$store.getters.getHasLogin
}
}
}
</script>
<style scoped>
.header-container {
display: flex;
height: 48px;
margin: 0 0 20px 0;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .2);
background-color: white;
}
.website-name {
cursor: pointer;
}
.left {
/*width: 400px;*/
margin: 5px 10px;
font-size: 24px;
font-weight: bolder;
}
.middle {
flex: 1;
display: flex;
}
.search {
margin: 5px auto;
}
.right {
margin: 5px 10px;
width: 250px;
}
.avatar {
margin: 4px 20px 0 0;
cursor: pointer;
}
</style>
插件部分
router/index.js(路由插件)
作用
vue-router插件,我们在浏览器上输入url时,它可以定位到前端代码里指定的页面。
代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import BlogDetail from '../views/blog/BlogDetail.vue'
import BlogEdit from '../views/blog/BlogEdit.vue'
import Home from "@/views/Home";
Vue.use(VueRouter)
const routes = [
/* {
path: '/',
name: 'Index',
redirect: { name: 'Home' }
}, */
{
path: '/',
name: 'Home',
component: Home
}, {
path: '/login',
name: 'Login',
component: Login,
meta: {
notRequireAuth: true
}
},
{
path: '/:userName',
name: 'UserHome',
// 懒加载
component: () => import('../views/user/UserHome'),
meta: {
notRequireAuth: true
}
},
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
component: BlogEdit,
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail,
meta: {
notRequireAuth: true
}
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
component: BlogEdit
}
];
const router = new VueRouter({
// mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
store/index.js(vuex插件)
说明
** **vuex可以用来跨组件共享数据。详见:Vue--Vuex--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
代码
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: localStorage.getItem("token"),
userInfo: JSON.parse(localStorage.getItem("userInfo")),
// userInfo: JSON.parse(sessionStorage.getItem("userInfo")),
},
mutations: {
// set
SET_TOKEN: (state, token) => {
state.token = token
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo
localStorage.setItem("userInfo", JSON.stringify(userInfo))
// sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
},
REMOVE_INFO: (state) => {
state.token = ''
state.userInfo = {}
localStorage.setItem("token", '')
localStorage.setItem("userInfo", JSON.stringify(''))
// sessionStorage.setItem("userInfo", JSON.stringify(''))
}
},
getters: {
// get
getUser: state => {
console.log("getUser:" + JSON.stringify(state));
return state.userInfo
},
getHasLogin: state => {
return state.token !== ''
}
},
actions: {},
modules: {}
})
版权归原作者 IT利刃出鞘 所有, 如有侵权,请联系我们删除。