仿美食杰项目第二篇

210 阅读3分钟

二、首页

首页布局

  1. 配置基本样式,在 assets 文件夹中创建 css 文件夹 -> index.styl

    • stylus 语法 参照 地址;
    • main.js 中引入 import "./assets/css/index.styl";
    • 在编辑器扩展中添加 stylus 插件
  2. 删除 app.vue 中的样式

  3. 更改布局为上中下

    • header
      • 头部
      • 导航
    • main
      • 每个页面的内容
      • 使用 router-view 组件 显示同的页面
    • footer
      • 页面结尾部分
  4. 创建组件 header、footer

    • 注意 在elmentUI中给 header,footer 设置了padding ,将padding重新设置为 0

header 部分

header 分为 2 部分-> 头部和导航

  1. 头部布局及样式代码
    • logo -> 背景图片
    • 登录和注册按钮
<template>
  <!-- 头部 -->
  <div class="header">
    <div class="header-content">
      <!-- type="flex" 可以实现栅格  即使栅格总数超出24 也不会换行,会按照相应比例划分区域-->
      <el-row :gutter="20" type="flex">
        <!-- logo -->
        <el-col :span="5" :offset="1">
          <!-- 我是用背景图片实现 -->
          <router-link to="/" class="logo"></router-link>
        </el-col>

        <!-- 空白部分 -->
        <el-col :span="10" :offset="2"> </el-col>
        <!-- 登录后显示的内容  用户名,用户头像,发布,退出登录 后面再做-->

        <!-- 登录和注册按钮 -->
        <el-col :span="6">
          <router-link to="/login" class="text login">登录</router-link>
          <router-link to="/login" class="text register">注册</router-link>
        </el-col>
      </el-row>
    </div>
  </div>
</template>

<style lang="stylus" scoped>
.header {
  height: 80px;
  background-color: #c90000;

  .logo {
    display: block;
    height: 80px;
    width: 184px;
    background: url('../assets/images/logo.png') no-repeat center;
    background-size: 60%;
  }

  .text {
    margin-left: 10px;
    color: #fff;
    line-height: 80px;
  }
}
</style>
  1. 导航菜单部分->首页,菜谱分类
    • 使用的是 el-menu 组件
      • mode 模式->水平或垂直
      • default-active 默认激活的菜单
      • select 自定义事件
      • 导航项 -> el-menu-item
        • index -> 唯一标识 可以使用 index 做选择
<template>
  <nav>
    <div class="nav-content">
      <!-- 导航 -->
      <!-- mode 导航方向  default-active 默认激活的导航 select自定义事件-->
      <el-menu mode="horizontal" default-active="home">
        <!-- 菜单项、导航项 -->
        <el-menu-item index="home">
          <router-link to="/">首页</router-link>
        </el-menu-item>
        <el-menu-item index="recipe">
          <router-link to="">菜谱大全</router-link>
        </el-menu-item>
      </el-menu>
    </div>
  </nav>
</template>
<style>
nav {
  background-color: #fff;
  /* x,y,blur,size,color */
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.4);
}

.nav-content {
  width: 1200px;
  margin: 0 auto;
}
</style>

home 页面

1. 轮播图 -> 使用 el-carousel 组件

<!-- 轮播图 -->
<el-carousel
  :interval="4000"    // 自动切换时间
  type="card"     // 显示类型
  height="300px"
  :autoplay="true"  // 自动播放
  :loop="true"     // 循环播放
>
  <!-- 要显示图片 遍历图片 banners  id 是图片的唯一标识 -->
  <el-carousel-item v-for="item in banners" :key="item.id">
    <!-- 图片的地址 product_pic_url -->
    <img :src="item.product_pic_url" alt="" />
  </el-carousel-item>
</el-carousel>

2. 获取 banners 数据

配置代理 vue.config.js

