1、路由配置
(1)引入静态模板(Views/detail/index.vue)
(2)配置路由(在这里,需要添加占位符id,当用户二级分类下的商品页面时需要携带这个id跳转到商品详情页
{
path: 'detail/:id',
component: Detail
},
(3)设置跳转(Home/HomeNew.vue)
来到首页下面的二级分类下,当我们点击商品的时候,需要携带参数跳转到商品详情页面,使用模板字符串进行拼接。
<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>
看下运行结果
2、基础数据渲染
(1)封装接口
export const getDetail = (id) => {
return request({
url: '/goods',
params: {
id
}
})
}
(2)请求获取数据
注意,记得先导入封装好的接口
const goods = ref({})
const route = useRoute()
const getGoods = async () => {
const res = await getDetail(route.params.id)
goods.value = res.result
}
onMounted(() => getGoods())
后台返回:
(3)根据返回的数据渲染模板
代码详情:
<div class="info-container">
<div>
<div class="goods-info">
<div class="media">
<!-- 图片预览区 -->
<XtxImageView :image-list="goods.mainPictures" />
<!-- 统计数量 -->
<ul class="goods-sales">
<li>
<p>销量人气</p>
<p> {{ goods.salesCount }}+ </p>
<p><i class="iconfont icon-task-filling"></i>销量人气</p>
</li>
<li>
<p>商品评价</p>
<p>{{ goods.commentCount }}+</p>
<p><i class="iconfont icon-comment-filling"></i>查看评价</p>
</li>
<li>
<p>收藏人气</p>
<p>{{ goods.collectCount }}+</p>
<p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>
</li>
<li>
<p>品牌信息</p>
<p>{{ goods.brand.name }}</p>
<p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>
</li>
</ul>
</div>
<div class="spec">
<!-- 商品信息区 -->
<p class="g-name"> {{ goods.name }} </p>
<p class="g-desc">{{ goods.desc }} </p>
<p class="g-price">
<span>{{ goods.oldPrice }}</span>
<span> {{ goods.price }}</span>
</p>
<div class="g-service">
<dl>
<dt>促销</dt>
<dd>12月好物放送,App领券购买直降120元</dd>
</dl>
<dl>
<dt>服务</dt>
<dd>
<span>无忧退货</span>
<span>快速退款</span>
<span>免费包邮</span>
<a href="javascript:;">了解详情</a>
</dd>
</dl>
</div>
<!-- sku组件 -->
<XtxSku :goods="goods" @change="skuChange" />
<!-- 数据组件 -->
<el-input-number v-model="count" @change="countChange" />
<!-- 按钮组件 -->
<div>
<el-button size="large" class="btn" @click="addCart">
加入购物车
</el-button>
</div>
</div>
</div>
<div class="goods-footer">
<div class="goods-article">
<!-- 商品详情 -->
<div class="goods-tabs">
<nav>
<a>商品详情</a>
</nav>
<div class="goods-detail">
<!-- 属性 -->
<ul class="attrs">
<li v-for="item in goods.details.properties" :key="item.value">
<span class="dt">{{ item.name }}</span>
<span class="dd">{{ item.value }}</span>
</li>
</ul>
<!-- 图片 -->
<img v-for="img in goods.details.pictures" :src="img" :key="img" alt="">
</div>
</div>
</div>
<!-- 24热榜+专题推荐 -->
<div class="goods-aside">
<!-- 24小时 -->
<DetailHot :hot-type="1" />
<!-- 周 -->
<DetailHot :hot-type="2" />
</div>
</div>
</div>
</div>
这里有个坑,大家需要注意一下:
如果按照正常的思路来写,代码可能会报这样的错误:
为什么会出现这样的情况呢?仔细观察我们的代码
const goods = ref({})
goods一开始是个空对象呀,访问它下面的第一项肯定是不存在的,更别说其它项了。在这里,有两种解决方案,一种是可选链?.,另一种是添加v-if属性控制渲染的时机,在这里,我采用第一种:第一种可选链:问号前面的数据有值的话才会执行后面的内容,反之则不会。第二种v-if控制:如果goods中的任意一项有值,说明整个内容的其它数据也是有值的。这里,我两种方法都是试过的,在代码中只粘贴第一种方法。
<div class="container" v-if="goods.details">
<el-breadcrumb-item :to="{ path: `/category/${goods.categories[1].id}` }">{{ goods.categories[1].name }}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[0].id}` }">{{
goods.categories[0].name
}}
</el-breadcrumb-item>
改完之后,可以看到数据被正确渲染了出来。
3、商品热榜区
(1)组件封装与数据渲染
在这里,我们要实现这样一个功能:
1)封装接口(apis/detail.js)
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {
return request({
url: '/goods/hot',
params: {
id,
type,
limit
}
})
}
2)请求接口获取数据:
const hotList = ref([])
const route = useRoute()
const getHotList = async () => {
const res = await getHotGoodsAPI({
id: route.params.id,
type: props.hotType
})
hotList.value = res.result
}
onMounted(() => getHotList())
浏览器返回
3)渲染模板:
<div class="goods-hot">
<h3>{{ title }}</h3>
<!-- 商品区块 -->
<RouterLink to="/" class="goods-item" v-for="item in hotList" :key="item.id">
<img :src="item.picture" alt="" />
<p class="name ellipsis">{{ item.name }}</p>
<p class="desc ellipsis">{{ item.desc }}</p>
<p class="price">¥{{ item.price }}</p>
</RouterLink>
</div>
效果演示:
(2)适配不同title和数据列表
1)适配标题内容:
// 适配title 1 - 24小时热榜 2-周热榜
const TYPEMAP = {
1: '24小时热榜',
2: '周热榜'
}
const title = computed(() => TYPEMAP[props.hotType])
2)页面中渲染
<div class="goods-aside">
<!-- 24小时 -->
<DetailHot :hot-type="1" />
<!-- 周 -->
<DetailHot :hot-type="2" />
</div>
浏览器返回
运行结果:
4、图片预览区
(1)小图切换大图显示
实现思路:维护一个数组图片列表,鼠标移入小图区域,记录当前小图下标值,通过下标在数组中取对应图片,显示到大图位置
1)准备静态模板(src/components/Imgeview.index.vue)
2)绑定激活状态:
<ul class="small">
<li v-for="(img, i) in imageList" :key="i" @mouseenter="enterHandler(i)" :class="{ active: i === activeIndex }">
<img :src="img" alt="" />
</li>
</ul>
定义激活项的下标:
const activeIndex = ref(0)
const enterHandler = (i) => {
activeIndex.value = i
}
左侧大盒子对应显示右侧小盒子的图片
<div class="middle" ref="target">
<img :src="imageList[activeIndex]" alt="" />
效果展示:
(2)滑块跟随鼠标移动
实现思路:首先,获取当前鼠标在盒子内的相对位置(这里是直接通过获取鼠标在浏览器窗口中的相对位置来确定的),使用到了vueuse中的一个函数useMouseInElement
1)导入函数
import { useMouseInElement } from '@vueuse/core';
2)定义目标响应式对象
const target = ref(null)
3)将elementX, elementY,从中解构出来
const { elementX, elementY, isOutside } = useMouseInElement(target)
4)编写逻辑代码
const target = ref(null)
const activeIndex = ref(0)
const positionX = ref(0)
const positionY= ref(0)
const { elementX, elementY, isOutside } = useMouseInElement(target)
const enterhandler = (i) => {
activeIndex.value = i
}
//获取鼠标相对位置i
const left=ref(0)
const top=ref(0)
watch ([elementX, elementY,isOutside],()=> {
console.log('xy变化了')
// 有效范围内控制滑块距离
//如果鼠标没有移入到盒子里面,直接不执行后面的逻辑
if(isOutside.value) return
//横向
if(elementX.value > 100 && elementX.value < 300){
left.value = elementX.value - 100
}
//纵向
if(elementY.value > 100 && elementY.value < 300){
top.value = elementY.value - 100
}
//边界
if(elementX.value > 300){
left.value = 200
}
if(elementX.value < 100)
{
left.value = 0
}
if(elementY.value > 300){
top.value = 200
}
if(elementY.value < 100)
{
top.value = 0
}
注意一下:在vue中,同样,变量必须先定义后使用:我这里刚开始就是由于变量没有定义就直接使用,导致运行的时候左右滑动的时候滑块会跟随鼠标移动,上下滑动的时候不会移动。Errlens也没有给我报错,很不理解啥原因。
简单看下运行结果吧
(3)放大镜效果
实现思路:
放大效果:大图的宽和高是小图的两倍
大图的移动方向与蒙层的移动方向相反,且数值是两倍
positionX.value = -left.value * 2
positionY.value = -top.value * 2
(3)组件props适配:与后端接通数据
1)
defineProps({
imageList:{
type:Array,
default:()=>[]
}
})
2)
<XtxImageView :image-list="goods.mainPictures" />
效果展示:
5、通用组件统一注册全局
Src\components\index.js,
1)全局化注册
// 把components中的所组件都进行全局化注册
// 通过插件的方式
import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin = {
install (app) {
// app.component('组件名字',组件配置对象)
app.component('XtxImageView', ImageView)
app.component('XtxSku', Sku)
}
}
2)main.js中注册引入
import { componentPlugin } from '@/components'
下期见~