0.学习大纲
1.Home模块
静态结构搭建和分类实现
整体结构创建
按照结构新增五个组件,准备最简单的模版,分别在Home模块的入口组件中引入
-
HomeCategory
-
HomeBanner
-
HomeNew
-
HomeHot
-
HomeProduct
在组件中添加简单模版
<script setup>
</script>
<template>
<div> HomeCategory </div>
</template>
Home模块入口组件中引入并渲染
<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.vue
<script setup>
import { useCategoryStore } from "@/stores/category";
const categoryStore = useCategoryStore();
</script>
<template>
<div class="home-category">
<ul class="menu">
<li v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
<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 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>
</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>
banner轮播图实现
实现思路
获取数据渲染组件
// /apis/home.js
import http from '@/utils/http'
export function getBannerAPI() {
return http({
url: 'home/banner'
})
}
// HomeBanner.vue
<script setup>
import { getBannerAPI } from "@/apis/home";
import { ref, onMounted } from "vue";
// 获取轮播图数据
const bannerList = ref([]);
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>
面板组件封装
场景说明
问:组件封装解决了什么问题?
答:1.复用问题 2.业务维护问题
新鲜好物和人气推荐模块,在结构上非常相似,只是内容不同,通过组件封装可以实现复用结构的效果。
核心思路:把可复用的结构只写一次,把可能发生的部分抽象成组件参数(props/插槽)
实现步骤
1.不做任何抽象,准备静态模版
2.抽象可变的部分
- 主标题和副标题是纯文本,可以抽象成prop传入
- 主体内容是复杂的模版,抽象成插槽传入
组件封装
// HomePanel.vue
<script setup>
defineProps({
title: {
type: String,
default: "",
},
subTitle: {
type: String,
default: "",
},
});
</script>
<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{{ title }}<small>{{ subTitle }}</small>
</h3>
</div>
<!-- 主体内容区域 -->
<slot name="main" />
</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>
新鲜好物与人气推荐业务实现
实现思路
获取数据渲染组件
// Home.js
/**
* @description: 获取新鲜好物
* @param {*}
* @return {*}
*/
export const findNewAPI = () => {
return http({
url: '/home/new'
})
}
/**
* @description: 获取人气推荐
* @param {*}
* @return {*}
*/
export const getHotAPI = () => {
return http({
url: '/home/hot'
})
}
// HomeNew.vue
<script setup>
import HomePanel from "./HomePanel.vue";
import { findNewAPI } from "@/apis/home";
import { onMounted, ref } from "vue";
// 获取数据
const newList = ref([]);
const getNewList = async () => {
const res = await findNewAPI();
newList.value = res.result;
};
onMounted(() => getNewList());
</script>
<template>
<HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">
<ul class="goods-list">
<li v-for="item in newList" :key="item.id">
<RouterLink :to="`/detail/${item.id}`">
<img :src="item.picture" alt="" />
<p class="name">{{ item.name }}</p>
<p class="price">¥{{ item.price }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>
<style scoped lang='scss'>
.goods-list {
display: flex;
justify-content: space-between;
height: 406px;
li {
width: 306px;
height: 406px;
background: #f0f9f4;
transition: all 0.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>
// HomeHot.vue
<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();
console.log(res);
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 0.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>
图片懒加载指令实现
场景和指令用法
场景:电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载手段可以做到只有进入视口区域才发送图片请求。
指令用法
指令实现
// /directive/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)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
// console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
stop()
}
},
)
}
})
}
}
全局注册
// main.js
// 全局指令注册
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)
产品列表实现与小组件封装
// home.js
/**
* @description: 获取所有商品模块
* @param {*}
* @return {*}
*/
export const getGoodsAPI = () => {
return http({
url: '/home/goods'
})
}
// GoodsItem.vue
<script setup>
defineProps({
goods: {
tppe: 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 lang="scss" scoped>
.goods-item {
display: block;
width: 220px;
padding: 20px 30px;
text-align: center;
transition: all 0.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
<script setup>
import HomePanel from "./HomePanel.vue";
import { getGoodsAPI } from "@/apis/home";
import { onMounted, ref } from "vue";
import GoodsItem from "./GoodsItem.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 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="goods in cate.goods" :key="goods.id">
<GoodsItem :goods="goods" />
</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>
2.商品一级分类
整体功能认识和路由配置
点击不同的导航,进行切换
配置路由
根据传入 id 的不同进行切换
面包屑导航功能实现
封装接口
// /apis/category.js
import http from '@/utils/http'
/**
* @description: 获取分类数据
* @param {*} id 分类id
* @return {*}
*/
export const getTopCategoryAPI = (id) => {
return http({
url: '/category',
params: {
id
}
})
}
页面实现
// /views/Category/index.vue
<script setup>
import { getTopCategoryAPI } from "@/apis/category";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
const categoryData = ref({});
const route = useRoute();
const getCategory = async () => {
const res = await getTopCategoryAPI(route.params.id);
categoryData.value = res.result;
};
onMounted(() => {
getCategory();
});
</script>
<template>
<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>
</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>
Banner轮播图功能实现
适配接口
// /apis/home.js
/**
*@description: 获取banner轮播图数据
* @param {*}
* @return {*}
*/
export function getBannerAPI(params = {}) {
// 默认为1 商品为2
const { distributionSite = '1' } = params
return http({
url: 'home/banner',
params: {
distributionSite
}
})
}
迁移首页Banner逻辑
// /views/Category/index.js
// 获取banner
const bannerList = ref([]);
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: "2",
});
console.log(res);
bannerList.value = res.result;
};
onMounted(() => getBanner());
</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.id">
<img :src="item.imgUrl" alt="" />
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
</template>
激活状态控制与分类列表功能实现
激活状态控制
分类列表实现
// /views/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>
路由缓存问题解决
当路由path一样,参数不同的时候会选择直接复用路由对应的组件。
解决方案:
-
给 routerv-view 添加key属性,破坏缓存
-
使用 onBeforeRouteUpdate钩子函数,做精确更新
方案一
问题:整个页面示例都被破坏,影响性能。
方案二
总结
1.路由缓存问题产生的原因是什么?
路由只有参数变化时,会复用组件实例。
2.两种方案都可以解决路由缓存问题,如何选择?
在意性能问题,选择 onBeforeUpdate,精细化控制
不在意性能问题,选择 key,简单粗暴
基于业务逻辑的函数拆分
有点像Java的抽出方法
// useCategory.js
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { getTopCategoryAPI } 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 getTopCategoryAPI(id)
categoryData.value = res.result
}
onMounted(() => getCategory())
// 目标:路由参数变化的时候 可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题:使用最新的路由参数请求最新的分类数据
getCategory(to.params.id)
})
return {
categoryData
}
}
// useBanner.js
// 封装banner轮播图相关的业务代码
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'
})
bannerList.value = res.result
}
onMounted(() => getBanner())
return {
bannerList
}
}
引入使用
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()