5.goods商品列表页开发

goods组件显示的区域是固定的,也没有滚动条,所以是采用绝对布局的,左右分为menu栏和foods栏,左边固定布局,右边自适应布局,采用flex布局。

写CSS样式的时候,尽量用class,少用tag,因为class的查找效率优于tag,尤其是嵌套层级比较多的时候。左边的menu栏有些是一行有些是多行,要做到不管单行还是多行都是垂直是居中,可以用display:table,table布局是实现垂直居中的有效布局。menu-item设置display:table,text设置display:table-cell,vertical-align:center

<ul>
  <li v-for="(item, index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index, $event)">
    <span class="text border-1px">
  <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>
  {{item.name}}
    </span>
  </li>
</ul>

右边的food栏用两个<ul v-for="XXX">来实现遍历

每个食品的显示分为左边icon和右边的content,用flex布局来实现,左边固定宽度,右边自适应布局。goods组件的右边的food栏,除了最后一个食品的下面都有1px的border,要去掉最后一个食品的border,课程中是在mixin中写了一个border-none()的样式,把after的伪类设置为display:none。也可以用另一种方式来实现,由于border是通过after伪类来实现的,那么如果给最后一个食品的after伪类设置为border:none应该就可以去掉border了。

border-none()
  &:after
    display: none

food-item设置了18px的margin,但是由于相邻两个food-item的上下margin是会重合的,所以要再设置一个padding-bottom,但是最后一个food-item就会有一个18px的margin-bottom和18px的padding-bottom,所以最后一个fodd-tiem的margin-bottom要设置为0。

.food-item
  display: flex
  margin: 18px
  padding-bottom: 18px
  border-1px(rgba(7, 17, 27, 0.1))

用better scroll库实现滚动效果, new BScroll()有两个参数,第一个是dom对象,第二个是可选对象。给dom对象增加ref属性,<div class="foods-wrapper" ref="menuWrapper">,在js中用this.$refs获取dom对象,this.meunScroll = new BScroll(this.$refs.menuWrapper, {});

DOM对象是异步更新的,所以要把滚动的初始化函数放在¥nextTick()函数中,$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM。

要实现左边的menu和右边的food的连动,要计算出右边每一个类的food的高度区间,用getElementsByClassNames获取每一个类food的DOM对象,用函数clientHeight得到的DOM对象的高度。

_calculateHeight() {
  let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
  let height = 0;
  this.listHeight.push(height);
  for (let i = 0; i < foodList.length; i++) {
    let item = foodList[i];
    height += item.clientHeight;
    this.listHeight.push(height);
  }

}

然后拿到实时的y值与高度区间值对比,在BScroll函数中传入参数probeType:3就可以获取实时滚动的位置,用this.foodsscroll.on()监听滚动事件

this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
  click: true,
  probeType: 3
});
this.foodsScroll.on('scroll', (pos) => {
  this.scrollY = Math.abs(Math.round(pos.y));
  });
},

当food栏滚动时,获取当前的坐标,与区间对比,得到对应的index,更新左边的menu栏,对应index的menu-item会获得一个current的class。用better Scroll之后,点击时会有两个事件,一个是浏览器原生事件,一个是better Scroll派发的时间,其中浏览器原生事件都没有_constructed属性,better scroll派发的事件有_constructed属性。

<li v-for="(item, index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index, $event)">

computed: {
  currentIndex() {
    for (let i = 0; i < this.listHeight.length; i++) {
      let height1 = this.listHeight[i];
      let height2 = this.listHeight[i + 1];
      if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
        return i;
      }
    }
    return 0;
  }
}

点击左边的menu列表时,根据index,通过scrollToElement把右边的列表滚动到对应的位置

selectMenu(index, event) {
  if (!event._constructed) {
    return;
  }
  let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
  let el = foodList[index];
  this.foodsScroll.scrollToElement(el, 300);
},

购物车组件开发