module.exports = defineConfig({
  devServer: {
    proxy: {
      // http://localhost:8080/api/banner 接口 直接对接下面接口
      "/api": {
        target: "http://39.102.89.187:7001", // http://39.102.89.187:7001/banner
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },
});

配置 axios

// src/utils/http.js
import axios from "axios";
const http = axios.create({
  baseURL: "/api",
});
// 响应拦截器
http.interceptors.response.use(
  (res) => {
    // 成功
    const { data, status } = res;
    if (status === 200) {
      return data;
    }
  },
  (err) => {
    // 失败 返回错误信息
    return Promise.reject(err);
  }
);
export default http;

编写获取数据接口,一定要返回数据

//src/apis/banner.js
import http from "@/utils/http";
export default async () => {
  // 最终请求 http://39.102.89.187:7001/banner
  return await http.get("/banner");
};

在 homeView.vue 页面中获取数据,在页面挂载完成(或者创建完成)的钩子函数中,获取数据

import getBanner from "@/apis/banner.js";
export default {
  data() {
    return {
      banners: [], // 广告条的数据
    };
  },
  // 页面挂载前
  mounted() {
    // 获取广告数据
    getBanner().then((res) => {
      this.banners = res.data.list;
    });
  },
};

3. 精选内容展示

1.获取数据 -> 编写 api(axios,和 proxy 不需要再次配置)

// src/apis/ menu.js
import http from "@/utils/http";

/**
 * 获取用户发布菜谱,可以做筛选 对应接口文档中的 10
 * @param {object} [params] -- 可选,不填写时,获取所有菜谱
 * @param {string} [params.userId] - 指定用户菜谱
 * @param {string} [params.classify] - 按照菜谱分类进行筛选
 * @param {string} [params.property] - 按照菜谱属性进行筛选
 * @param {string} [params.property.craft] - 按照工艺筛选
 * @param {string} [params.property.flavor] - 按照口味筛选
 * @param {string} [params.property.hard] - 按照难度筛选
 * @param {string} [params.property.people] - 按照人数筛选
 * @param {string} [params.page] - 指定页码
 * @returns
 */
export async function getMenus(params) {
  // 返回数据
  return await http.get("/menu/query", { params });
}

页面做数据展示

  1. 在声明周期函数 mounted(挂载完成) 请求数据

      // 页面挂载前
      mounted() {
        // 获取广告数据
        getBanner().then((res) => {
          console.log(res);
          // 获取数据并赋值
          this.banners = res.data.list;
        });
    
        // 获取菜单
        getMenus({ page: this.page }).then((res) => {
          // res.data.list 值赋值给 menus
          this.menus = res.data.list;
        });
      },
    
  2. 数据展示-> 瀑布流的方式渲染页面 -> 创建组件 WaterFall.vue

<template>
  <div class="waterfall" ref="waterfall">
    <el-row :gutter="20" type="flex" justify="start" class="menu-card">
      <!-- mongodb 中会自动创建 _id 字段,这个字段是数据的唯一标识 -->
      <el-col v-for="item in info" :key="item._id">
        <el-card :body-style="{ padding: '0px' }">
          <!-- 使用超链接 <router-link>-->
          <router-link to="菜品详情">
            <img :src="item.product_pic_url" class="image" />
            <div style="padding: 14px" class="menu-card-detail">
              <h3>{{ item.title }}</h3>
              <div class="comment-len">{{ item.comments_len }} 评论</div>

              <!-- 在a标签中通常不包裹a标签, 使用其他标签做替换 -->
              <router-link to="个人主页" tag="span">
                <!-- 在前端显示是 span 标签,超链接仍然生效 -->
                <!-- 跳转到个人主页 -->
                <div class="author">{{ item.name }}</div>
              </router-link>
            </div>
          </router-link>
        </el-card>
      </el-col>
    </el-row>

    <!-- 显示加载更多 -->
    <div class="waterfall-loading">
      <i class="el-icon-loading"></i> 等待加载
    </div>
  </div>
</template>

<style scoped lang="stylus">
.waterfall-loading {
  margin-top: 10px;
  text-align: center;
  font-size: 14px;
  color: #888;
}

.menu-card {
  flex-wrap: wrap; // el-row 超出的内容换行

  .el-col-24 {
    /* el-col-24 本身有 10xp 的内填充(行内样式) 需要去掉 */
    padding: 0 !important;
    margin: 10px 4px;
    width: 220px;
    background-color: #fff;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
    border-radius: 5px;

    .menu-card-detail {
      h3 {
        font-size: 18px;
        line-height: 1.5em;
      }

      /* 评论条数 */
      .comment-len {
        color: #888;
        font-size: 12px;
        line-height: 2em;
      }

      /* 作者 */
      .author {
        font-size: 12px;
        line-height: 2em; // em 相对于本身标签文字大小
        color: #ff4450;
      }
    }
  }
}
</style>
  1. 实现滚动加载

    • 什么时候现在加载下一页?
      • 还有数据的时候要加载下一页
      • 滚动到底部(滚动条滚动到 加载更多的这个位置时),才会去加载内容
        • 比较 加载更多元素的底部距视窗顶部的距离 s1 与 视窗的高度 h1 的大小
        • s1 > h1 不显示 加载更多 否则 s1 < h1 时,说明滚动条已经到加载更多这个位置 显示加载更多
        • dom.getBoundingClientRect() 方法用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
        • 获取视窗的高度 document.documentElement.clientHeight
    • 滚动事件
      • 滚动时获取上面两个值,去比较
      • 然后向后台请求数据
    export default {
      props: {
        // 数据
        info: {
          type: Array, // 规定数据类型是数组
          default: () => [], // 引用数据类型,default是一个函数 ,返回值是数组
        },
      },
      data() {
        return {
          isLoading: false, // 1. 保存加载状态,默认不加载
        };
      },
      // 2. 页面挂载完成,去注册事件
      mounted() {
        // 事件监听器,监听滚动事件, fn函数 在methods 中
        window.addEventListener("scroll", this.scroll);
      },
      methods: {
        // 3. 滚动时执行的程序
        scroll() {
          // 如果当前 已经是加载中,直接返回,不做其他操作,等待数据加载完成
          if (this.isLoading) return;
    
          // 否则,往下走
          // 判断 列表底部 距离窗口顶部的距离 与  视窗高度比较
          const s = this.$refs.waterfall.getBoundingClientRect().bottom;
          const h = document.documentElement.clientHeight;
          if (s < h) {
            console.log(1111);
            // 显示加载更多
            this.isLoading = true;
          }
        },
      },
    };
    

防抖( debounce )和节流( throttle) 获取数据 ,一定时间内只允许发送一个请求

防抖 debounce

触发事件后在 n 秒内只执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

节流 throttle

在 n 秒内连续触发事件,但是在 n 秒只执行一次函数(第一次触发的函数)

使用模块 throttle-debounce

  1. 安装

npm install throttle-debounce --save

  1. 方法 throttle(delay,callback,{options})

    • delay 以毫秒为单位的延迟
    • callback 延迟后执行的函数
    • options 可选 (简单逻辑用不到)
      • noLeading
      • noTrailing true 会调用最后一次节流函数,默认是 false
      • debounceMode 值为真, 在 delay 之后执行 clear;值为假,在 delay 之后执行 callback
  2. debounce(delay,atBegin,callback)

    • delay 以毫秒为单位的延迟
    • atBegin 默认是 false。会在最后一次事件再去调用函数。如果是 true 在第一次去调用
    • callback 延迟后执行的函数
  3. 滚动事件使用节流

mounted() {
    // 使用节流,对于高频事件一段时间内只触发一次,
    // scrollHandle 自定义的方法
    this.scrollHandle = throttle(1000, this.scroll.bind(this));
    window.addEventListener("scroll", this.scrollHandle);
  },

精品内容数据获取:

  1. 滚动到底部获取数据,将获取到的下一页 10 条数据,也放到 home 页面中的 menus 中,涉及子组件给父组件传值,使用 $emit 触发自定义事件,home 组件中监听事件,再去向后台请求数据

    • 数据 -> homeView.vue
    • 滚动事件 -> homeView.vue 的子组件 waterFall.vue
    • 要做的事是改变 homeView.vue 中数据
    • 子组件给父组件传值 使用自定义事件 $emit()
    // waterFall.vue
    scroll() {
      .....
    // 自定义事件 加载更多
    this.$emit("loading-more");
    }
    
  2. 请求数据是在 home 组件中实现, 获取到数据之后将加载中的图标隐藏

<template>
  // 绑定自定义事件,, 绑定组件
  <water-fall
    :info="menus"
    @loading-more="loadingMoreHandle"
    ref="waterfall"
  ></water-fall>
</template>

<script>
export default {
  methods: {
    loadingMoreHandle() {
      console.log("在homeView中监听到加载更多。。。。");
      // 1. 判断当前页是否是最后一页 , 总页数?
      if (this.page >= this.pages) {
        // 已经是最后一页  // 隐藏加载更多图标
        this.$refs.waterfall.isLoading = false;
        return; // 其他事情不做直接退出程序
      }
      this.page++; // 获取下一页内容,将页码加1
      // 加载内容,传下一页的页码
      getMenus({ page: this.page }).then((res) => {
        // 获取list
        const { list } = res.data;
        // 需要将list数组中的数据添加到 menus 中
        this.menus.push(...list);
        // 隐藏加载更多
        this.$refs.waterfall.isLoading = false;
      });
    },
  },
  // 初次加载时,获取页码和总页数
  mounted() {
    // 获取广告数据
    getBanner().then((res) => {
      // 获取数据并赋值
      this.banners = res.data.list;
    });

    // 获取菜单
    getMenus({ page: this.page }).then((res) => {
      console.log(res);
      // res.data.list 值赋值给 menus
      this.menus = res.data.list;
      // 设定总页数
      const pages = Math.ceil(res.data.total / res.data.page_size);
      this.pages = pages;
    });
  },
};
</script>

添加链接

  1. 配置路由
    • 商品详情
    • 个人主页
    • 菜谱大全
    • 登录、注册
  2. 添加链接