vue美食节项目

这是一个基于Vue.js的美食节项目,包含多个组件如header、menu-card、menus、upload-img和waterfall,以及路由配置、API服务、状态管理等。项目涵盖了创建、查看、评论、用户登录注册和空间管理等多个功能页面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

// 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(
            
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值