购物车组件fixed布局,由于购物车是一直显示的所以要之一z-index的设置。购物车也可以分为左右两部分用flex布局,左边固定宽度,右边自适应。左边包括logo,price和desc,用display:inline-block

.shopcart
  position: fixed
  left: 0
  bottom: 0
  z-index: 50
  width: 100%
  height: 48px
  background: #000

购物车的logo-wrapper是一个超出购物车一部分的圆形,用relative布局和负的top值实现超出购物车,圆形可通过相同的width和height和border-radius:50%实现。其中用到了box-sizing: border-box,这是指用IE下的盒子模型,这种方法在移动端布局比较常用,可以省去计算width。price的右边有border,设置line-height:24px和maring-top: 12px,这样子能使border的高度是24px. 在App组件中,seller数据要在router-view中传递过来<router-view :seller="seller"></router-view>。

购物车的状态是由选择的商品决定的,购物车要根据goods组件选择的商品来改变样式,selectFoods是一个数组元素,保存了选择的商品种类和数量,在props中如果数据时数组或者对象,default是一个函数。

selectFoods: {
  type: Array,
  default() {
    return [
      {
        price: 10,
        count: 10
      }
   ];
}

v-show可以等于一个表达式,当表达式是true的时候,就显示DOM元素<div class="num" v-show="totalCount>0">{{totalCount}}</div>

cartcontrol组件

cartcontrol从父组件接收数据food,<cartcontrol @add="addFood" :food="food"></cartcontrol>

为了美观,按钮很小,为了方便增加有效的点击区域便于用户点击操作,可以给cart-decrase,cart-increase增加padding。

要为food添加一个count属性时,不能直接添加,要用Vue的set方法

addCart(event) {
  if (!event._constructed) {
    return;
  }
  if (!this.food.count) {
    Vue.set(this.food, 'count', 1);
  } else {
    this.food.count++;
  }
  this.$emit('add', event.target);
},

个减号出现时,添加一个滚动出现的动画,这个动画包含了平移和滚动,所以要有两个层,外层实现平移,内层实现滚动。

<transition name="move">
  <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
    <span class="inner icon-remove_circle_outline"></span>
  </div>
</transition>

.cart-decrease
  display: inline-block
  padding: 6px
  opacity: 1
  transform: translate3d(0, 0, 0)
  .inner
    display: inline-block
    line-height: 24px
    font-size: 24px
    color: rgb(0, 160, 220)
    transition: all 0.4s linear
    transform: rotate(0)
  &.move-enter-active, &.move-leave-active
    transition: all 0.4s linear
 &.move-enter, &.move-leave-to
   opacity: 0
   transform: translate3d(24px, 0, 0)
   .inner
     transform: rotate(180deg)

计算属性selectFoods通过两个forEach来遍历goods,把count大于0的food push到数组中,返回这个数组,这样子就拿到了和shopcart状态显示有关的的selectFoods数组

 小球落入购物车的动画,用一个balls数组来控制小球的显示,一共有5个小球。小球是相对于视口做动画的,所以用fixed布局,小球在屏幕上方,z-index要大于shopcart的z-index。有水平方向和竖直方向的动画,所以也需要内外两层来实现两个方向上的动画

.ball-container
  .ball
    position: fixed
    left: 32px
    bottom: 22px
    z-index: 200
    transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
    .inner
      width: 16px
      height: 16px
      border-radius: 50%
      background: rgb(0, 160, 220)
      transition: all 0.4s linear

这个动画中起始点的位置需要动态计算,要获取加按钮的相对于屏幕的位置。这个动画不是通过设置enter和leave实现的。点击加号时,向父组件goods发送一个add事件,把这个dom元素传递给父组件this.$emit('add', event.target);在父组件goods中对这个事件做处理,获取加号DOM对象,<cartcontrol @add="addFood" :food="food"></cartcontrol>

addFood(target) {
  this._drop(target);
},
_drop(target) {
  // 体验优化,异步执行下落动画
  this.$nextTick(() => {
    this.$refs.shopcart.drop(target);
});

在shopcart组件中获取加号的DOM。在drop函数中遍历balls数组,取出第一个不可见的小球,设为可见,放入dropBalls数组中。用钩子函数before-enter,enter和after-enter实现动画效果。

<transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop">

在beforeDrop函数中用getBoundingClientRect()函数获取加号DOM元素相对于视口的位置(这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离),计算出小球在x方向和y方向需要移动的距离,设置内外两层小球的动画

beforeDrop(el) {
  let count = this.balls.length;
  while (count--) {
    let ball = this.balls[count];
    if (ball.show) {
      let rect = ball.el.getBoundingClientRect();
      let x = rect.left - 32;
      let y = -(window.innerHeight - rect.top - 22);
      ball.el.style.display = '';
      el.style.webkitTransform = `translate3d(0, ${y}px, 0)`;
      el.style.transform = `translate3d(0, ${y}px, 0)`;
      let inner = el.getElementsByClassName('inner-hook')[0];
      inner.style.webkitTransform = `translate3d(${x}px, 0, 0)`;
      inner.style.transform = `translate3d(${x}px, 0, 0)`;
    }
  }
}

在dropping(el,done)函数中用let rf = el.offsetHeight;手动触发重绘

dropping(el, done) {
  /* eslint-disable no-unused-vars */
  let rf = el.offsetHeight;
  this.$nextTick(() => {
    el.style.webkitTransform = 'translate3d(0, 0, 0)';
    el.style.transform = 'translate3d(0, 0, 0)';
    let inner = el.getElementsByClassName('inner-hook')[0];
    inner.style.webkitTransform = 'translate3d(0, 0, 0)';
    inner.style.transform = 'translate3d(0, 0, 0)';
    el.addEventListener('transitionend', done);
  });
},

在afterDrop函数中重新把ball设置为不可见

afterDrop(el) {
  console.log(el);
  let ball = this.dropBalls.shift();
  if (ball) {
    ball.show = false;
    el.style.display = 'none';
  }
}

购物车详情页

购物车详情页由两部分组成list-header和list-content。shopcart-list用局对布局,注意高度的设置,高度的设置是在动画中设置为-100%,实现购物车详情页从底部向上显示出来的一个效果。

.shopcart-list
  position: absolute;
  left: 0
  top: 0
  z-index: -1
  width: 100%
  transform: translate3d(0, -100%, 0)
  &.fold-enter-active, &.fold-leave-active
    transition: all 0.5s
  &.fold-enter, &.fold-leave-to
    transform: translate3d(0, 0, 0)

标题栏的左右两边的title和empty采用float布局

food中的price和cartcontrol-wrapper都使用绝对定位

 

用计算属性listShow控制购物车详情页是否可见。

listShow() {
  if (!this.totalCount) {
    this.fold = true;
    return false;
  }
  let show = !this.fold;
  if (show) {
    this.$nextTick(() => {
      if (!this.scroll) {
        this.scroll = new BScroll(this.$refs.listContent, {
          click: true
        });
      } else {
        this.scroll.refresh();
      }
  });
  }
  return show;
}
给list-content添加滚动的效果,这部分内容也写在计算属性listShow中,如果scroll已经存在,则用refresh()进行刷新.

最后给empty按钮增加清空事件<span class="empty" @click="empty">清空</span>

empty()对selectFoods进行遍历,把count都置为0.

empty() {
  this.selectFoods.forEach((food) => {
    food.count = 0;
  });
}

背景list-mask和shopcart同级,也由listShow控制显示,采用fixed布局,z-index要比shopcart的z-index小。

.list-mask
  position: fixed
  top: 0
  left: 0
  width: 100%
  height: 100%
  z-index: 40
  backdrop-filter: blur(10px)
  background: rgba(7, 17, 27, 0.6)
  &.fade-enter-active, &.fade-leave-active
    transition: all 0.5s
  &.fade-enter, &.fade-leave-to
    background: rgba(7, 17, 27, 0)

pay按钮增加点击结算事件,这个点击要阻止冒泡(stop)和默认事件(prevent),不然除了结算窗口还会弹出购物车详情页。

<div class="content-right" @click.stop.prevent="pay">

转载于:https://www.cnblogs.com/dingzibetter/p/7267094.html

<think>我们正在构建一个Vue项目中的商品管理功能,需要初始化商品列表数据。根据引用的内容,我们可以总结出以下几个关键步骤:1.**状态管理**:使用Pinia或Vuex进行状态管理(引用[1]中提到stores目录,引用[2]中使用methods和data)2.**组件结构**:商品列表页面(例如ProductList.vue)以及商品卡片组件(ProductCard.vue)等(引用[1])3.**数据获取**:在生命周期钩子(如created或mounted)中调用获取商品列表的方法(引用[2])4.**API请求**:使用axios或其他HTTP库从后端API获取数据(引用[2]和引用[4])5.**数据处理**:将获取到的数据赋值给状态管理中的状态或组件的数据属性(引用[2])根据这些,我们可以在商品列表页面(例如ProductList.vue)中实现初始化商品列表数据。下面是一种常见的实现方式:###步骤1:安装并配置HTTP请求库(如axios)在项目中,我们通常使用axios来发送HTTP请求。首先确保已经安装axios:```bashnpminstallaxios```然后在入口文件(如src/main.js)中全局引入axios,或者根据需求在需要的地方引入。###步骤2:创建状态管理(以Pinia为例,因为Vue3推荐使用Pinia)首先安装Pinia:```bashnpminstallpinia```在main.js中设置Pinia:```javascriptimport{createApp}from'vue'import{createPinia}from'pinia'importAppfrom'./App.vue'constapp=createApp(App)app.use(createPinia())app.mount('#app')```创建商品状态管理文件(src/stores/productStore.js):```javascriptimport{defineStore}from'pinia'importaxiosfrom'axios'exportconstuseProductStore=defineStore('product',{state:()=>({goodslist:[],//商品列表total:0//商品总数}),actions:{//获取商品列表asyncgetGoodsList(queryInfo){try{constres=awaitaxios.get('goods',{params:queryInfo})if(res.data.meta.status===200){this.goodslist=res.data.data.goodsthis.total=res.data.data.total}else{thrownewError('获取商品列表失败')}}catch(error){console.error(error)//这里可以处理错误,例如显示错误消息}}}})```###步骤3:在商品列表页面(ProductList.vue)中初始化商品列表在商品列表页面,我们可以在组件的setup函数中使用状态管理并调用获取商品列表的方法。```vue<template><div><!--商品列表展示--><ul><liv-for="productinproductStore.goodslist":key="product.id">{{product.name}}</li></ul><!--分页组件等--></div></template><script>import{useProductStore}from'@/stores/productStore'import{onMounted}from'vue'exportdefault{setup(){constproductStore=useProductStore()//初始化商品列表onMounted(()=>{//这里可以传入查询参数,例如分页信息等constqueryInfo={pagenum:1,//当前页码pagesize:10//每页显示条数}productStore.getGoodsList(queryInfo)})return{productStore}}}</script>```###说明:-在`onMounted`生命周期钩子中调用`getGoodsList`方法,确保组件挂载后立即获取数据。-状态管理使用了Pinia,将数据保存在store中,这样可以在多个组件中共享。-请求的URL为`'goods'`,这需要根据实际的后端API进行调整。-查询参数`queryInfo`根据实际需求传递,例如包含分页、排序、筛选等信息。###备选方案(不使用状态管理)如果项目较小,也可以直接在组件中定义数据和方法:```vue<script>importaxiosfrom'axios'exportdefault{data(){return{goodslist:[],total:0}},created(){this.getGoodsList()},methods:{asyncgetGoodsList(){constqueryInfo={pagenum:1,pagesize:10}try{constres=awaitaxios.get('goods',{params:queryInfo})if(res.data.meta.status===200){this.goodslist=res.data.data.goodsthis.total=res.data.data.total}else{console.error('获取商品列表失败')}}catch(error){console.error(error)}}}}</script>```###相关问题:1.如何在Vue项目中处理分页?2.商品管理中的筛选和排序如何实现?3.如何优化商品列表的加载性能?4.如何在后端接口未完成时显示加载状态?</think> 在 Vue 项目中初始化商品列表数据,可以按照以下步骤实现,结合引用内容中的最佳实践: ### 步骤 1:创建商品状态管理(使用 Pinia) ```javascript // src/stores/productStore.js import { defineStore } from 'pinia' import { ref } from 'vue' import axios from 'axios' export const useProductStore = defineStore('product', () => { // 商品列表数据 const productList = ref([]) // 分页信息 const pagination = ref({ currentPage: 1, pageSize: 10, total: 0 }) // 初始化商品列表 const initProductList = async (params = {}) => { try { const { data } = await axios.get('/api/products', { params: { page: pagination.value.currentPage, size: pagination.value.pageSize, ...params } }) if (data.code === 200) { productList.value = data.result.products pagination.value.total = data.result.totalCount } else { console.error('商品列表初始化失败:', data.message) } } catch (error) { console.error('API请求错误:', error) } } return { productList, pagination, initProductList } }) ``` ### 步骤 2:在生命周期钩子中调用初始化 ```vue <!-- src/views/ProductList.vue --> <script setup> import { onMounted } from 'vue' import { useProductStore } from '@/stores/productStore' const productStore = useProductStore() onMounted(() => { // 初始化商品列表 productStore.initProductList() }) </script> ``` ### 步骤 3:处理接口参数的优化版本(参考引用[2][^2]) ```javascript // 在productStore.js中添加 const queryParams = ref({ name: '', category: '', minPrice: null, maxPrice: null }) const fetchProducts = async () => { const actualParams = { ...queryParams.value, page: pagination.value.currentPage, size: pagination.value.pageSize } // 过滤空值参数 Object.keys(actualParams).forEach(key => { if (actualParams[key] === null || actualParams[key] === '') { delete actualParams[key] } }) const { data } = await axios.get('/api/products', { params: actualParams }) // ...处理响应数据 } ``` ### 步骤 4:商品卡片组件集成(参考引用[1][^1]) ```vue <!-- src/components/ProductCard.vue --> <template> <div class="product-card"> <img :src="product.image" :alt="product.name"> <h3>{{ product.name }}</h3> <p>{{ product.price | currency }}</p> </div> </template> <script> export default { props: { product: { type: Object, required: true } } } </script> ``` ### 步骤 5:在商品列表页面使用 ```vue <!-- src/views/ProductList.vue --> <template> <div class="product-list"> <div v-if="productStore.loading">加载中...</div> <div v-else> <ProductCard v-for="product in productStore.productList" :key="product.id" :product="product" /> <Pagination :current="productStore.pagination.currentPage" :total="productStore.pagination.total" @page-change="handlePageChange" /> </div> </div> </template> <script setup> import { useProductStore } from '@/stores/productStore' import ProductCard from '@/components/ProductCard.vue' const productStore = useProductStore() // 分页变化处理 const handlePageChange = (page) => { productStore.pagination.currentPage = page productStore.fetchProducts() } </script> ``` ### 关键实践说明: 1. **状态管理**:使用 Pinia 集中管理商品状态(参考引用[1][^1]) 2. **生命周期钩子**:在 `onMounted` 中初始化数据(参考引用[2][^2]) 3. **API 请求**:使用 Axios 进行数据获取,带分页参数 4. **组件化**:商品卡片独立组件(参考引用[1][^1]) 5. **错误处理**:包含 API 请求错误处理 6. **参数管理**:支持筛选参数(参考引用[2][^2]的请求参数处理) > **注意**:实际开发中需要: > - 将 `/api/products` 替换为真实 API 端点 > - 根据后端返回的数据结构调整数据处理逻辑 > - 添加加载状态指示器 > - 实现错误提示界面
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值