系列文章目录
文章目录
Home
1.Home整体结构搭建和分类实现
Home
文件夹创建components
五个组件HomeBanner、HomeCategory、HomeHot、HomeNew、HomeProduct
,写点代码。
<template>
我是...
</template>
打开Home的index.vue
,引入并使用组件
<script setup>
import HomeCategory from './components/HomeCategory.vue'
import HomeBanner from './components/HomeBanner.vue'
import HomeNew from './components/HomeNew.vue'
import HomeHot from './components/HomeHot.vue'
import homeProduct from './components/HomeProduct.vue'
</script>
<template>
<div class="container">
<HomeCategory />
<HomeBanner />
</div>
<HomeNew />
<HomeHot />
<homeProduct />
</template>
效果:
分类的实现
- 准备模板
<!--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>
- 使用
pinia
渲染出来
<script setup>
import { useCategoryStore } from "@/stores/category";
const categoryStore = useCategoryStore() //实例有了
</script>
看一下数据结构
<!--将数据绑定给对应字段-->
<li v-for="item in categoryStore.categoryList" :key="item">
<RouterLink to="/">{{ item.name }}</RouterLink>
<RouterLink v-for="i in item.children.slice(0, 2)" :key="i" 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 alt="" :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>
2. banner轮播图功能
- 准备模板
<script setup>
</script>
<!-- HomeBanner.vue -->
<template>
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in 4" :key="item">
<img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg"
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>
- 熟悉
elementPlus
相关组件 - 获取接口数据
封装接口
//获取轮播图数据 @/apis/home.js
export function getBannerAPI(params) {
return httpInstance({
url: '/home/banner',
params
})
}
获取数据
<script setup>
import { getBannerAPI } from '@/apis/home.js'
import { onMounted } from 'vue'
// 获取轮播图数据
const getBannerList = async () => {
const res = await getBannerAPI()
console.log(res)
}
onMounted(() => getBannerList())
</script>
- 渲染组件
<script setup>
import { getBannerAPI } from '@/apis/home.js'
import { onMounted } from 'vue'
import { ref } from 'vue'
// 获取轮播图数据
const bannerList = ref([]) //存储轮播图数组
const getBannerList = async () => {
const res = await getBannerAPI()
// console.log(res)
bannerList.value = res.result
// console.log(bannerList)
}
onMounted(() => getBannerList())
</script>
<!-- HomeBanner.vue -->
<template>
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</template>
效果:
3. Home 面板组件封装
组件封装能解决的问题:a. 复用问题 b. 业务维护问题
新鲜好物 和 人气推荐 模块 结构非常相似,我们考虑进行组件封装,将组件进行复用
组件封装核心思路: 把可复用的结构只写一次,把可能发生变化的部分(比如说上图的 新鲜好物人气推荐文字就是不同点;图片下的文字也不同)抽象成组件参数(props
/插槽)
实现步骤:
- 准备静态模板 在
Home
文件夹的components
创建一个HomePanel.vue
<!-- HomePanel -->
<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>
-
抽象可变的部分:
- 主标题副标题是纯文本,可以抽象为
prop
传入
<!-- HomePanel 相关代码--> <script setup> //定义props defineProps({ title: { type: String }, //主标题 subTitile: { type: String } //负标题 }) </script> <template> <div class="home-panel"> <div class="container"> <div class="head"> <!-- 主标题和副标题 --> <h3> {{ title }}<small>{{ subTitile }}</small> </h3> </div> <!-- 主体内容区域 --> <div> 主体内容 </div> </div> </div> </template>
- 主体内容是复杂模板,抽象成插槽传入
- 主标题副标题是纯文本,可以抽象为
<!-- HomePanel 相关代码-->
<!-- 主体内容区域 -->
<!-- <div> 主体内容 </div> -->
<slot />
导入到入口组件Home/index.vue
中,测试一下
<!-- Home/index.vue -->
<script setup>
import HomeCategory from './components/HomeCategory.vue'
import HomeBanner from './components/HomeBanner.vue'
import HomeNew from './components/HomeNew.vue'
import HomeHot from './components/HomeHot.vue'
import HomeProduct from './components/HomeProduct.vue'
import HomePanel from './components/HomePanel.vue'
</script>
<template>
<div class="container">
<HomeCategory />
<HomeBanner />
</div>
<HomeNew />
<HomeHot />
<HomeProduct />
<!-- 测试,插槽要成对出现 -->
<HomePanel title="新鲜好物" subTitle="新鲜好物 好多商品">
<div>我是新鲜好物的插槽内容</div>
</HomePanel>
<HomePanel title="人气推荐" subTitle="人气推荐 好多商品">
<div>我是人气推荐的插槽内容</div>
</HomePanel>
</template>
纯展示类组件通用封装思路总结:
- 搭建纯静态的部分,不管可变的部分
- 抽象可变的部分为组件参数(非复杂的模板抽象为props,复杂的结构模板抽象为插槽)
4.新鲜好物和人气推荐实现
步骤:
- 准备模板
<!-- 新鲜好物 HomeNew.vue -->
<script setup>
</script>
<template>
<div></div>
<!-- 下面是插槽主体内容模版
<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>
-->
</template>
<style scoped lang='scss'>
.goods-list {
display: flex;
justify-content: space-between;
height: 406px;
li {
width: 306px;
height: 406px;
background: #f0f9f4;
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;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.price {
color: $priceColor;
}
}
}
</style>
- 定制props
引入HomePanel
,写入props
<!-- 新鲜好物 HomeNew.vue -->
<script setup>
import HomePanel from './HomePanel.vue'
</script>
<template>
<HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱"></HomePanel>
<div></div>
...
- 定制插槽内容(接口+渲染模板)
//获取新鲜好物内容
export function getFreshAndGoodAPI(params) {
return httpInstance({
url: '/home/new',
params
})
}
<!-- 新鲜好物 HomeNew.vue 相关代码 -->
<script setup>
import HomePanel from './HomePanel.vue'
import { getFreshAndGoodAPI } from '@/apis/home.js'
import { onMounted } from 'vue'
import { ref } from 'vue'
const freshAndGoodList = ref([]) //装新鲜好物数据的数组
const getFreshAndGood = async () => {
const res = await getFreshAndGoodAPI()
// console.log(res)
freshAndGoodList.value = res.result
}
onMounted(() => {
getFreshAndGood()
})
</script>
<template>
<HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱">
<div>
<ul class="goods-list">
<li v-for="item in freshAndGoodList" :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>
</div>
</HomePanel>
</template>
人气推荐 类似写法,在此贴上相关代码
//获取 人气推荐 内容
export function getHotAPI() {
return httpInstance({
url: '/home/hot'
})
}
<script setup>
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/apis/home'
import { ref } from 'vue'
const hotList = ref([])
const getHotList = async () => {
const res = await getHotAPI()
hotList.value = res.result
}
getHotList()
</script>
<template>
<HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
<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>
5. 图片懒加载指令实现
进入视口区域才发送图片请求。
步骤:
- 熟悉指令语法
自定义指令:https://cn.vuejs.org/guide/reusability/custom-directives.html,记得选 组合式
定义指令
//定义全局指令 main.js
app.directive('img-lazy', {
mounted(el, binding) {
//el:指令绑定的那个元素 img
//binding:binding》value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value)
}
})
- 判断图片是否进入视口(
vueUse
)
使用useIntersectionObserver
//main.js
//定义全局指令
app.directive('img-lazy', {
mounted(el, binding) {
//el:指令绑定的那个元素 img
//binding:binding》value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value)
useIntersectionObserver(
el, //监听的元素el
([{ isIntersecting }]) => { //isIntersecting 布尔值
console.log(isIntersecting) //图片进入视口区域,那么isIntersecting为true,否则false
}
)
}
})
- 如果图片进入视口,发送图片资源请求(
img.src = url
)
//定义全局指令
app.directive('img-lazy', {
mounted(el, binding) {
//el:指令绑定的那个元素 img
//binding:binding》value 指令等于号后面绑定的表达式的值 图片url
//console.log(el, binding.value)
useIntersectionObserver(
el, //监听的元素el
([{ isIntersecting }]) => { //isIntersecting 布尔值
//console.log(isIntersecting) //图片进入视口区域,那么isIntersecting为true,否则false
if (isIntersecting) {
//进入视口区域
el.src = binding.value
}
}
)
}
})
<!--HomeHot.vue-->
<img v-img-lazy="item.picture" alt="">
懒加载指令优化
- a. 书写位置不合理。我们将懒加载指令封装为一个插件,
main.js
入口文件只需要负责注册插件即可
//directinve新建index.js
//定义懒加载插件
import { useIntersectionObserver } from "@vueuse/core";
export const lazyPlugin = {
install(app) {
//懒加载指令逻辑
app.directive('img-lazy', {
mounted(el, binding) {
//el:指令绑定的那个元素 img
//binding:binding》value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value)
useIntersectionObserver(
el, //监听的元素el
([{ isIntersecting }]) => { //isIntersecting 布尔值
console.log(isIntersecting) //图片进入视口区域,那么isIntersecting为true,否则false
if (isIntersecting) {
//进入视口区域
el.src = binding.value
}
}
)
}
})
}
}
在main.js中注册插件
import { lazyPlugin } from './directives'
app.use(lazyPlugin)
- b.
useIntersectionObserver
对应元素的监听一直存在,除非手动停止监听,存在内存浪费
图片加载完成就不需要监听了,也就是完成第一次加载就停止监听
const { stop } = useIntersectionObserver(
el, //监听的元素el
([{ isIntersecting }]) => { //isIntersecting 布尔值
//console.log(isIntersecting) //图片进入视口区域,那么isIntersecting为true,否则false
if (isIntersecting) {
//进入视口区域
el.src = binding.value
stop() //结构出的停止监听的方法
}
}
)
6. Home- product产品列表实现
重复几个下图结构组成
-
准备静态模板
<!--HomeProduct.vue--> <script setup> import HomePanel from './HomePanel.vue' </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 :src="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 :src="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>封装接口
-
封装接口
/**
*获取商品模块Product
*/
export const getGoodsAPI = () => {
return httpInstance({
url: '/home/goods'
})
}
- 获取数据渲染模板
<!--HomeProduct.vue-->
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
const goodsProduct = ref([])
const getGoods = async () => {
const res = await getGoodsAPI()
goodsProduct.value = res.result
}
onMounted(() => {
getGoods()
})
</script>
<!--HomeProduct.vue-->
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
const goodsProduct = ref([])
const getGoods = async () => {
const res = await getGoodsAPI()
goodsProduct.value = res.result
}
onMounted(() => {
getGoods()
})
</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 :src="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 :src="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>
- 图片懒加载
查看是否成功:向下滚动看看是否发送请求获取图片:
7. Home-GoodsItem 组件封装
结构类似,没必要重复定义,封装起来,方便复用
核心思想:将要显示的数据对象设计为props参数,传入什么对象,显示什么数据
新增GoodItem.vue
, 将代码抽离出来。定义props。把相关样式抽离出来。
<script setup>
defineProps({
goods: {
type: Object,
default: () => { }
}
})
</script>
<template>
<RouterLink to="/" class="goods-item">
<img v-img-lazy="goods.picture" alt="" />
<p class="name ellipsis">{{ goods.name }}</p>
<p class="desc ellipsis">{{ goods.desc }}</p>
<p class="price">¥{{ goods.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>
使用:
<!--HomeProduct.vue-->
<ul class="goods-list">
<li v-for="goods in cate.goods" :key="goods.id">
<GoodItem :goods="goods" />
</li>
</ul>
一级路由
1. 整体认识和路由配置
配置路由,点击哪个就跳转到哪个页面,地址还要有参数
找到LayoutHeader
组件,拼接字符串
效果,鼠标放到category上,看左下角:
吸顶也要配置一下,一样的方式,打开LayoutFixed
<!-- 导航区域 -->
<ul class="app-header-nav">
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
</li>
</ul>
2. 面包屑导航
- 准备模板
<!--Category/index.vue-->
<script setup>
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>居家</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.top-category {
h3 {
font-size: 28px;
color: #666;
font-weight: normal;
text-align: center;
line-height: 100px;
}
.sub-list {
margin-top: 20px;
background-color: #fff;
ul {
display: flex;
padding: 0 32px;
flex-wrap: wrap;
li {
width: 168px;
height: 160px;
a {
text-align: center;
display: block;
font-size: 16px;
img {
width: 100px;
height: 100px;
}
p {
line-height: 40px;
}
&:hover {
color: $xtxColor;
}
}
}
}
}
.ref-goods {
background-color: #fff;
margin-top: 20px;
position: relative;
.head {
.xtx-more {
position: absolute;
top: 20px;
right: 20px;
}
.tag {
text-align: center;
color: #999;
font-size: 20px;
position: relative;
top: -20px;
}
}
.body {
display: flex;
justify-content: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
</style>
- 封装接口 这个接口有一个参数,和之前的不一样
// apis/category.js
//获取二级分类列表
export function getCategoryAPI(id) {
return httpInstance({
url: '/category',
params: {
id
}
})
}
- 调用接口获取数据
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const categoryData = ref({})
const getCategoryData = anysc() => {
const route = useRoute()
const res = await getCategoryAPI(route.params.id)
categoryData.value = res.result
}
onMounted(() => {
getCategoryData()
})
</script>
- 渲染模板
<!--Category/index.vue-->
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
3. 一级分类 - 轮播图的实现
轮播图的结构很相似,我们改造一下接口,再复用一下首页轮播图的逻辑
- 改造之前的接口(适配参数)
//获取轮播图banner数据
export function getBannerAPI(params = {}) {
// 默认为1 商品为2
const { distributionSite = '1' } = params
return httpInstance({
url: '/home/banner',
params: {
distributionSite
}
})
}
- 迁移首页轮播图的逻辑,样式也要记得迁移
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
//获取数据
const categoryData = ref({})
const route = useRoute()
const getCategoryData = async () => {
const res = await getCategoryAPI(route.params.id)
categoryData.value = res.result
}
// 获取轮播图数据
const bannerList = ref([]) //存储轮播图数组
const getBannerList = async () => {
const res = await getBannerAPI({
// 默认为1 商品为2
distributionSite: '2'
})
// console.log(res)
bannerList.value = res.result
// console.log(bannerList)
}
onMounted(() => {
getCategoryData()
getBannerList()
})
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 轮播图 -->
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.top-category {
h3 {
font-size: 28px;
color: #666;
font-weight: normal;
text-align: center;
line-height: 100px;
}
.sub-list {
margin-top: 20px;
background-color: #fff;
ul {
display: flex;
padding: 0 32px;
flex-wrap: wrap;
li {
width: 168px;
height: 160px;
a {
text-align: center;
display: block;
font-size: 16px;
img {
width: 100px;
height: 100px;
}
p {
line-height: 40px;
}
&:hover {
color: $xtxColor;
}
}
}
}
}
.ref-goods {
background-color: #fff;
margin-top: 20px;
position: relative;
.head {
.xtx-more {
position: absolute;
top: 20px;
right: 20px;
}
.tag {
text-align: center;
color: #999;
font-size: 20px;
position: relative;
top: -20px;
}
}
.body {
display: flex;
justify-content: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
.home-banner {
width: 1240px;
height: 500px;
// position: absolute;
margin: 0 auto; //居中
left: 0;
top: 0;
z-index: 98;
img {
width: 100%;
height: 500px;
}
}
</style>
4. 激活状态控制
点击后 样式发生如下改变
添加active-class
属性
<!--LayoutHeader.vue-->
<ul class="app-header-nav">
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
</li>
</ul>
5.分类列表的渲染
-
模板
<!--category/index.vue--> <div class="sub-list"> <h3>全部分类</h3> <ul> <li v-for="i in categoryData.children" :key="i.id"> <RouterLink to="/"> <img :src="i.picture" /> <p>{{ i.name }}</p> </RouterLink> </li> </ul> </div> <div class="ref-goods" v-for="item in categoryData.children" :key="item.id"> <div class="head"> <h3>- {{ item.name }}-</h3> </div> <div class="body"> <GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" /> </div> </div>
全部代码:
<!--Category/index.vue--> <script setup> import { getCategoryAPI } from '@/apis/category.js' import { getBannerAPI } from '@/apis/home.js' import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import GoodsItem from '../Home/components/GoodItem.vue' //获取数据 const categoryData = ref({}) const route = useRoute() const getCategoryData = async () => { const res = await getCategoryAPI(route.params.id) categoryData.value = res.result } // 获取轮播图数据 const bannerList = ref([]) //存储轮播图数组 const getBannerList = async () => { const res = await getBannerAPI({ // 默认为1 商品为2 distributionSite: '2' }) // console.log(res) bannerList.value = res.result // console.log(bannerList) } onMounted(() => { getCategoryData() getBannerList() }) </script> <template> <div class="top-category"> <div class="container m-top-20"> <!-- 面包屑 --> <div class="bread-container"> <el-breadcrumb separator=">"> <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> <el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item> </el-breadcrumb> </div> <!-- 轮播图 --> <div class="home-banner"> <el-carousel height="500px"> <el-carousel-item v-for="item in bannerList" :key="item"> <img :src="item.imgUrl" alt=""> </el-carousel-item> </el-carousel> </div> <!-- 分类列表 --> <div class="sub-list"> <h3>全部分类</h3> <ul> <li v-for="i in categoryData.children" :key="i.id"> <RouterLink to="/"> <img :src="i.picture" /> <p>{{ i.name }}</p> </RouterLink> </li> </ul> </div> <div class="ref-goods" v-for="item in categoryData.children" :key="item.id"> <div class="head"> <h3>- {{ item.name }}-</h3> </div> <div class="body"> <GoodsItem v-for="goods in item.goods" :goods="goods" :key="goods.id" /> </div> </div> </div> </div> </template> <style scoped lang="scss"> .top-category { h3 { font-size: 28px; color: #666; font-weight: normal; text-align: center; line-height: 100px; } .sub-list { margin-top: 20px; background-color: #fff; ul { display: flex; padding: 0 32px; flex-wrap: wrap; li { width: 168px; height: 160px; a { text-align: center; display: block; font-size: 16px; img { width: 100px; height: 100px; } p { line-height: 40px; } &:hover { color: $xtxColor; } } } } } .ref-goods { background-color: #fff; margin-top: 20px; position: relative; .head { .xtx-more { position: absolute; top: 20px; right: 20px; } .tag { text-align: center; color: #999; font-size: 20px; position: relative; top: -20px; } } .body { display: flex; justify-content: space-around; padding: 0 40px 30px; } } .bread-container { padding: 25px 0; } } .home-banner { width: 1240px; height: 500px; // position: absolute; margin: 0 auto; //居中 left: 0; top: 0; z-index: 98; img { width: 100%; height: 500px; } } </style>
6.一级分类-解决路由缓存问题
官方说明:
一级分类切换满足这个条件,组件实例复用,导致分类数据无法进行更新。
点击美食、居家,参数会发生变化但是页面不会变化。
解决问题思路:
- 办法1:不复用组件实例,强制销毁
以当前路由完整路径为key的值,给router-view组件绑定
<RouterView :key="$route.fullPath"/>
- 办法2:监听路由变化,变化之后执行 数据更新 操作
使用beforeRouteUpdate
导航钩子,这个函数可以再每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可。
// Category/index.vue
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../Home/components/GoodItem.vue'
import { onBeforeRouteUpdate } from 'vue-router'
//获取数据
const categoryData = ref({})
const route = useRoute()
const getCategoryData = async () => {
const res = await getCategoryAPI(route.params.id)
categoryData.value = res.result
}
// 获取轮播图数据
const bannerList = ref([]) //存储轮播图数组
const getBannerList = async (id = route.params.id) => {
const res = await getBannerAPI(id)
// console.log(res)
bannerList.value = res.result
// console.log(bannerList)
}
onMounted(() => {
getCategoryData()
getBannerList()
})
//期望在路由参数变化时,分类数据接口重新发送
onBeforeRouteUpdate((to) => {
getCategoryData(to.params.id)
})
</script>
7. 使用 逻辑函数 拆分业务
指的是,把同一个组件中独立的业务代码通过函数作封装处理,提高代码的可维护性
实现步骤:
- 安装业务声明以
use
打头的逻辑函数 - 把独立的业务逻辑封装到各个函数内部
- 函数内部把组件中需要用到的数据或者方法
return
出去 - 在组件中调用函数,把数据或者方法组合回来使用
新建两个js文件
// 封装banner轮播图相关的业务代码 useBanner.js
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/apis/home'
export function useBanner() {
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
})
console.log(res)
bannerList.value = res.result
}
onMounted(() => getBanner())
return {
bannerList
}
}
//useCategory.js
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { getCategoryAPI } from '@/apis/category'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'
export function useCategory() {
// 获取分类数据
const categoryData = ref({})
const route = useRoute()
const getCategory = async (id = route.params.id) => {
const res = await getCategoryAPI(id)
categoryData.value = res.result
}
onMounted(() => getCategory())
// 目标:路由参数变化的时候 可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题:使用最新的路由参数请求最新的分类数据
getCategory(to.params.id)
})
return {
categoryData
}
}
<!--Category/index.vue-->
<script setup>
import GoodItem from '../Home/components/GoodItem.vue'
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 轮播图 -->
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
<!-- 分类列表 -->
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="i in categoryData.children" :key="i.id">
<RouterLink to="/">
<img :src="i.picture" />
<p>{{ i.name }}</p>
</RouterLink>
</li>
</ul>
</div>
<div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
<div class="head">
<h3>- {{ item.name }}-</h3>
</div>
<div class="body">
<GoodsItem v-for="goods in item.goods" :goods="goods" :key="goods.id" />
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.top-category {
h3 {
font-size: 28px;
color: #666;
font-weight: normal;
text-align: center;
line-height: 100px;
}
.sub-list {
margin-top: 20px;
background-color: #fff;
ul {
display: flex;
padding: 0 32px;
flex-wrap: wrap;
li {
width: 168px;
height: 160px;
a {
text-align: center;
display: block;
font-size: 16px;
img {
width: 100px;
height: 100px;
}
p {
line-height: 40px;
}
&:hover {
color: $xtxColor;
}
}
}
}
}
.ref-goods {
background-color: #fff;
margin-top: 20px;
position: relative;
.head {
.xtx-more {
position: absolute;
top: 20px;
right: 20px;
}
.tag {
text-align: center;
color: #999;
font-size: 20px;
position: relative;
top: -20px;
}
}
.body {
display: flex;
justify-content: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
.home-banner {
width: 1240px;
height: 500px;
// position: absolute;
margin: 0 auto; //居中
left: 0;
top: 0;
z-index: 98;
img {
width: 100%;
height: 500px;
}
}
</style>
小结
本章对于Home和一级路由功能进行了实现
组件的静态结构也很有价值去研究和书写
love and peace
祝大家学习顺利