路由配置
模板代碼
創(chuàng)建 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: '/' }">首頁(yè)</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">母嬰
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">跑步鞋
</el-breadcrumb-item>
<el-breadcrumb-item>抓絨保暖,毛毛蟲(chóng)子兒童運(yùn)動(dòng)鞋</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 商品信息 -->
<div class="info-container">
<div>
<div class="goods-info">
<div class="media">
<!-- 圖片預(yù)覽區(qū) -->
<!-- 統(tǒng)計(jì)數(shù)量 -->
<ul class="goods-sales">
<li>
<p>銷(xiāo)量人氣</p>
<p> 100+ </p>
<p><i class="iconfont icon-task-filling"></i>銷(xiāo)量人氣</p>
</li>
<li>
<p>商品評(píng)價(jià)</p>
<p>200+</p>
<p><i class="iconfont icon-comment-filling"></i>查看評(píng)價(jià)</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>品牌主頁(yè)</p>
</li>
</ul>
</div>
<div class="spec">
<!-- 商品信息區(qū) -->
<p class="g-name"> 抓絨保暖,毛毛蟲(chóng)兒童鞋 </p>
<p class="g-desc">好穿 </p>
<p class="g-price">
<span>200</span>
<span> 100</span>
</p>
<div class="g-service">
<dl>
<dt>促銷(xiāo)</dt>
<dd>12月好物放送,App領(lǐng)券購(gòu)買(mǎi)直降120元</dd>
</dl>
<dl>
<dt>服務(wù)</dt>
<dd>
<span>無(wú)憂(yōu)退貨</span>
<span>快速退款</span>
<span>免費(fèi)包郵</span>
<a href="javascript:;">了解詳情</a>
</dd>
</dl>
</div>
<!-- sku組件 -->
<!-- 數(shù)據(jù)組件 -->
<!-- 按鈕組件 -->
<div>
<el-button size="large" class="btn">
加入購(gòu)物車(chē)
</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熱榜+專(zhuān)題推薦 -->
<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 中添加對(duì)應(yīng)路由【/detail/{goodId}
】:
routes: [
{
path: '/',
component: Layout,
children: [
{
path: 'category/sub/:id',
component: SubCategory
},
{
path: "/detail/:id",
component: Detail
}
]
}
]
鏈接跳轉(zhuǎn)
對(duì) src\views\Home\components\HomeNew.vue 文件及其他涉及商品信息的頁(yè)面修改路由跳轉(zhuǎn):
<RouterLink :to="`/detail/${item.id}`">
<img v-img-lazy="item.picture" alt="" />
<p class="name">{{ item.name }}</p>
<p class="price">¥{{ item.price }}</p>
</RouterLink>
渲染基礎(chǔ)數(shù)據(jù)
封裝接口
在 src\apis\detail.js 文件中封裝接口用于獲取商品信息:
import http from "@/utils/http"
//獲取商品信息
export const getDetail = (id) => {
return http({
url: '/goods',
params: {
id
}
})
}
渲染數(shù)據(jù)
在 src\views\Detail\index.vue 文件中編寫(xiě)方法用于接收商品信息數(shù)據(jù):
<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>
修改模板代碼,渲染數(shù)據(jù):
<!-- v-if 條件渲染 -->
<div class="container" v-if="goods.details">
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首頁(yè)</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">
<!-- 統(tǒng)計(jì)數(shù)量 -->
<ul class="goods-sales">
<li>
<p>銷(xiāo)量人氣</p>
<p>{{ goods.salesCount }}+</p>
<p><i class="iconfont icon-task-filling"></i>銷(xiāo)量人氣</p>
</li>
<li>
<p>商品評(píng)價(jià)</p>
<p>{{ goods.commentCount }}+</p>
<p><i class="iconfont icon-comment-filling"></i>查看評(píng)價(jià)</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>品牌主頁(yè)</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>
熱榜區(qū)域
模板代碼
創(chuàng)建 src\views\Detail\components\DetailHot.vue 文件,將榜單的代碼粘貼進(jìn)去,方便日榜和周榜進(jìn)行復(fù)用:
<script setup>
</script>
<template>
<div class="goods-hot">
<h3>周日榜單</h3>
<!-- 商品區(qū)塊 -->
<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">¥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,獲取熱榜商品,通過(guò) type 參數(shù)進(jìn)行區(qū)分日榜和周榜數(shù)據(jù):
/**
* 獲取熱榜商品
* @param {Number} id - 商品id
* @param {Number} type - 1代表24小時(shí)熱銷(xiāo)榜 2代表周熱銷(xiāo)榜
* @param {Number} limit - 獲取個(gè)數(shù)
*/
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {
return http({
url: '/goods/hot',
params: {
id,
type,
limit
}
})
}
渲染數(shù)據(jù)
定義 props 參數(shù),接收傳入的 type 參數(shù),導(dǎo)入封裝好的方法,獲取對(duì)應(yīng) type 榜單的數(shù)據(jù):
<script setup>
import { getHotGoodsAPI } from '@/apis/detail'
import { ref, onMounted,computed } from 'vue'
import { useRoute } from 'vue-router'
// type適配不同類(lèi)型熱榜數(shù)據(jù)
const props = defineProps({
type: {
type: Number, // 1代表24小時(shí)熱銷(xiāo)榜 2代表周熱銷(xiāo)榜 3代表總熱銷(xiāo)榜 可以使用type去適配title和數(shù)據(jù)列表
default: 1
}
})
const TITLEMAP = {
1: '24小時(shí)熱榜',
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>
將獲取到的榜單商品渲染到頁(yè)面中:
<template>
<div class="goods-hot">
<h3>{{ title }}</h3>
<!-- 商品區(qū)塊 -->
<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">¥{{ item.price }}</p>
</RouterLink>
</div>
</template>
src\views\Detail\index.vue 中使用組件傳入不同的 type:
import GoodHot from '@/views/Detail/components/DetailHot.vue'
<!-- 24熱榜+專(zhuān)題推薦 -->
<div class="goods-aside">
<!-- 24小時(shí)熱榜 -->
<GoodHot :type="1" />
<!-- 周熱榜 -->
<GoodHot :type="2" />
</div>
圖片預(yù)覽組件封裝
小圖切換大圖顯示
思路:維護(hù)一個(gè)數(shù)組圖片列表,鼠標(biāo)劃入小圖記錄當(dāng)前小圖下標(biāo)值,通過(guò)下標(biāo)值在數(shù)組中取對(duì)應(yīng)圖片,顯示到大圖位置。
模版代碼
創(chuàng)建 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">
<!-- 左側(cè)大圖-->
<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 將來(lái)控制背景圖的移動(dòng)來(lái)實(shí)現(xiàn)放大的效果查看 background-position
background-size: 800px 800px;
background-color: #f8f8f8;
}
.layer {
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.2);
// 絕對(duì)定位 然后跟隨咱們鼠標(biāo)控制left和top屬性就可以讓滑塊移動(dòng)起來(lái)
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>
綁定事件
為小圖綁定事件,記錄當(dāng)前激活下標(biāo)值,通過(guò)下標(biāo)切換大圖顯示:
//記錄激活下標(biāo)
const activeIndex = ref(0)
//鼠標(biāo)劃過(guò)事件
const enterhandler=(i)=>{
activeIndex.value=i
}
修改模板代碼,通過(guò)判斷圖片下標(biāo)與當(dāng)前激活圖片的下標(biāo)是否相等來(lái)添加激活樣式:
<!-- 左側(cè)大圖-->
<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()
獲取到當(dāng)前的鼠標(biāo)在盒子內(nèi)的相對(duì)位置,控制滑塊跟隨鼠標(biāo)移動(dòng)(left/top):
-
有效移動(dòng)范圍內(nèi)的計(jì)算邏輯:
橫向: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=)
-
src\components\ImageView\index.vue 中導(dǎo)入 useMouseInElement 組件:
import { ref,watch } from 'vue' import { useMouseInElement } from '@vueuse/core'
-
處理邏輯如下:
// 2. 獲取鼠標(biāo)相對(duì)位置 const target = ref(null) const { elementX, elementY, isOutside } = useMouseInElement(target) // 3. 控制滑塊跟隨鼠標(biāo)移動(dòng)(監(jiān)聽(tīng)elementX/Y變化,一旦變化 重新設(shè)置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變化了') // 如果鼠標(biāo)沒(méi)有移入到盒子里面 直接不執(zhí)行后面的邏輯 if (isOutside.value) return console.log('后續(xù)邏輯執(zhí)行了') // 有效范圍內(nèi)控制滑塊距離 // 橫向 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 * 2 positionY.value = -top.value * 2 }
-
修改模板代碼調(diào)用
<!-- 左側(cè)大圖--> <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>
圖片優(yōu)化
在 src\components\ImageView\index.vue 中定義 props 參數(shù),接收?qǐng)D片列表:
// 圖片列表
defineProps({
imageList: {
type: Array,
default: () => []
}
})
修改 src\views\Detail\index.vue 中圖片預(yù)覽部分:
<!-- 圖片預(yù)覽區(qū) -->
<ImageView :image-list="goods.mainPictures"/>
SKU組件熟悉
SKU:存貨單位(英語(yǔ):stock keeping unit,SKU/,es,keju:/),也翻譯為庫(kù)存單元,是一個(gè)會(huì)計(jì)學(xué)名詞,定義為庫(kù)存管理
中的最小可用單元,例如紡織品中一個(gè)SKU通常表示規(guī)格、顏色、款式,而在連鎖零售門(mén)店中有時(shí)稱(chēng)單品為一個(gè) SKU。
SKU組件的作用:產(chǎn)出當(dāng)前用戶(hù)選擇的商品規(guī)格,為加入購(gòu)物車(chē)操作提供數(shù)據(jù)信息。
-
導(dǎo)入 src\components\XtxSku 組件:
-
在 src\views\Detail\index.vue 中引入 XtxSku 組件:
import XtxSku from '@/components/XtxSku/index.vue'
-
在 Html 代碼中插入組件:
<!-- sku組件 --> <XtxSku :goods="goods"/>
全局組件統(tǒng)一插件化
背景:components 目錄下有可能還會(huì)有很多其他通用型組件,有可能在多個(gè)業(yè)務(wù)模塊中共享,所有統(tǒng)一進(jìn)行全局組件注冊(cè)比較好。
插件化開(kāi)發(fā)
新建 src\components\index.js 文件,在其中進(jìn)行封裝 components 目錄下的所有組件:
// 把components中的所組件都進(jìn)行全局化注冊(cè)
// 通過(guò)插件的方式
import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin = {
install (app) {
// app.component('組件名字',組件配置對(duì)象)
app.component('XtxImageView', ImageView)
app.component('XtxSku', Sku)
}
}
插件注冊(cè)
在 main.js 文件中進(jìn)行注冊(cè)插件即可:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-498400.html
// 引入全局組件插件
import { componentPlugin } from '@/components'
app.use(componentPlugin)
修改 src\views\Detail\index.vue 中的代碼,替換插件的方式:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-498400.html
<!-- 圖片預(yù)覽區(qū) -->
<XtxImageView :image-list="goods.mainPictures"/>
<!-- sku組件 -->
<XtxSku :goods="goods"/>
到了這里,關(guān)于黑馬程序員前端 Vue3 小兔鮮電商項(xiàng)目——(七)詳情頁(yè)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!