【组件分享】商品列表组件-最佳实践

商品列表组件

商品列表组件用于展示商品信息列表,支持多种布局方式和自定义配置。

基础用法

vue 复制代码
<template>
  <ProGoodsList :goods-list="goodsList" :layout="layout" @item-click="handleItemClick" />
</template>

<script setup>
import { ref } from 'vue';

const layout = ref('grid'); // 'grid' | 'list'
const goodsList = ref([
  {
    id: 1,
    name: '商品名称',
    price: 99.99,
    image: 'https://example.com/image.jpg',
    description: '商品描述',
  },
]);

const handleItemClick = (item) => {
  console.log('点击商品:', item);
};
</script>

API

Props

参数 说明 类型 默认值
goodsList 商品列表数据 Array \[\]
layout 布局方式 String 'grid'
showPrice 是否显示价格 Boolean true
showDescription 是否显示描述 Boolean true

Events

事件名称 说明 回调参数
item-click 点击商品项时触发 (item: Object)
load-more 加载更多时触发 -

Slots

名称 说明
item 自定义商品项内容
empty 自定义空状态内容

主题定制

组件支持通过 CSS 变量进行主题定制:

css 复制代码
.pro-goods-list {
  --goods-item-bg: #ffffff;
  --goods-item-padding: 16px;
  --goods-item-radius: 8px;
  --goods-item-gap: 16px;
  --goods-price-color: #ff6b6b;
}

布局示例

网格布局

vue 复制代码
<template>
  <ProGoodsList :goods-list="goodsList" layout="grid" />
</template>

列表布局

vue 复制代码
<template>
  <ProGoodsList :goods-list="goodsList" layout="list" />
</template>
复制代码

商品列表组件

javascript 复制代码
<template>
  <view class="goods">
    <view v-if="list.length > 0" class="scroll-wrapper">
      <scroll-view
        :scroll-top="scrollTop"
        :scroll-y="scrollY"
        :class="scrollY && 'scroll-Y'"
        @scrolltolower="onScrolltolower"
      >
        <view class="goods-card-wrapper">
          <view
            v-for="(item, index) in list"
            :key="index"
            @click.stop="itemClick(item)"
            class="card-item"
          >
            <view class="pro-good-card">
              <view class="img-wrapper-A" :class="cardType === 'B' && 'img-wrapper-B'">
                <image class="pro-good-img" mode="widthFix" :src="item.picUrl" alt="" />
              </view>
              <view class="pro-good-info">
                <view class="first-title">
                  {{ item.name }}
                </view>
                <view
                  v-if="item.goodsDes"
                  class="sec-title-A"
                  :class="cardType === 'B' && 'sec-title-A'"
                >
                  {{ item.goodsDes }}
                </view>
                <view class="pro-good-price-A" :class="cardType === 'B' && 'pro-good-price-B'">
                  <view>
                    <text v-if="priceInfo.unit" class="price-unit">{{ priceInfo.unit }}</text>
                    <text class="retail-price">
                      {{ item.countPrice }}
                    </text>
                    <text v-if="priceInfo.des" class="price-des">{{ priceInfo.des }}</text>
                  </view>
                  <view v-if="item.counterPrice" class="counter-price">
                    ¥{{ item.counterPrice }}
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </scroll-view>
    </view>
    <view v-else>
      <image
        class="empty-img"
        src="https://static.wxb.com.cn/frontEnd/images/benz-mp/goods-empty.png"
      ></image>
      <view class="empty-text">暂无商品</view>
    </view>
  </view>
</template>
<script setup>
  import { ref, computed } from "vue";

  const props = defineProps({
    // 列表数据
    list: {
      type: Array,
      required: true,
      default: () => [],
    },
    // 卡片类型:A/B
    cardType: {
      type: String,
      default: "A",
    },
    // 卡片样式
    goodsStyle: {
      type: Object,
      default: () => {
        return {};
      },
    },
    // 价格相关的信息
    priceInfo: {
      type: Object,
      default: () => {
        return {
          unit: "¥",
          des: "起",
        };
      },
    },
    // 是否开启上拉加载
    scrollY: {
      type: Boolean,
      default: false,
    },
    //  滚动高度
    scrollHeight: {
      type: Number,
      default: 0,
    },
  });
  const scrollHeight = props.scrollHeight;
  const _goodsStyle = computed(() => {
    const options = {
      borderRadius: "16rpx", // 卡片圆角
      bgColor: props.cardType === "A" ? "#fff" : "rgba(255, 255, 255, 0.74)", // 卡片背景颜色
      //一级标题样式
      firstTitle: {
        fontSize: "28rpx",
        color: "#333",
        fontFamily: "PingFang SC, sans-serif",
      },
      //二级标题样式
      secondTitle: {
        fontSize: "22rpx",
        color: "#666",
        fontFamily: "PingFang SC, sans-serif",
      },
      priceColor: "#FF4D3B", // 价格字体颜色
      columnGap: "24rpx", // 列间距
      rowGap: "24rpx", // 行间距
    };
    return Object.assign(options, props.goodsStyle);
  });
  // 卡片圆角
  const borderRadius = _goodsStyle.value.borderRadius;
  // 一级标题样式
  const { fontSize: ffs, color: fc, fontFamily: ffm } = _goodsStyle.value.firstTitle;
  // 二级标题样式
  const { fontSize: sfs, color: sc, fontFamily: sfm } = _goodsStyle.value.secondTitle;
  // 价格颜色
  const priceColor = _goodsStyle.value.priceColor;
  // 卡片背景色
  const bgColor = _goodsStyle.value.bgColor;
  // 列间距
  const columnGap = _goodsStyle.value.columnGap;
  // 行间距
  const rowGap = _goodsStyle.value.rowGap;

  // 点击事件
  const emits = defineEmits(["itemClick", "scroll"]);
  const itemClick = (item) => {
    emits("itemClick", item.id);
  };

  const onScrolltolower = () => {
    console.log("上拉加载");
    emits("scroll");
  };
