// vue.config.js
// webpack开发者自己写的配置,vue-cli会把开发者写的配置合并到内置的webpack配置中。
module.exports = {
publicPath: '/',
devServer: {
proxy: {
//代理 拦截/api,需要把目标转为target地址上,允许跨域
'/api': {
// http://localhost:8081/api/banner
target: 'http://127.0.0.1:7001', //如 将http://localhost:8081/api/banner 前边api设置为空 ===> http://127.0.0.1:7001/banner
changeOrigin: true, // 允许跨域
pathRewrite: {
//重写,把api重写为空
'^/api': ''
}
}
}
}
}
src/components/header.vue
<template>
<el-header style="height: auto;">
<div class="header">
<div class="header_c">
<el-row type="flex" justify="start" align="middle">
<el-col :span="6">
<a href="" class="logo">
</a>
</el-col>
<el-col :span="10" :offset="2"></el-col>
<el-col :span="6" :offset="3" class="avatar-box" v-show="isLogin">
<router-link :to="{name: 'space'}">
<el-avatar style="vertical-align: middle;" shape="square" size="medium" :src="userInfo.avatar"></el-avatar>
</router-link>
<router-link :to="{name: 'space'}" class="user-name">{
{
userInfo.name}}</router-link>
<router-link :to="{name: 'create'}" class="collection">发布菜谱</router-link>
<a href="javascript:;" class="collection" @click="loginOut">退出</a>
</el-col>
<el-col :span="6" :offset="3" class="avatar-box" v-show="!isLogin">
<router-link :to="{name: 'login'}" class="user-name">登录</router-link>
<router-link :to="{name: 'login'}" class="collection">注册</router-link>
</el-col>
</el-row>
</div>
</div>
<div class="nav-box">
<div class="nav_c">
<Menus></Menus>
</div>
</div>
</el-header>
</template>
<script>
import Menus from '@/components/menus'
import {
login_out} from '@/service/api'
export default {
name: 'headers',
components: {
Menus},
computed:{
isLogin(){
return this.$store.getters.isLogin;
},
userInfo(){
return this.$store.state.userInfo;
}
},
methods:{
loginOut(){
this.$confirm('真的确定要登出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const data = await login_out();
localStorage.removeItem('token');
window.location.href = '/';
}).catch(() => {
});
}
}
}
</script>
<style lang="stylus">
.header
height 129px
background-color #c90000
.logo
display: block;
height: 129px;
width: 184px;
background url(https://s1.c.meishij.net/n/images/logo2.png) -15px 9px no-repeat;
.header_c, .nav_c
width 990px
margin 0 auto
.nav-box
height 60px
background-color #fff;
box-shadow 10px 0px 10px rgba(0,0,0,0.3)
.user-name
margin-left 5px
color #fff
.collection
margin-left 5px
color #fff
</style>
src/components/menu-card.vue
<template>
<!-- row 行 -->
<el-row class="menu-card" type="flex" justify="start">
<!-- col 列 -->
<el-col
v-for="item in info"
:key="item._id"
style="flex: none"
:style="{ 'margin-left': marginLeft + 'px' }"
>
<el-card :body-style="{ padding: '0px' }">
<router-link :to="{ name: 'detail', query: { menuId: item._id } }">
<img
:src="item.product_pic_url"
class="image"
style="width: 232px; height: 232px"
/>
<div style="padding: 14px" class="menu-card-detail">
<strong>{
{
item.title }}</strong>
<span>{
{
item.comments_len }} 评论</span>
<router-link
:to="{ name: 'space', query: { userId: item.userId } }"
tag="em"
>
{
{
item.name }}
</router-link>
</div>
</router-link>
</el-card>
</el-col>
</el-row>
</template>
<script>
export default {
name: "menu-card",
props: {
marginLeft: {
type: Number,
default: 22,
},
info: {
type: Array,
default: () => [],
},
},
};
</script>
<style lang="stylus">
.menu-card {
flex-wrap: wrap;
.el-col-24 {
width: auto;
margin-bottom: 20px;
margin-left: 22px;
}
.menu-card-detail {
> * {
display: block;
}
strong {
height: 24px;
line-height: 24px;
font-size: 14px;
font-weight: bold;
color: #333;
}
span {
height: 26px;
line-height: 26px;
font-size: 12px;
color: #999;
}
em {
height: 23px;
line-height: 23px;
font-size: 12px;
color: #ff3232;
}
}
}
</style>
src/components/menus.vue
<template>
<el-menu :default-active="'1'" class="el-menu-demo" mode="horizontal" :unique-opened='true'>
<el-menu-item index="1">
<router-link class="nav-link" :to="{name: 'home'}">首页</router-link>
</el-menu-item>
<el-menu-item index="2">
<router-link class="nav-link" :to="{name: 'recipe'}">菜谱大全</router-link>
</el-menu-item>
</el-menu>
</template>
<script>
export default {
name: 'menus'
}
</script>
<style lang="stylus">
.nav-link {
display inline-block
}
</style>
src/components/upload-img.vue
<template>
<el-upload
class="avatar-uploader"
:action="action"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img :src="url" :style="{maxWidth: imgMaxWidth + 'px'}" />
</el-upload>
</template>
<script>
export default {
props:{
action: String,
maxSize: {
type: Number,
default: 2
}, // 2M
imageUrl: {
type: String,
default: ''
},
imgMaxWidth:{
// 设置的最大宽度
type: [Number, String],
default: 'auto'
}
},
data(){
return {
url: this.imageUrl
}
},
methods: {
handleAvatarSuccess(res, file) {
if(res.code === 1){
this.$message({
message: res.mes,
type: 'warning'
});
return;
}
this.url = URL.createObjectURL(file.raw);
this.$emit('res-url', {
resImgUrl: res.data.url
})
},
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/gif';
const isLt2M = file.size / 1024 / 1024 < this.maxSize;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
},
}
}
</script>
src/components/waterfall.vue
<template>
<div class="waterfall" ref="waterfall">
<slot></slot>
<div class="waterfall-loading" ref='loading' v-show="isLoading">
<i class="el-icon-loading"></i>
</div>
</div>
</template>
<script>
// 什么时候到可视区中了
// waterfall 元素的下边距 < 可视区的高度 到达可视区
import {
throttle} from 'throttle-debounce'
export default {
name: 'Waterfall',
data(){
return {
isLoading: false
}
},
mounted(){
// 优化,每隔一段时间再去执行函数,不用频繁触发 节流函数
this.scrollHandler = throttle(300, this.scroll.bind(this));
window.addEventListener('scroll', this.scrollHandler);
},
destroyed(){
window.removeEventListener('scroll', this.scrollHandler)
},
methods:{
scroll(){
console.log(123)
if(this.isLoading) return;
if(this.$refs.waterfall.getBoundingClientRect().bottom < document.documentElement.clientHeight){
console.log('已到达可视区')
this.isLoading = true;
this.$emit('view')
}
}
}
}
</script>
<style lang="stylus">
.waterfall-loading
width 100%
height 20px
text-align center
</style>
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import {
userInfo } from '@/service/api';
import Store from '@/store'
// @ 表示src
import Home from '@/views/home/Home.vue'
// 引入组件 打包会打包在文件中,如果都用import的话,所有组件都会打包在一个文件中,导致文件很大
// 以下是“按需加载”,访问路径的时候才会加载,不访问,不加载
const Recipe = () =>
import ('@/views/recipe/recipe');
const Create = () =>
import ('@/views/create/create');
const Edit = () =>
import ('@/views/user-space/edit');
// 分组导入:组名space
// prefetch 表示预加载到缓存中,当点击时直接从缓存中拿取
const Space = () =>
import ( /* webpackChunkName: "space" */ '@/views/user-space/space');
const MenuList = () =>
import ( /* webpackChunkName: "space" */ '@/views/user-space/menu-list');
const Fans = () =>
import ( /* webpackChunkName: "space" */ '@/views/user-space/fans');
const Detail = () =>
import ('@/views/detail/detail');
const Login = () =>
import ('@/views/user-login/index');
const viewsRoute = [{
path: '/recipe',
name: 'recipe',
title: '菜谱大全',
component: Recipe
},
{
path: '/create',
name: 'create',
title: '发布菜谱',
component: Create,
meta: {
login: true
}
},
{
path: '/edit',
title: '编辑个人资料',
name: 'edit',
meta: {
login: true },
component: Edit
},
{
path: '/space', // 一级路由
title: '个人空间',
name: 'space',
component: Space,
redirect: {
name: 'works'
},
meta: {
login: true
},
children: [{
// 二级路由
path: 'works',
name: 'works',
title: '作品',
component: MenuList,
meta: {
login: true
},
},
{
path: 'fans',
name: 'fans',
title: '我的粉丝',
component: Fans,
meta: {
login: true
},
},
{
path: 'following',
name: 'following',
title: '我的关注',
component: Fans,
meta: {
login: true
},
},
{
path: 'collection',
name: 'collection',
title: '收藏',
component: MenuList,
meta: {
login: true
},
}
]
},
{
path: '/detail',
name: 'detail',
title: '菜谱细节',
component: Detail
}
]
const router = new Router({
mode: 'history', // hash(#)低版本下使用哈希模式会方便点 http://localhost:8081#home http://localhost:8081/home
routes: [{
path: '/', //path是路径,一个path路径对应一个component组件
name: 'home', //name与title不是必须的,建议写上name(给路由取名字,path路径可能会变,name不变就行),方便查找及阅读
title: '首页',
component: Home
},
{
path: '/login',
name: 'login',
title: '登录页',
component: Login,
meta: {
//meta 设置 必须是登录状态才能访问
login: true
},
},
...viewsRoute,
{
path: '*', // * 表示以上都未找到则重定向至首页home
name: 'noFound',
title: '未找到',
redirect: {
name: 'home'
}
}
]
})
router.beforeEach(async(to, from, next) => {
const token = localStorage.getItem('token');
const isLogin = !!token;
// 进入路由的时候,都要想后端发送token,验证合法不合法
// 不管路由需要不需要登录,都需要展示用户信息
// 都需要想后端发请求,拿到用户信息
const data = await userInfo();
Store.commit('chnageUserInfo', data.data);
if (to.matched.some(item => item.meta.login)) {
// 需要登录,判断登录状态
if (isLogin) {
if (data.error === 400) {
// 后端告诉你,登录没成功
next({
name: 'login' });
localStorage.removeItem('token');
return;
}
if (to.name === 'login') {
next({
name: 'home' })
} else {
next();
}
return;
}
// 没登录,进入login,直接进入
if (!isLogin && to.name === 'login') {
next();
}
// 没登录,进入的不是login,跳到login
if (!isLogin && to.name !== 'login') {
next({
name: 'login' })
}
} else {
next();
}
})
export default router;
src/service/api.js
// axios封装接口
import axios from 'axios';
// 封装一层进行拦截
class HttpRequest {
constructor(options) {
this.defaults = {
baseUrl: ''
}
this.defaults = Object.assign(this.defaults, options);
};
setConfig() {
};
// 1.拦截请求:目的(在发送数据的时候添加一些数据,或者是在请求头添加一些数据,如token)
// 2.拦截响应
interceptors(install) {
// 拦截请求,给请求的数据或者头信息添加一些数据
install.interceptors.request.use(
config => {
let token = localStorage.getItem('token');
if (token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.authorization = `token ${
token}`; //authorization与后端协商的字段
}
return config;
},
err => {
return Promise.reject(err);
}
);
// 拦截响应,给响应的数据添加一些数据
install.interceptors.response.use(