黑马程序员前端 Vue3 小兔鲜电商项目——(七)详情页

news/2024/11/25 17:59:46/

文章目录

    • 路由配置
      • 模板代码
      • 配置路由
      • 链接跳转
    • 渲染基础数据
      • 封装接口
      • 渲染数据
    • 热榜区域
      • 模板代码
      • 封装接口
      • 渲染数据
    • 图片预览组件封装
      • 小图切换大图显示
        • 模版代码
        • 绑定事件
      • 放大镜效果
      • 图片优化
    • SKU组件熟悉
    • 全局组件统一插件化
      • 插件化开发
      • 插件注册

image-20230623225107221

路由配置

模板代码

创建 src\views\Detail\index.vue 文件,添加以下代码:

<script setup></script><template><div class="xtx-goods-page"><div class="container"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: '/' }">母婴</el-breadcrumb-item><el-breadcrumb-item :to="{ path: '/' }">跑步鞋</el-breadcrumb-item><el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 图片预览区 --><!-- 统计数量 --><ul class="goods-sales"><li><p>销量人气</p><p> 100+ </p><p><i class="iconfont icon-task-filling"></i>销量人气</p></li><li><p>商品评价</p><p>200+</p><p><i class="iconfont icon-comment-filling"></i>查看评价</p></li><li><p>收藏人气</p><p>300+</p><p><i class="iconfont icon-favorite-filling"></i>收藏商品</p></li><li><p>品牌信息</p><p>400+</p><p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p></li></ul></div><div class="spec"><!-- 商品信息区 --><p class="g-name"> 抓绒保暖,毛毛虫儿童鞋 </p><p class="g-desc">好穿 </p><p class="g-price"><span>200</span><span> 100</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组件 --><!-- 数据组件 --><!-- 按钮组件 --><div><el-button size="large" class="btn">加入购物车</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 3" :key="item.value"><span class="dt">白色</span><span class="dd">纯棉</span></li></ul><!-- 图片 --></div></div></div><!-- 24热榜+专题推荐 --><div class="goods-aside"></div></div></div></div></div></div>
</template><style scoped lang='scss'>
.xtx-goods-page {.goods-info {min-height: 600px;background: #fff;display: flex;.media {width: 580px;height: 600px;padding: 30px 50px;}.spec {flex: 1;padding: 30px 30px 30px 0;}}.goods-footer {display: flex;margin-top: 20px;.goods-article {width: 940px;margin-right: 20px;}.goods-aside {width: 280px;min-height: 1000px;}}.goods-tabs {min-height: 600px;background: #fff;}.goods-warn {min-height: 600px;background: #fff;margin-top: 20px;}.number-box {display: flex;align-items: center;.label {width: 60px;color: #999;padding-left: 10px;}}.g-name {font-size: 22px;}.g-desc {color: #999;margin-top: 10px;}.g-price {margin-top: 10px;span {&::before {content: "¥";font-size: 14px;}&:first-child {color: $priceColor;margin-right: 10px;font-size: 22px;}&:last-child {color: #999;text-decoration: line-through;font-size: 16px;}}}.g-service {background: #f5f5f5;width: 500px;padding: 20px 10px 0 10px;margin-top: 10px;dl {padding-bottom: 20px;display: flex;align-items: center;dt {width: 50px;color: #999;}dd {color: #666;&:last-child {span {margin-right: 10px;&::before {content: "•";color: $xtxColor;margin-right: 2px;}}a {color: $xtxColor;}}}}}.goods-sales {display: flex;width: 400px;align-items: center;text-align: center;height: 140px;li {flex: 1;position: relative;~li::after {position: absolute;top: 10px;left: 0;height: 60px;border-left: 1px solid #e4e4e4;content: "";}p {&:first-child {color: #999;}&:nth-child(2) {color: $priceColor;margin-top: 10px;}&:last-child {color: #666;margin-top: 10px;i {color: $xtxColor;font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;cursor: pointer;}}}}}
}.goods-tabs {min-height: 600px;background: #fff;nav {height: 70px;line-height: 70px;display: flex;border-bottom: 1px solid #f5f5f5;a {padding: 0 40px;font-size: 18px;position: relative;>span {color: $priceColor;font-size: 16px;margin-left: 10px;}}}
}.goods-detail {padding: 40px;.attrs {display: flex;flex-wrap: wrap;margin-bottom: 30px;li {display: flex;margin-bottom: 10px;width: 50%;.dt {width: 100px;color: #999;}.dd {flex: 1;color: #666;}}}>img {width: 100%;}
}.btn {margin-top: 20px;}.bread-container {padding: 25px 0;
}
</style>

配置路由

在 src\router\index.js 中添加对应路由【/detail/{goodId}】:

routes: [{path: '/',component: Layout,children: [{path: 'category/sub/:id',component: SubCategory},{path: "/detail/:id",component: Detail}]}
]

链接跳转

对 src\views\Home\components\HomeNew.vue 文件及其他涉及商品信息的页面修改路由跳转:

<RouterLink :to="`/detail/${item.id}`"><img v-img-lazy="item.picture" alt="" /><p class="name">{{ item.name }}</p><p class="price">&yen;{{ item.price }}</p>
</RouterLink>

渲染基础数据

封装接口

在 src\apis\detail.js 文件中封装接口用于获取商品信息:

import http from "@/utils/http"//获取商品信息
export const getDetail = (id) => {return http({url: '/goods',params: {id}})
}

渲染数据

在 src\views\Detail\index.vue 文件中编写方法用于接收商品信息数据:

<script setup>
import { getDetail } from '@/apis/detail';import { ref, onMounted } from 'vue'
import { useRoute } from "vue-router";const route = useRoute()
const goods = ref({})
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}onMounted(() => {getGoods()
})
</script>

修改模板代码,渲染数据:

<!-- v-if 条件渲染 -->
<div class="container" v-if="goods.details"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><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><el-breadcrumb-item>{{ goods.name }}</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 统计数量 --><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><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" v-img-lazy="img" :key="img" alt="" /></div></div></div></div></div></div>
</div>

热榜区域

模板代码

创建 src\views\Detail\components\DetailHot.vue 文件,将榜单的代码粘贴进去,方便日榜和周榜进行复用:

<script setup></script><template>
<div class="goods-hot"><h3>周日榜单</h3><!-- 商品区块 --><RouterLink to="/" class="goods-item" v-for="item in 3" :key="item.id"><img :src="item.picture" alt="" /><p class="name ellipsis">一双男鞋</p><p class="desc ellipsis">一双好穿的男鞋</p><p class="price">&yen;200.00</p></RouterLink></div>
</template><style scoped lang="scss">.goods-hot {h3 {height: 70px;background: $helpColor;color: #fff;font-size: 18px;line-height: 70px;padding-left: 25px;margin-bottom: 10px;font-weight: normal;}.goods-item {display: block;padding: 20px 30px;text-align: center;background: #fff;width: 280px;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>

封装接口

在 src\apis\detail.js 文件中,封装 API,获取热榜商品,通过 type 参数进行区分日榜和周榜数据:

/*** 获取热榜商品* @param {Number} id - 商品id* @param {Number} type - 1代表24小时热销榜 2代表周热销榜* @param {Number} limit - 获取个数*/
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {return http({url: '/goods/hot',params: {id,type,limit}})
}

渲染数据

定义 props 参数,接收传入的 type 参数,导入封装好的方法,获取对应 type 榜单的数据:

<script setup>
import { getHotGoodsAPI } from '@/apis/detail'
import { ref, onMounted,computed } from 'vue'
import { useRoute } from 'vue-router'// type适配不同类型热榜数据
const props = defineProps({type: {type: Number, // 1代表24小时热销榜 2代表周热销榜 3代表总热销榜 可以使用type去适配title和数据列表default: 1}
})const TITLEMAP = {1: '24小时热榜',2: '周热榜',
}
const title = computed(() => TITLEMAP[props.type])const route = useRoute()
const hotList = ref([])const getHostList = async () => {const res = await getHotGoodsAPI({id: route.params.id,type: props.type})hotList.value = res.result
}
onMounted(() => {getHostList()
})
</script>

将获取到的榜单商品渲染到页面中:

<template><div class="goods-hot"><h3>{{ title }}</h3><!-- 商品区块 --><RouterLink :to="`/detail/${item.id}`" 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">&yen;{{ item.price }}</p></RouterLink></div>
</template>

src\views\Detail\index.vue 中使用组件传入不同的 type:

import GoodHot from '@/views/Detail/components/DetailHot.vue'<!-- 24热榜+专题推荐 -->
<div class="goods-aside"><!-- 24小时热榜 --><GoodHot :type="1" /><!-- 周热榜 --><GoodHot :type="2" />
</div>

图片预览组件封装

小图切换大图显示

思路:维护一个数组图片列表,鼠标划入小图记录当前小图下标值,通过下标值在数组中取对应图片,显示到大图位置。

模版代码

创建 src\components\ImageView\index.vue 文件,添加代码:

<script setup>
// 图片列表
const imageList = ["https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png","https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg","https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg","https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg","https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
]
</script><template><div class="goods-image"><!-- 左侧大图--><div class="middle" ref="target"><img :src="imageList[0]" alt="" /><!-- 蒙层小滑块 --><div class="layer" :style="{ left: `0px`, top: `0px` }"></div></div><!-- 小图列表 --><ul class="small"><li v-for="(img, i) in imageList" :key="i"><img :src="img" alt="" /></li></ul><!-- 放大镜大图 --><div class="large" :style="[{backgroundImage: `url(${imageList[0]})`,backgroundPositionX: `0px`,backgroundPositionY: `0px`,},]" v-show="false"></div></div>
</template><style scoped lang="scss">
.goods-image {width: 480px;height: 400px;position: relative;display: flex;.middle {width: 400px;height: 400px;background: #f5f5f5;}.large {position: absolute;top: 0;left: 412px;width: 400px;height: 400px;z-index: 500;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);background-repeat: no-repeat;// 背景图:盒子的大小 = 2:1  将来控制背景图的移动来实现放大的效果查看 background-positionbackground-size: 800px 800px;background-color: #f8f8f8;}.layer {width: 200px;height: 200px;background: rgba(0, 0, 0, 0.2);// 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来left: 0;top: 0;position: absolute;}.small {width: 80px;li {width: 68px;height: 68px;margin-left: 12px;margin-bottom: 15px;cursor: pointer;&:hover,&.active {border: 2px solid $xtxColor;}}}
}
</style>

绑定事件

为小图绑定事件,记录当前激活下标值,通过下标切换大图显示:

//记录激活下标
const activeIndex = ref(0)
//鼠标划过事件
const enterhandler=(i)=>{activeIndex.value=i
}

修改模板代码,通过判断图片下标与当前激活图片的下标是否相等来添加激活样式:

<!-- 左侧大图-->
<div class="middle" ref="target"><img :src="imageList[activeIndex]" alt="" /><!-- 蒙层小滑块 --><div class="layer" :style="{ left: `0px`, top: `0px` }"></div>
</div>
<!-- 小图列表 -->
<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>

放大镜效果

使用 VueUse 提供的 useMouselnElement() 获取到当前的鼠标在盒子内的相对位置,控制滑块跟随鼠标移动(left/top):

  • 有效移动范围内的计算逻辑:

    横向:100<elementX<300,left=elementX-小滑块宽度一半

    纵向:100<elementY<300,top=elementY-小滑块高度一半

  • 边界距离控制

    横向:elementX>300(left=200) elementX<100(left=0)

    纵向:elementY>300(top=200) elementY<100(top=0=)

  1. src\components\ImageView\index.vue 中导入 useMouseInElement 组件:

    import { ref,watch } from 'vue'
    import { useMouseInElement } from '@vueuse/core'
    
  2. 处理逻辑如下:

    // 2. 获取鼠标相对位置
    const target = ref(null)
    const { elementX, elementY, isOutside } = useMouseInElement(target)// 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
    const left = ref(0)
    const top = ref(0)const positionX = ref(0)
    const positionY = ref(0)watch([elementX, elementY, isOutside], () => {console.log('xy变化了')// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑if (isOutside.value) returnconsole.log('后续逻辑执行了')// 有效范围内控制滑块距离// 横向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 }// 控制大图的显示positionX.value = -left.value * 2positionY.value = -top.value * 2
    }
    
  3. 修改模板代码调用

    <!-- 左侧大图-->
    <div class="middle" ref="target"><img :src="imageList[activeIndex]" alt="" /><!-- 蒙层小滑块 --><div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div>
    </div>
    ...
    <!-- 放大镜大图 -->
    <div class="large" :style="[{backgroundImage: `url(${imageList[activeIndex]})`,backgroundPositionX: `${positionX}px`,backgroundPositionY: `${positionY}px`,},]" v-show="!isOutside"></div>
    

图片优化

在 src\components\ImageView\index.vue 中定义 props 参数,接收图片列表:

// 图片列表
defineProps({imageList: {type: Array,default: () => []}
})

修改 src\views\Detail\index.vue 中图片预览部分:

<!-- 图片预览区 -->
<ImageView :image-list="goods.mainPictures"/>

SKU组件熟悉

SKU:存货单位(英语:stock keeping unit,SKU/,es,keju:/),也翻译为库存单元,是一个会计学名词,定义为库存管理
中的最小可用单元,例如纺织品中一个SKU通常表示规格、颜色、款式,而在连锁零售门店中有时称单品为一个 SKU。

SKU组件的作用:产出当前用户选择的商品规格,为加入购物车操作提供数据信息。

  1. 导入 src\components\XtxSku 组件:

    image-20230623175246010

  2. 在 src\views\Detail\index.vue 中引入 XtxSku 组件:

    import XtxSku from '@/components/XtxSku/index.vue'
    
  3. 在 Html 代码中插入组件:

    <!-- sku组件 -->
    <XtxSku :goods="goods"/>
    

全局组件统一插件化

背景:components 目录下有可能还会有很多其他通用型组件,有可能在多个业务模块中共享,所有统一进行全局组件注册比较好。

插件化开发

新建 src\components\index.js 文件,在其中进行封装 components 目录下的所有组件:

// 把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)}
}

插件注册

在 main.js 文件中进行注册插件即可:

// 引入全局组件插件
import { componentPlugin } from '@/components'app.use(componentPlugin)

修改 src\views\Detail\index.vue 中的代码,替换插件的方式:

<!-- 图片预览区 -->
<XtxImageView :image-list="goods.mainPictures"/><!-- sku组件 -->
<XtxSku :goods="goods"/>

http://www.ppmy.cn/news/539435.html

相关文章

海尔“小微”怎么玩

http://companies.caixin.com/2014-07-21/100706854.html

C语言 猜神童年龄

题目内容&#xff1a; 美国数学家维纳&#xff08;N.Wiener&#xff09;智力早熟&#xff0c;11岁就上了大学。他曾在1935~1936年应邀来中国清华大学讲学。一次&#xff0c;他参加某个重要会议&#xff0c;年轻的脸孔引人注目。于是有人询问他的年龄&#xff0c;他回答说&…

古代神童

PPT下载&#xff1a; http://img1.51cto.com/attachment/201205/1246491_1337385297.ppt 提纲 春秋 - 项橐 战国 - 甘罗 东汉 - 曹冲 东汉 - 周不疑 东汉 - 孔融 西晋 - 王戎 唐初 - 骆宾王 唐初 - 王勃 中唐 - 李贺 北宋 - 方仲永 北宋 - 司马光 北宋 - 汪洙 明初 - 解缙 明末…

猜神童年龄

/* 猜神童年龄 题目内容&#xff1a; 美国数学家维纳&#xff08;N.Wiener&#xff09;智力早熟&#xff0c;11岁就上了大学。他曾在1935~1936年应邀来中国清华大学讲学。一次&#xff0c;他参加某个重要会议&#xff0c;年轻的脸孔引人注目。于是有人询问他的年龄&#xff0c;…

小神童·哈密尔顿解决分配问题

目录 前言一、问题提出二、汉密尔顿方法三、方法实现&#xff1a;四、具体问题分析并解决&#xff1a;总结 前言 简要介绍哈密尔顿 哈密尔顿自幼聪明&#xff0c;被称为神童。他15岁开始对数学产生浓厚的兴趣。在对复数长期研究的基础上&#xff0c;他于1843年正式提出四元数…

设计模式之命令模式笔记

设计模式之命令模式笔记 说明Command(命令)目录命令模式示例类图订单类厨师类抽象命令类订单命令类服务员类测试类 说明 记录下学习设计模式-命令模式的写法。JDK使用版本为1.8版本。 Command(命令) 意图:将一个请求封装为一个对象&#xff0c;从而使得可以用不同的请求对客…

性能测试的具体流程

以下是一个基本的性能测试过程&#xff0c;旨在帮助了解性能测试的具体流程和步骤。 1. 确定性能测试目标及指标 首先&#xff0c;需要确定性能测试的目标和指标&#xff0c;包括响应时间、吞吐量、并发用户数等方面。这些指标应该根据业务需求和用户场景进行设定&#xff0c…