二、首页
首页布局
-
配置基本样式,在
assets文件夹中创建css文件夹 ->index.stylstylus语法 参照 地址;- 在
main.js中引入import "./assets/css/index.styl"; - 在编辑器扩展中添加
stylus插件
-
删除
app.vue中的样式 -
更改布局为上中下
- header
- 头部
- 导航
- main
- 每个页面的内容
- 使用 router-view 组件 显示同的页面
- footer
- 页面结尾部分
- header
-
创建组件 header、footer
- 注意 在
elmentUI中给header,footer设置了padding,将padding重新设置为 0
- 注意 在
header 部分
header 分为 2 部分-> 头部和导航
- 头部布局及样式代码
- 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>
- 导航菜单部分->首页,菜谱分类
- 使用的是
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 });
}
页面做数据展示
-
在声明周期函数 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; }); }, -
数据展示-> 瀑布流的方式渲染页面 -> 创建组件 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>
-
实现滚动加载
- 什么时候现在加载下一页?
- 还有数据的时候要加载下一页
- 滚动到底部(滚动条滚动到 加载更多的这个位置时),才会去加载内容
- 比较 加载更多元素的底部距视窗顶部的距离 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
- 安装
npm install throttle-debounce --save
-
方法
throttle(delay,callback,{options})delay以毫秒为单位的延迟callback延迟后执行的函数- options 可选 (简单逻辑用不到)
- noLeading
- noTrailing true 会调用最后一次节流函数,默认是 false
- debounceMode 值为真, 在 delay 之后执行 clear;值为假,在 delay 之后执行 callback
-
debounce(delay,atBegin,callback)delay以毫秒为单位的延迟atBegin默认是 false。会在最后一次事件再去调用函数。如果是 true 在第一次去调用callback延迟后执行的函数
-
滚动事件使用节流
mounted() {
// 使用节流,对于高频事件一段时间内只触发一次,
// scrollHandle 自定义的方法
this.scrollHandle = throttle(1000, this.scroll.bind(this));
window.addEventListener("scroll", this.scrollHandle);
},
精品内容数据获取:
-
滚动到底部获取数据,将获取到的下一页 10 条数据,也放到
home页面中的menus中,涉及子组件给父组件传值,使用$emit触发自定义事件,home组件中监听事件,再去向后台请求数据- 数据 ->
homeView.vue中 - 滚动事件 ->
homeView.vue的子组件waterFall.vue中 - 要做的事是改变
homeView.vue中数据 - 子组件给父组件传值 使用自定义事件
$emit()
// waterFall.vue scroll() { ..... // 自定义事件 加载更多 this.$emit("loading-more"); } - 数据 ->
-
请求数据是在
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>
添加链接
- 配置路由
- 商品详情
- 个人主页
- 菜谱大全
- 登录、注册
- 添加链接