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

商品列表组件

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

基础用法

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>
相关推荐
工业互联网专业27 分钟前
基于springboot+vue的高校社团管理系统的设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计
白宇横流学长2 小时前
基于SpringBoot+Vue的旅游管理系统【源码+文档+部署讲解】
vue.js·spring boot·旅游
张人玉3 小时前
小白误入(需要一定的vue基础 )使用node建立服务器——vue前端登录注册页面连接到数据库
服务器·前端·vue.js
大大。3 小时前
element el-table合并单元格
前端·javascript·vue.js
杨.某某3 小时前
若依 v-hasPermi 自定义指令失效场景
前端·javascript·vue.js
Lysun0014 小时前
vue2的$el.querySelector在vue3中怎么写
前端·javascript·vue.js
海的预约5 小时前
VUE之路由Props、replace、编程式路由导航、重定向
前端·vue.js·智能路由器
大叔_爱编程5 小时前
wx036基于springboot+vue+uniapp的校园快递平台小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
小彭努力中7 小时前
16.在Vue3中使用Echarts实现词云图
前端·javascript·vue.js·echarts