vue3学习日记7 - Home页面
最近发现职场前端用的框架大多为vue,所以最近也跟着黑马程序员vue3的课程进行学习,以下是我的学习记录
视频网址:
Day2-17.Layout-Pinia优化重复请求_哔哩哔哩_bilibili
学习日记:
vue3学习日记1 - 环境搭建-CSDN博客
vue3学习日记2 - 组合式API基础学习-CSDN博客
vue3学习日记3 - 组合式API练习小案例-CSDN博客
vue3学习日记4 - Pinia-CSDN博客
vue3学习日记5 - 项目起步-CSDN博客
vue3学习日记6 - Layout-CSDN博客
一、整体结构拆分和分类实现
1、创建如下文件夹及目录
新建文件的内容分别为
<template>
Banner页面
</template>
<template>
我是分类页
</template>
<template>
人气推荐
</template>
<template>
新鲜好物
</template>
<template>
产品列表
</template>
index.vue
<script setup>
import HomeCategory from './componments/HomeCategory.vue'
import HomeBanner from './componments/HomeBanner.vue'
import HomeNew from './componments/HomeNew.vue'
import HomeHot from './componments/HomeHot.vue'
import homeProduct from './componments/HomeProduct.vue'
</script>
<template>
<div class="container">
<HomeCategory />
<HomeBanner />
</div>
<HomeNew />
<HomeHot />
<homeProduct />
</template>
运行截图
2、修改HomeCategory的页面
<script setup>
</script>
<template>
<div class="home-category">
<ul class="menu">
<li v-for="item in 9" :key="item">
<RouterLink to="/">居家</RouterLink>
<RouterLink v-for="i in 2" :key="i" to="/">南北干货</RouterLink>
<!-- 弹层layer位置 -->
<div class="layer">
<h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
<ul>
<li v-for="i in 5" :key="i">
<RouterLink to="/">
<img alt="" />
<div class="info">
<p class="name ellipsis-2">
男士外套
</p>
<p class="desc ellipsis">男士外套,冬季必选</p>
<p class="price"><i>¥</i>200.00</p>
</div>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</div>
</template>
<style scoped lang='scss'>
.home-category {
width: 250px;
height: 500px;
background: rgba(0, 0, 0, 0.8);
position: relative;
z-index: 99;
.menu {
li {
padding-left: 40px;
height: 55px;
line-height: 55px;
&:hover {
background: $xtxColor;
}
a {
margin-right: 4px;
color: #fff;
&:first-child {
font-size: 16px;
}
}
.layer {
width: 990px;
height: 500px;
background: rgba(255, 255, 255, 0.8);
position: absolute;
left: 250px;
top: 0;
display: none;
padding: 0 15px;
h4 {
font-size: 20px;
font-weight: normal;
line-height: 80px;
small {
font-size: 16px;
color: #666;
}
}
ul {
display: flex;
flex-wrap: wrap;
li {
width: 310px;
height: 120px;
margin-right: 15px;
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 4px;
background: #fff;
&:nth-child(3n) {
margin-right: 0;
}
a {
display: flex;
width: 100%;
height: 100%;
align-items: center;
padding: 10px;
&:hover {
background: #e3f9f4;
}
img {
width: 95px;
height: 95px;
}
.info {
padding-left: 10px;
line-height: 24px;
overflow: hidden;
.name {
font-size: 16px;
color: #666;
}
.desc {
color: #999;
}
.price {
font-size: 22px;
color: $priceColor;
i {
font-size: 16px;
}
}
}
}
}
}
}
// 关键样式 hover状态下的layer盒子变成block
&:hover {
.layer {
display: block;
}
}
}
}
}
</style>
运行截图
3、在HomeCategory页面中访问接口,将返回的数据渲染在页面上
<script setup>
import { useCategoryStory } from '@/stores/category'
const categoryStory = useCategoryStory()
</script>
<template>
<div class="home-category">
<ul class="menu">
<!-- 获取categoryList,修改一级标题名字 -->
<li v-for="item in categoryStory.categoryList" :key="item.id">
<!-- 修改一级标题名字 -->
<RouterLink to="/">{{ item.name }}</RouterLink>
<!-- 便利里面children,只展示前两个,将标题名字渲染 -->
<RouterLink v-for="i in item.children.slice(0,2)" :key="i.id" to="/">{{ i.name }}</RouterLink>
<!-- 弹层layer位置 -->
<div class="layer">
<h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
<ul>
<!-- 修改商品的名字,图片,描述,价格 -->
<li v-for="i in item.goods" :key="i.id">
<RouterLink to="/">
<img :src="i.picture" />
<div class="info">
<p class="name ellipsis-2">
{{ i.name }}
</p>
<p class="desc ellipsis">{{ i.desc }}</p>
<p class="price"><i>¥</i>{{i.price}}</p>
</div>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</div>
</template>
运行结果
二、Banner轮播图
1、使用elemnetPlue中的轮播图
<script setup>
import {getBannerAPI} from '@/apis/Home.js'
import { onMounted, ref } from 'vue'
// 设置响应式数据bannerList
const bannerList = ref([])
// 访问接口,将获取到的数据赋值给bannerList
const getBanner = async () => {
const res = await getBannerAPI()
bannerList.value = res.result
}
// 挂载时调用方法
onMounted(()=>getBanner())
</script>
<template>
<div class="home-banner">
<el-carousel height="500px">
<!-- 将接口返回的数据,渲染在页面上 -->
<el-carousel-item v-for="item in bannerList" :key="item.id">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</template>
<style scoped lang='scss'>
.home-banner {
width: 1240px;
height: 500px;
position: absolute;
left: 0;
top: 0;
z-index: 98;
img {
width: 100%;
height: 500px;
}
}
</style>
2、在api文件夹下新建以下文件
import httpInstance from "@/utils/http";
// 获取BannerAPI
export function getBannerAPI(){
return httpInstance({
url:'/home/banner'
})
}
3、运行截图
三、面板组件封装
1、核心思路
把可复用的结构只写一次,把可能发生变化的部分抽象成组件参数(props/插槽)
2、实现步骤
1、不做任何抽象,准备静态模板
2、抽象可变的部分
- 主标题和副标题是纯文本,可以抽象成prop传入
- 主题内容是复杂的目标那,抽象成插槽传入
3、新建文件
模板如下
<script setup>
</script>
<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
新鲜好物<small>新鲜出炉 品质靠谱</small>
</h3>
</div>
<!-- 主体内容区域 -->
<div> 主体内容 </div>
</div>
</div>
</template>
<style scoped lang='scss'>
.home-panel {
background-color: #fff;
.head {
padding: 40px 0;
display: flex;
align-items: flex-end;
h3 {
flex: 1;
font-size: 32px;
font-weight: normal;
margin-left: 6px;
height: 35px;
line-height: 35px;
small {
font-size: 16px;
color: #999;
margin-left: 20px;
}
}
}
}
</style>
4、修改HomePanel部分代码
<script setup>
defineProps({
// 主标题
title:{
type:String
},
// 副标题
typetitle:{
type:String
}
})
</script>
<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{{title}}<small>{{typetitle}}</small>
</h3>
</div>
<!-- 主体内容区域 -->
<slot/>
</div>
</div>
</template>
5、在index.vue中修改部分代码
<template>
<div class="container">
<HomeCategory />
<HomeBanner />
</div>
<HomeNew />
<HomeHot />
<homeProduct />
<HomePanel title="新鲜好物" typetitle="新鲜好物 值得推荐">
<div>新鲜好物哈哈哈</div>
</HomePanel>
<HomePanel title="人气推荐" typetitle="人气推荐 值得推荐">
<div>人气推荐哈哈哈</div>
</HomePanel>
</template>
四、新鲜好物板块实现
1、修改HomeNew代码
<script setup>
import HomePanel from './HomePanel.vue'
import {getNewAPI} from '@/apis/Home'
import { onMounted, ref } from 'vue'
const newList = ref([])
const getNew = async () => {
const res = await getNewAPI()
console.log(res)
newList.value = res.result
}
onMounted(()=>getNew())
</script>
<template>
<HomePanel title="新鲜好物" typetitle="新鲜好物 值得推荐">
<!-- 下面是插槽主体内容模版 -->
<ul class="goods-list">
<li v-for="item in newList" :key="item.id">
<RouterLink to="/">
<img :src="item.picture" alt="" />
<p class="name">{{ item.name }}</p>
<p class="price">¥{{ item.price }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>
2、修改Home.js代码,添加访问新鲜好物的数据
import httpInstance from "@/utils/http";
// 获取BannerAPI
export function getBannerAPI(){
return httpInstance({
url:'/home/banner'
})
}
// 获取新鲜好物API
export function getNewAPI(){
return httpInstance({
url:'/home/new'
})
}
3、运行结果
五、人气推荐板块实现
1、修改HomeHot代码
<script setup>
import HomePanel from './HomePanel.vue'
import {getHotAPI} from '@/apis/Home'
import { onMounted, ref } from 'vue'
const hotList = ref([])
const getHot = async() =>{
const res = await getHotAPI()
console.log(res,123)
hotList.value = res.result
}
onMounted(()=>getHot())
</script>
<template>
<HomePanel title="人气推荐" typetitle="人气爆款 不容错过">
<ul class="goods-list">
<li v-for="item in hotList" :key="item.id">
<RouterLink to="/">
<img :src="item.picture" alt="">
<p class="name">{{ item.title }}</p>
<p class="desc">{{ item.alt }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>
<style scoped lang='scss'>
.goods-list {
display: flex;
justify-content: space-between;
height: 426px;
li {
width: 306px;
height: 406px;
transition: all .5s;
&:hover {
transform: translate3d(0, -3px, 0);
box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
}
img {
width: 306px;
height: 306px;
}
p {
font-size: 22px;
padding-top: 12px;
text-align: center;
}
.desc {
color: #999;
font-size: 18px;
}
}
}
</style>
2、修改Home.js代码,添加访问新鲜好物的数据
// 获取人气板块API
export function getHotAPI(){
return httpInstance({
url:'/home/hot'
})
}
3、运行结果
六、图片懒加载指令实现
1、核心原理
图片进入视口才发送资源请求
2、在main.js中使用useIntersectionObserver检测目标元素的可见性
1、官网
useIntersectionObserver | VueUse 中文网 (nodejs.cn)
2、使用
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import '@/styles/common.scss'
// useIntersectionObserver:检测目标元素的可见性
import { useIntersectionObserver } from '@vueuse/core'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
// 定义全局指令
app.directive('img-lazy',{
/**
*
* @param {*} el : 指令绑定的哪个元素 img
* @param {*} binding :binding.value 指令等于号后面绑定的表达式的值 图片url
*/
mounted(el,binding){
useIntersectionObserver(
el,
([{ isIntersecting }])=>{
// isIntersecting : 可以判断是否进入视口区域
if( isIntersecting ){
el.src = binding.value
}
}
)
}
})
运行截图
七、懒加载优化
1、问题1:逻辑书写位置不合理
懒加载指令的逻辑不应该直接写到入口文件中,只是不合理的 入口文件通常制作一些初始化的事情,不应该包含太多的逻辑代码,可以通过插件的方法把懒加载指令为插件,main.js入口文件只需要负责注册插件即可
新建以下文件
import { useIntersectionObserver } from '@vueuse/core'
// 定义全局指令
export const lazyPlugin = {
install(app){
app.directive('img-lazy',{
/**
*
* @param {*} el : 指令绑定的哪个元素 img
* @param {*} binding :binding.value 指令等于号后面绑定的表达式的值 图片url
*/
mounted(el,binding){
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }])=>{
// isIntersecting : 可以判断是否进入视口区域
if( isIntersecting ){
el.src = binding.value
// 停止useIntersectionObserver
stop()
}
}
)
}
})
}
}
2、问题2:重复监听问题
useIntersectionObserver对于元素的监听一直存在,除非手动停止监听,存在内存浪费
// 设置一个stop
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }])=>{
// isIntersecting : 可以判断是否进入视口区域
if( isIntersecting ){
el.src = binding.value
// 停止useIntersectionObserver
stop()
}
}
)
3、测试一下
八、Product产品列表实现
1、添加接口
// 获取Product数据
export function getProductAPI(){
return httpInstance({
url:'/home/goods'
})
}
2、修改HomeProduct代码
<script setup>
import HomePanel from './HomePanel.vue'
import { getProductAPI } from '@/apis/Home'
import { onMounted, ref } from 'vue'
const goodsProduct = ref([])
const getProduct = async () => {
const res = await getProductAPI()
goodsProduct.value = res.result
}
onMounted(()=>getProduct())
</script>
<template>
<div class="home-product">
<HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
<div class="box">
<RouterLink class="cover" to="/">
<img v-img-lazy="cate.picture" />
<strong class="label">
<span>{{ cate.name }}馆</span>
<span>{{ cate.saleInfo }}</span>
</strong>
</RouterLink>
<ul class="goods-list">
<li v-for="good in cate.goods" :key="good.id">
<RouterLink to="/" class="goods-item">
<img v-img-lazy="good.picture" alt="" />
<p class="name ellipsis">{{ good.name }}</p>
<p class="desc ellipsis">{{ good.desc }}</p>
<p class="price">¥{{ good.price }}</p>
</RouterLink>
</li>
</ul>
</div>
</HomePanel>
</div>
</template>
<style scoped lang='scss'>
.home-product {
background: #fff;
margin-top: 20px;
.sub {
margin-bottom: 2px;
a {
padding: 2px 12px;
font-size: 16px;
border-radius: 4px;
&:hover {
background: $xtxColor;
color: #fff;
}
&:last-child {
margin-right: 80px;
}
}
}
.box {
display: flex;
.cover {
width: 240px;
height: 610px;
margin-right: 10px;
position: relative;
img {
width: 100%;
height: 100%;
}
.label {
width: 188px;
height: 66px;
display: flex;
font-size: 18px;
color: #fff;
line-height: 66px;
font-weight: normal;
position: absolute;
left: 0;
top: 50%;
transform: translate3d(0, -50%, 0);
span {
text-align: center;
&:first-child {
width: 76px;
background: rgba(0, 0, 0, 0.9);
}
&:last-child {
flex: 1;
background: rgba(0, 0, 0, 0.7);
}
}
}
}
.goods-list {
width: 990px;
display: flex;
flex-wrap: wrap;
li {
width: 240px;
height: 300px;
margin-right: 10px;
margin-bottom: 10px;
&:nth-last-child(-n + 4) {
margin-bottom: 0;
}
&:nth-child(4n) {
margin-right: 0;
}
}
}
.goods-item {
display: block;
width: 220px;
padding: 20px 30px;
text-align: center;
transition: all .5s;
&:hover {
transform: translate3d(0, -3px, 0);
box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
}
img {
width: 160px;
height: 160px;
}
p {
padding-top: 10px;
}
.name {
font-size: 16px;
}
.desc {
color: #999;
height: 29px;
}
.price {
color: $priceColor;
font-size: 20px;
}
}
}
}
</style>
3、运行截图
九、GoodItem组件封装
设置一个GoodsItem属于纯展示类组件,这类组件的封装思想就是:抽象Props参数,传入什么就显示什么
1、新建文件
<script setup>
import {defineProps} from 'vue'
defineProps({
good:{
type:String
}
})
</script>
<template>
<RouterLink to="/" class="goods-item">
<img v-img-lazy="good.picture" alt="" />
<p class="name ellipsis">{{ good.name }}</p>
<p class="desc ellipsis">{{ good.desc }}</p>
<p class="price">¥{{ good.price }}</p>
</RouterLink>
</template>
<style scoped lang="scss">
.goods-item {
display: block;
width: 220px;
padding: 20px 30px;
text-align: center;
transition: all .5s;
&:hover {
transform: translate3d(0, -3px, 0);
box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
}
img {
width: 160px;
height: 160px;
}
p {
padding-top: 10px;
}
.name {
font-size: 16px;
}
.desc {
color: #999;
height: 29px;
}
.price {
color: $priceColor;
font-size: 20px;
}
}
</style>
2、修改HomeProduct页面代码
<script setup>
import HomePanel from './HomePanel.vue'
import { getProductAPI } from '@/apis/Home'
import { onMounted, ref } from 'vue'
import GoodsItem from './GoodsItem.vue'
const goodsProduct = ref([])
const getProduct = async () => {
const res = await getProductAPI()
goodsProduct.value = res.result
}
onMounted(()=>getProduct())
</script>
<template>
<div class="home-product">
<HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
<div class="box">
<RouterLink class="cover" to="/">
<img v-img-lazy="cate.picture" />
<strong class="label">
<span>{{ cate.name }}馆</span>
<span>{{ cate.saleInfo }}</span>
</strong>
</RouterLink>
<ul class="goods-list">
<li v-for="good in cate.goods" :key="good.id">
<GoodsItem :good="good"/>
</li>
</ul>
</div>
</HomePanel>
</div>
</template>
<style scoped lang='scss'>
.home-product {
background: #fff;
margin-top: 20px;
.sub {
margin-bottom: 2px;
a {
padding: 2px 12px;
font-size: 16px;
border-radius: 4px;
&:hover {
background: $xtxColor;
color: #fff;
}
&:last-child {
margin-right: 80px;
}
}
}
.box {
display: flex;
.cover {
width: 240px;
height: 610px;
margin-right: 10px;
position: relative;
img {
width: 100%;
height: 100%;
}
.label {
width: 188px;
height: 66px;
display: flex;
font-size: 18px;
color: #fff;
line-height: 66px;
font-weight: normal;
position: absolute;
left: 0;
top: 50%;
transform: translate3d(0, -50%, 0);
span {
text-align: center;
&:first-child {
width: 76px;
background: rgba(0, 0, 0, 0.9);
}
&:last-child {
flex: 1;
background: rgba(0, 0, 0, 0.7);
}
}
}
}
.goods-list {
width: 990px;
display: flex;
flex-wrap: wrap;
li {
width: 240px;
height: 300px;
margin-right: 10px;
margin-bottom: 10px;
&:nth-last-child(-n + 4) {
margin-bottom: 0;
}
&:nth-child(4n) {
margin-right: 0;
}
}
}
}
}
</style>