duck blog 博客项目

  • 第一阶段----雏形
  • 1. vue前端
    • 1.1 全局文件说明
    • 1.2 登录功能
    • 1.3 注册功能
    • 1.4 博客列表
    • 1.5 博客编辑
    • 1.6 博客详情
    • 1.7 路由守卫
  • 2. springboot后端
  • 待完善内容
  • 第二阶段 优化完善+新功能

技术栈

  • SpringBoot
  • mybatis plus
  • shiro
  • lombok
  • redis
  • hibernate validatior
  • jwt

第一阶段----雏形

1. vue前端

1.1 全局文件说明


所有页面注册路由的文件,router下index

import Vue from 'vue'
import Router from 'vue-router'
import Login from "../view/Login";
import Blogs from "../view/Blogs";
import Regist from "../view/Regist";
import BlogEdit from "../view/BlogEdit";
import BlogDetail from "../view/BlogDetail";
import Community from "../view/Community";Vue.use(Router)export default new Router({routes: [// 重定向{path: "/",redirect:'/login'},{path:"/blog_community",name:"BlogCommunity",component:Community},{path: "/login",name: "Login",component: Login},{path: "/regist",name: "Regist",component: Regist},{path:"/add",name:"Edit",meta:{requireAuth: true},component:BlogEdit},{path:"/blogs",name:"Home",meta:{requireAuth: true},component:Blogs},{path:"/blog_detail/:blogId",name:"BlogDetail",meta:{requireAuth: true},component:BlogDetail}]
})

mainjs全局文件引入插件,自己写的js等

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI, {MessageBox} from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
import less from 'less'
// 添加全局样式
import './assets/css/globle.css';
// 引入iconfront
import './assets/font/iconfont.css'
// 引入vuex-store
import store from './store/index';
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
//引入路由守卫
import './permission'Vue.use(mavonEditor)
Vue.prototype.$confirm=MessageBox.confirm
Vue.use(less)
Vue.prototype.$axios = axios;
Vue.use(ElementUI);Vue.config.productionTip = false/* eslint-disable no-new */
new Vue({el: '#app',router,store,components: { App },template: '<App/>'
})

App.vue路由占位符,vue是单页面,router-view也就是每次加载页面替换的页面部分

<template><div id="app"><!--路由占位符--><router-view></router-view></div>
</template><style>#app{}
</style><script>export default {};
</script>

store下index.js

啥是store?
Vuex就是提供一个仓库,Store仓库里面放了很多对象。其中state就是数据源存放地,对应于与一般Vue对象里面的data(后面讲到的actions和mutations对应于methods)。
在使用Vuex的时候通常会创建Store实例new Vuex.store({state,getters,mutations,actions})有很多子模块的时候还会使用到modules。

  • vuex优势:相比sessionStorage,存储数据更安全,sessionStorage可以在控制台被看到。

  • vuex劣势:在F5刷新页面后,vuex会重新更新state,所以,存储的数据会丢失。(后面因为这个踩坑)

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({//相当于私有属性state: {token: '',userInfo: ''},//相当于set方法mutations: {SET_TOKEN: (state, token) => {state.token = tokenlocalStorage.setItem("token", token)},SET_USERINFO: (state, userInfo) => {state.userInfo = userInfosessionStorage.setItem("userInfo", JSON.stringify(userInfo))},REMOVE_INFO: (state) => {localStorage.setItem("token", '')sessionStorage.setItem("userInfo", JSON.stringify(''))state.userInfo = {}}},// 相当于getgetters: {getUser: state => {return state.userInfo}},actions: {},modules: {}
})

公共组件
因为后面页面都需要一个头部信息,所以抽取出来做成组件,只要引用就可以了。

<template><div class="m-content"><h3>欢迎来到{{user.username}}的博客</h3><div class="block"><el-avatar :size="50" :src="require('.././assets/css/img/user.jpg')" fit="cover"></el-avatar><div>{{ user.username }}</div></div><div><el-divider direction="vertical"></el-divider><el-link class="el-icon-edit" @click="toAdd" type="primary">发表</el-link><el-divider direction="vertical"></el-divider><el-link class="el-icon-delete" type="danger" @click="toBlogs">主页</el-link><el-divider direction="vertical"></el-divider><el-link class="el-icon-switch-button" type="warning" v-show="hasLogin" @click="logout">退出</el-link><el-link class="el-icon-switch-button" type="warning" v-show="!hasLogin">请登录</el-link><el-divider direction="vertical"></el-divider></div></div>
</template>
<script>export default {name: "Header",data() {return {hasLogin: false,user: {username: '',avatar: "../assets/css/img/user.jpg"},blogs: {},currentPage: 1,total: 0}},methods: {logout() {const _this = this_this.$store.commit('REMOVE_INFO')_this.$router.push('/login')},toAdd() {this.$router.push({path:'/add',})},toBlogs(){this.$router.push({path:'/blogs',})}},// 从store中取出用户信息存到data中定义的变量user中created() {if(this.$store.getters.getUser.username) {this.user.username = this.$store.getters.getUser.usernamethis.user.img = "../assets/css/img/user.jpg"this.hasLogin = true}}}
</script><style scoped>.m-content{text-align: center;}
</style>

引入方式
<template>中引入,注意别和小写header弄混
然后在<script>
import Header from "../components/Header";

1.2 登录功能

login页面


<template><div><el-container><el-main>
<!--      绑定script中data数据model="submitForm"--><el-form :model="submitForm" :rules="rules" ref="ruleForm" class="LoginForm"><el-form-item label="用户名" prop="username"><!--  v-model  绑定data中数据"--><el-input v-model="submitForm.username" prefix-icon="iconfont icon-user"></el-input></el-form-item><el-form-item label="密码" prop="password"><el-input type="password" v-model="submitForm.password" prefix-icon="iconfont icon-unlock"></el-input></el-form-item><el-row><el-button type="primary" @click="login" >登录/注册</el-button></el-row></el-form></el-main></el-container></div>
</template><script>export default {data() {return {userToken:"",// 登录表单的数据绑定对象submitForm: {username: '',password: ''},rules: {username: [{ required: true, message: '请输入用户名', trigger: 'blur' },{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' },{required: true,pattern: /^[\u4e00-\u9fa5_a-zA-Z0-9.·-]+$/,message: '姓名不支持特殊字符',trigger: 'blur'}],password: [{ required: true, message: '请输入密码', trigger: 'change' },{pattern: /([a-zA-Z0-9][!@#$%^&()*])|([!@#$%^&()*][a-zA-Z0-9])+/,message: '密码必须由字母、数字、特殊符号组成,区分大小写',trigger: 'blur'}]}};},methods: {login() {// 发起请求后this会丢失,先保存var that=thisthis.$refs.ruleForm.validate((valid) => {if (valid) {// 向后端发起请求,提交数据this.$axios.post('http://localhost:9090/login',this.submitForm).then(function (response){//查询到用户跳转个人中心界面if (response.data.data!==""&&response.data.data!=null){alert("登陆成功!")const token=response.headers["authorization"]console.log(token)//先写死token,因为总是获取不到tokenthat.$store.commit('SET_TOKEN',token)that.$store.commit('SET_USERINFO',response.data.data)console.log(that.$store.getters.getUser.userId)console.log(that.$store.getters.getUser.username)that.$router.push({path:'/blogs'})}//查询不到弹窗提示注册else{const confirmRes=that.$confirm("用户名或密码错误!是否注册?",{confirmButtonText:'注册',type:'info'}).then((action)=>{if(action === 'confirm'){that.$router.push({path:'/regist'})}})}})} else {console.log('error submit!!');return false;}});}}}
</script><style scoped lang="less">.el-form{background-color: gainsboro;/*表单边框*/border: 1px solid #DCDFE6;width: 350px;height: 300px;margin: 200px auto;/*设置各边上内边距的宽度*/padding: 35px 35px 15px 35px;/*设置圆角边框*/border-radius: 5px;/*处理圆角效果的*/-webkit-border-radius: 5px;-moz-border-radius: 5px;/*添加阴影*/box-shadow: 0 0 25px SlateGray;/*设置表单透明度*/filter:alpha(Opacity=90);-moz-opacity:0.9;opacity: 0.9;}.el-row{margin-top: 50px;/*文本对齐方式*/text-align: center;}.el-button{text-align: center;width:300px ;margin-top: 0px;}
</style>

使用了element ui的表单和弹窗,以及阿里巴巴矢量图,记得加入购物车再下载,把下载的文件复制到assert,具体百度。

踩坑: header的token怎么都拿不到,按网上的后台也打开了暴露设置,后来发现axios 会把 header 转为小写,比如 Content-Type -> content-type,Authorization 会被转为 authorization……

1.3 注册功能

regist页面

<template><div><el-container><el-form :model="registForm" :rules="rules" ref="registForm" class="demo-registForm"><el-form-item label="用户名" prop="username" required><el-input v-model="registForm.username" prefix-icon="iconfont icon-user"></el-input></el-form-item><el-form-item label="密码" prop="password" required><el-input type="password" v-model="registForm.password" autocomplete="off" prefix-icon="iconfont icon-unlock"></el-input></el-form-item><el-form-item label="确认密码" prop="checkPass" required><el-input type="password" v-model="registForm.checkPass" autocomplete="off" prefix-icon="iconfont icon-unlock"></el-input></el-form-item><el-form-item label="生日" required><el-col :span="11"><el-form-item prop="date1"><el-date-picker type="date" placeholder="选择日期" v-model="registForm.date1" style="width: 100%;"></el-date-picker></el-form-item></el-col></el-form-item><el-form-item><el-row><el-button class="regist_button" type="primary" @click="submitForm('registForm')">立即注册</el-button><el-button class="reset_button" @click="resetForm('registForm')">重置</el-button></el-row></el-form-item></el-form></el-container></div>
</template><script>export default {data() {var test_Pass = (rule, value, callback) => {if (value !== this.registForm.password) {callback(new Error("两次输入的密码不一致!"));} else {callback();}};var test_username = (rule, value, callback) => {this.$axios.post('http://localhost:9090/query_name',{username:this.registForm.username}).then(function (response) {if (response.data.data==false){callback(new Error(response.data.msg))}callback();})};return {registForm: {username: '',password: '',birthday: ''},rules: {username: [// 有多个那么失去焦点时按顺序执行{ required: true, message: '请输入用户名', trigger: 'blur' },{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' },{required: true,pattern: /^[\u4e00-\u9fa5_a-zA-Z0-9.·-]+$/,message: '姓名不支持特殊字符',trigger: 'blur'},{// 有message的话上面的error不会显示required: true,validator:test_username,trigger: 'blur'}],password: [{ required: true, message: '请输入密码', trigger: 'blur' },{pattern: /([a-zA-Z0-9][!@#$%^&()*])|([!@#$%^&()*][a-zA-Z0-9])+/,message: '密码必须由字母、数字、特殊符号组成,区分大小写',trigger: 'blur'}],checkPass:[{required: true,message:'请确认密码!',trigger: 'blur'},{required: true,validator: test_Pass,trigger:'blur'}],date1: [{ type: 'date', required: true, message: '请选择日期', trigger: 'change' }]}};},methods: {submitForm(registForm) {this.$refs[registForm].validate((valid) => {if (valid) {var _this=thisthis.$axios.post('http://localhost:9090/regist',this.registForm).then(function (response) {if (response.data.data==true){_this.tips();_this.$router.push({path:'/login'})}})} else {console.log('error submit!!');return false;}});},resetForm(registForm) {this.$refs[registForm].resetFields();},tips() {this.$notify({title: '成功',message: '注册成功请登陆!',type: 'success'});},}}
</script><style scoped lang="less">.el-form{background-color: gainsboro;/*表单边框*/border: 1px solid #DCDFE6;width: 350px;height: 450px;margin: 150px auto;/*设置各边上内边距的宽度*/padding: 35px 35px 15px 35px;/*设置圆角边框*/border-radius: 5px;/*处理圆角效果的*/-webkit-border-radius: 5px;-moz-border-radius: 5px;/*添加阴影*/box-shadow: 0 0 25px palegreen;/*设置表单透明度*/filter:alpha(Opacity=90);-moz-opacity:0.9;opacity: 0.9;}.el-row{margin-top: 30px;}.reset_button{position: absolute;margin-left: 180px;}
</style>

1.4 博客列表

blogs

<template><div class="blogs"><Header></Header><div class="block"><el-timeline><el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs"><el-card><router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}"><h4>{{blog.title}}</h4></router-link><p>{{blog.description}}</p></el-card></el-timeline-item></el-timeline></div>
<!--    分页--><el-pagination class="mpage"@current-change="page"backgroundlayout="prev, pager, next":current-page="currentPage":page-size="pageSize":total="total">});</el-pagination></div>
</template><script>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=thisvar userInfo=JSON.parse(sessionStorage.getItem("userInfo"))console.log("id---"+userInfo.userId)this.$axios.get('http://localhost:9090/blogs?currentPage=' + currentPage ,{params:{userId:userInfo.userId}}).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"]})},},created() {this.page(1)}}
</script><style scoped>.mpage{margin: 0 auto;text-align: center;}.blogs{max-width:1000px;margin: 0 auto;}
</style>

踩坑:f5刷新后登录信息丢失,列表空白,是因为发起请求提交userId参数的时候用的是_this.$store.getters.getUser.userId,上面说过刷新的话store会重新清空,那么_this.$store.getters.getUser.userId就成了null,后台得不到userId,改成上面的userInfo=JSON.parse(sessionStorage.getItem("userInfo")),直接从sessionstorage取不通过store,但是注意store中是user整个json存的所以要先转换在取userid,params:{userId:userInfo.userId}

这里列表只显示登录用户的博客,标题是超链接,跳转url会携带博客id跳转到博客详情页面,http://localhost:8080/#/blog_detail/5

分页: 使用elementui分页组件

1.5 博客编辑

<template><div><el-container><el-header><Header></Header></el-header><el-main><el-form ref="ruleForm" :model="editForm" label-width="80px" :rules="rules"><el-form-item label="标题"><el-input v-model="editForm.title"></el-input></el-form-item><el-form-item label="摘要"><el-input v-model="editForm.description"></el-input></el-form-item><el-form-item label="标签"><el-checkbox-group v-model="editForm.tag"><el-checkbox label="Java" name="type"></el-checkbox><el-checkbox label="Vue" name="type"></el-checkbox><el-checkbox label="SpringBoot" name="type"></el-checkbox><el-checkbox label="SpringCloud" name="type"></el-checkbox></el-checkbox-group><el-button class="el-icon-plus">添加标签</el-button></el-form-item><el-form-item label="可见范围"><el-radio-group v-model="editForm.permission"><el-radio label="私密"></el-radio><el-radio label="公开"></el-radio></el-radio-group></el-form-item><el-form-item label="内容" class="content_input"><mavon-editor v-model="editForm.content"></mavon-editor></el-form-item><el-form-item><el-button type="primary" @click="SubmitFrom">立即创建</el-button><el-button>取消</el-button></el-form-item></el-form></el-main></el-container></div>
</template>
<script>import Header from "../components/Header";export default {components: {Header},data() {return {editForm: {user_id:this.$store.getters.getUser.user_id,title:'',description:'',tag:'',permission:'',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.query.blogIdconsole.log(blogId)const _this = thisif(blogId) {this.$axios.get('http://localhost:9090/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: {SubmitFrom() {this.$refs.ruleForm.validate((valid) => {if (valid) {var _this=thisconsole.log(this.$store.getters.getUser.user_id),// 向后端发起请求,提交数据this.$axios.post('http://localhost:9090/edit_blog',this.editForm,{header: {"Authorization": localStorage.getItem("token")}}).then(function (response){if (response.data.data=true)_this.tips()_this.$router.push({path:'/blogs'})})} else {console.log('error submit!!');return false;}});console.log('submit!');},tips() {this.$notify({title: '上传成功',message: '博客已成功上传!',type: 'success',duration:1000});}}}
</script>
<style>.el-main{margin-top: 110px;}
</style>

如果博客id不为空,说明从博客列表跳转的博客编辑,那么获取url上的博客id,后台发起请求,回显博客内容,否则是空白的新博客编辑,还调用了vue的markdown插件

cnpm install mavon-editor --save

1.6 博客详情

<template><div class="m-container"><Header></Header><div class="mblog"><h2>{{ blog.title }}</h2><el-link type="success" class="edit" icon="el-icon-edit" v-if="ownBlog" @click="toEdit">编辑</el-link><el-divider></el-divider><div class="content markdown-body" v-html="blog.content"></div></div></div>
</template><script>import 'github-markdown-css/github-markdown.css' // 然后添加样式markdown-bodyimport Header from "../components/Header";export default {name: "BlogDetail",components: {Header},data() {return {blog: {userId: null,title: "",description: "",content: ""},ownBlog: false}},methods: {getBlog() {// this.$route.params来获取路由中的参数const blogId = this.$route.params.blogIdconst _this = thisthis.$axios.get('http://localhost:9090/blog/' + blogId).then((res) => {console.log(res)console.log(res.data.data)_this.blog = res.data.datavar 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)});},toEdit(){const blogId = this.$route.params.blogIdconsole.log(blogId)this.$router.push({path:'/add',query:{blogId:blogId}})}},created() {this.getBlog()}}
</script><style scoped>.edit{margin-left: 1300px;}
</style>

博客详情中需要回显博客信息,然后有个问题就是,后端传过来的是博客内容是markdown格式的内容,我们需要进行渲染然后显示出来,这里我们使用一个插件markdown-it,用于解析md文档,然后导入github-markdown-c,所谓md的样式。然后就可以在需要渲染的地方使用.

 //用于解析md文档
cnpm install markdown-it --save
// md样式
cnpm install github-markdown-css

这个页面的路由是带参数的,页面有编辑,点击携带id到编辑页面可以修改微博

1.7 路由守卫

permission

import router from "./router";
// 路由判断登录 根据路由配置文件的参数
router.beforeEach((to, from, next) => {if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限,通过验证index.js里路由的meta.requireAuthconst token = localStorage.getItem("token")if (token) { // 判断当前的token是否存在 ; 登录存入的tokenif (to.path === '/login') {} else {next()}} else {next({path: '/login'})}} else {next()}
})

2. springboot后端

  • codegen是mybaitsplus的代码生成器
  • Result是统一结果封装
package com.tutougirl.duck.common;
import lombok.Data;import java.io.Serializable;@Data
public class Result implements Serializable {private String code;private String msg;private Object data;public static Result succ(Object data) {Result m = new Result();m.setCode("0");m.setData(data);m.setMsg("操作成功");return m;}public static Result succ(String mess, Object data) {Result m = new Result();m.setCode("0");m.setData(data);m.setMsg(mess);return m;}public static Result fail(String mess) {Result m = new Result();m.setCode("-1");m.setData(null);m.setMsg(mess);return m;}public static Result fail(String mess, Object data) {Result m = new Result();m.setCode("-1");m.setData(data);m.setMsg(mess);return m;}
}
  • corsconfig是解决跨域问题的配置类
package com.tutougirl.duck.Config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** Created with IntelliJ IDEA.** @Auther: 何艳莹* @Date: 2021/04/12/21:41* @Description:*///解决cros跨域问题
@Configuration
public class CrosConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("GET","POST","HEAD","PUT","DELETE","OPTIONS").allowCredentials(true).maxAge(3600).allowedHeaders("*");}
}
  • mybaitsplus里是开启mybaitsplus自带分页插件配置类
package com.tutougirl.duck.Config;import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;/*** Created with IntelliJ IDEA.** @Auther: 何艳莹* @Date: 2021/05/02/17:54* @Description:*/
@Configuration
@EnableTransactionManagement
@MapperScan("com.tutougirl.duck.Mapper")
public class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {return new PaginationInterceptor();}
}
  • tokenuntils是生产和验证token的工具类
package com.tutougirl.duck.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tutougirl.duck.Entity.User;import java.util.Date;public class TokenUtil {private static final long EXPIRE_TIME= 60*1000;private static final String TOKEN_SECRET="txdy";  //密钥盐/*** 签名生成* @param user* @return*/public static String produceToken(User user){String token = null;try {Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME);token = JWT.create().withIssuer("auth0").withClaim("username", user.getUsername()).withExpiresAt(expiresAt)// 使用了HMAC256加密算法。.sign(Algorithm.HMAC256(TOKEN_SECRET));} catch (Exception e){e.printStackTrace();}return token;}/*** 签名验证* @param token* @return*/public static boolean verify(String token){try {JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();DecodedJWT jwt = verifier.verify(token);System.out.println("认证通过:");System.out.println("username: " + jwt.getClaim("username").asString());System.out.println("过期时间:      " + jwt.getExpiresAt());return true;} catch (Exception e){return false;}}
}

踩坑:

  1. get请求,接收参数不用加@requestbody不然报错
  2. 根据登录的用户去数据库查出来的user的id一直是0,但是数据库不是0,原因是数据库中字段带下划线的在mybatisplus里对应的在java中默认是驼峰命名,er我的字段是user_id,它给我自动转换了,导致找不到这个数据库属性,可能默认返回了0.后来改了字段为userId, 在属性上加@TableField(value=“数据库字段名”) 来匹配数据库字段
 //mybaitsplus会自动加下划线,如果列名是userId报错,哪怕字段和列名对应也不行。@TableField("user_id")private int userId;


待完善内容

想增加一个公共博客页面,博客列表不再是自己的,而是所有用户的,大家都可以看见,还可以评论。
后台没有验证token,统一异常处理。考虑整合shiro,整合redis。

第二阶段 优化完善+新功能

这段时间我又成长了。并且学习方向从后端转向了前端。
2022.2.14 情人节
前几天对前端代码进行了优化,增加了API统一管理,axios二次封装,vuex存储公共数据,分割组件,增加事件委派减少组件的复用导致的多次发送请求,增加了函数防抖。并且将标签抽取成组件,实现了博客页面分类展示博客、动态增加标签。由于不再深入后端,学习了mock模拟后端数据。对header增加了个人中心。鼠标放置弹出组件,由于想要在所有页面都可以打开个人中心所以设置了z-index,然而由于使用了vue的动画,translation。导致出现bug,弹出先在最底下,延迟一会儿才会到最上层。还没有解决。

springboot+vue+elementUI搭建个人博客相关推荐

  1. SpringBoot+vue前后端分离博客项目

    SpringBoot+vue前后端分离博客项目 Java后端接口开发 1.前言 2.新建Springboot项目 3.整合mybatis plus 第一步:导入jar包 第二步:然后去写配置文件: 第 ...

  2. vue.js搭建个人博客

    博客2.0更新啦 博客2.0 ​ 博客地址:mangoya.cn ​ 整体样式基本没有变化,主要变化是前后端重构,之前的1.0版本后端为php,并非博主所开发,这次重构调整了数据结构和所熟悉的语言,采 ...

  3. 超详细!4小时开发一个SpringBoot+vue前后端分离博客项目!!

    小Hub领读: 前后端分离的博客项目终于出来啦,真是花了好多心思录制咧.文末直接进入B站看视频哈! 作者:吕一明 项目代码:https://github.com/MarkerHub/vueblog 项 ...

  4. SpringBoot技术栈搭建个人博客【项目准备】

    前言:很早之前就想要写一个自己的博客了,趁着现在学校安排的实习有很多的空档,决定把它给做出来,也顺便完成实习的任务(搞一个项目出来...) 需求分析 总体目标:设计一套自适应/简洁/美观/易于文章管理 ...

  5. 大二期末作孽(SpringBoot+Vue前后端分离博客社区(重构White Hole))

    文章目录 前言 目录 效果演示 前言 由于时间关系,完成度确实不高,而且不签只是完成了客户端,当然目前这样也是已经可以正常使用了,当然有点勉强.不过后续还是会不断的去更新维护的,不过大体的架构是这样的 ...

  6. 基于Java+SpringBoot+Vue前后端分离博客系统设计与实现

    博主介绍:✌全网粉丝3W+,全栈开发工程师,从事多年软件开发,在大厂呆过.持有软件中级.六级等证书.可提供微服务项目搭建与毕业项目实战✌ 博主作品:<微服务实战>专栏是本人的实战经验总结, ...

  7. 搭建个人博客网站 -- vue初学者学习总结

    搭建个人博客网站 – vue技术学习 开源代码:个性化个人博客系统 参考项目:风丶宇的个人博客 一.项目概述 项目主要是基于SpringBoot + Vue 开发的前后端分离博客,本文主要涉及项目前端 ...

  8. Springboot搭建个人博客系列

    前言 为什么想要搭建这个博客? 我还记得,在大二寒假的某天,同往常一样的在家解决这某个bug,不停地问度娘,很巧的碰到了一个同行在他的博客中完美的记录了我的bug的解决方案,随后我又看了看他写的其他博 ...

  9. Vue整合SpringBoot项目实战之Vue+Element-Ui搭建前端项目

    Vue整合SpringBoot项目实战之Vue+Element-Ui搭建前端项目 源码(欢迎star): 前端项目代码 后端项目代码 系列文章: Vue整合SpringBoot项目实战之后端业务处理 ...

最新文章

  1. 美媒人工智能(AI)代表了计算的优点,没有人类推理的缺点
  2. python主要运用于-Python八大主要应用领域,你都知道吗?
  3. 小程序向webview传参_独家 | 支付宝小程序向个人开发者开放公测
  4. C# 获取枚举的DescriptionAttribute内的字符串
  5. 事物与持久化_DDD之聚合持久化应该怎么做?
  6. 个人计算机多核cpu好处,CPU是多核好还是高主频好?
  7. ArcGIS三维建模(三)
  8. 高级语言c 与三菱plc通讯宝典,三菱PLCL系列串行通信模块用户手册基本篇中文高清版...
  9. Android 消息机制
  10. python中sys是什么意思_python里的sys是什么意思
  11. 基于JAVA毕业生交流学习平台计算机毕业设计源码+系统+数据库+lw文档+部署
  12. golang 环境搭建-windows
  13. 区块链网络的价值是啥?
  14. error: variable '__this_module' has initializer but incomplete type错误解决
  15. 机房重构一路走来——初步总结
  16. 【论文阅读】Lie-Algebraic Averaging For Globally Consistent Motion Estimation
  17. excel中VLOOKUP函数 匹配数据 使用方法
  18. Principle安装包
  19. 西门子MES解决方案
  20. Ubuntu20.04 交叉编译openssl 1.0.1f

热门文章

  1. 3D艺术家推荐——4款最佳3D建模软件
  2. 微信定位当前城市 php,html5,javascript_微信内置浏览器如何定位用户所在城市,html5,javascript - phpStudy...
  3. pandas中的窗口对象(窗口函数)
  4. MFC如何设置背景图片
  5. 从输出海外吃鸡游戏浅谈创新
  6. C语言键盘方向键的读入
  7. 机器人教育对孩子们的作用
  8. adf的主要功能之一是_ADF 入门第一步系列
  9. uni-app返回上一级并刷新页面
  10. 牛逼的人,都不太要面子