</script>
<style scoped lang="scss">
  @import '../base.scss';
  // 用于数字的特殊字体
  @font-face {
    font-family: "TCloudNumberRegular";
    src: url("./fonts/TCloudNumber-Regular.ttf") format("truetype"),
      url("./fonts/TCloudNumber-Regular.ttf") format("woff2");
  }
  .goods {
    text-align: center;
    height: v-bind(scrollHeight);
    .scroll-wrapper {
      height: 100%;
      .scroll-Y {
        height: 100%;
      }
      .goods-card-wrapper {
        column-gap: v-bind(columnGap);
        column-count: 2;
        .card-item {
          margin-bottom: 24rpx;
          break-inside: avoid; //避免子元素内容被截取
          .pro-good-card {
            width: 100%;
            background: v-bind(bgColor);
            border-radius: v-bind(borderRadius);
            overflow: hidden;
            .img-wrapper-A {
              padding: 12rpx 12rpx 0 12rpx;
              .pro-good-img {
                display: block;
                width: 100%;
                border-radius: 12rpx;
              }
            }
            .img-wrapper-B {
              padding: 0;
              .pro-good-img {
                display: block;
                width: 100%;
                border-radius: 0;
              }
            }

            .pro-good-info {
              padding: 0rpx 20rpx 28rpx 20rpx;
            }
            .first-title {
              font-size: v-bind(ffs);
              color: v-bind(fc);
              font-family: v-bind(ffm);
              font-weight: 500;
              line-height: 36rpx;
              overflow: hidden;
              text-overflow: ellipsis;
              display: -webkit-box;
              -webkit-box-orient: vertical;
              -webkit-line-clamp: 2;
              margin-top: 20rpx;
              text-align: left;
            }
            .first-title-B {
              margin-top: 10rpx;
            }
            .sec-title-A {
              font-size: v-bind(sfs);
              color: v-bind(sc);
              font-family: v-bind(sfm);
              line-height: 32rpx;
              margin-top: 8rpx;
              overflow: hidden;
              text-overflow: ellipsis;
              display: -webkit-box;
              -webkit-box-orient: vertical;
              -webkit-line-clamp: 1;
              text-align: left;
            }
            .sec-title-B {
              margin-top: 4rpx;
            }
            .pro-good-price-A {
              display: flex;
              align-items: baseline;
              margin-top: 24rpx;
              .price-unit {
                font-size: 20rpx;
                font-family: "TCloudNumberRegular";
                font-weight: 400;
                color: v-bind(priceColor);
                line-height: 24rpx;
              }
              .price-des {
                font-size: 20rpx;
                font-family: PingFang SC, sans-serif;
                font-weight: 500;
                color: v-bind(priceColor);
                line-height: 28rpx;
                margin-left: 4rpx;
              }
              .retail-price {
                font-size: 40rpx;
                color: v-bind(priceColor);
                line-height: 44rpx;
                font-family: "TCloudNumberRegular";
                vertical-align: middle;
              }
              .counter-price {
                font-size: 24rpx;
                text-decoration: line-through;
                margin-left: 12rpx;
                color: #cecece;
                font-family: "TCloudNumberRegular";
              }
            }
            .pro-good-price-B {
              margin-top: 20rpx;
            }
          }
        }
      }
    }
  }
</style>
相关推荐
荒诞英雄3 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro3 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte5 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅5 小时前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七7 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒9 小时前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉9 小时前
前端调试跨域如何解决
前端·vue.js
卓怡学长10 小时前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
迅速的自行车1 天前
从一段模板说起
前端·javascript·vue.js
阿奇__1 天前
uniapp微信小程序图片上传含回显组件(拍照,相册)
前端·javascript·vue.js