vue3项目实战-第五章-商品详情页(商品热榜区/图片预览/全局组件统一注册)

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">&yen;{{ 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">&yen;{{ 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'

下期见~

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学英语的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值