重要场景能力| 支持商城列表pwc卡片个性化

基本思路

外层复用标准对象的, 选对象列表页的pwc插件, 通过table上get('pluginService') 将插件实例传给卡片组件,  卡片组件暴露 render.before 钩子, 支持定制卡片内容. 

实现方案

服用选对象列表页中的插件实例, 定义订货商城专属钩子, 将其返回的shoppingCard 组件, 透传给vcrm中的商城卡片, 然后替换, 入参和响应事件与订货通现有卡片保持一致. 

接口参数说明

hook事件定义

层级/分类hook事件名称(eventName)事件描述及适合执行的逻辑参数(opt)返回结果(rst)demo
选对象列表页dht.shopmall.list.render.before执行时机:渲染订货商城列表之前, 不包括顶部筛选主要作用:1、卡片替换1、商城列表定制 见下面返回结果(rst) 见下面demo
返回结果(rst)
{ // 商城卡片 shoppingCard: Card // Card为vue }
demo
import Card from './card'; export default class Plugin { apply() { return [ { event: "dht.shopmall.list.render.before", functional: this.shopmalRenderBefore.bind(this) } ]; } shopmalRenderBefore() { return Promise.resolve({ shoppingCard: Card }); } }

Card中的参数说明

Attributes

参数说明类型示例
product(产品 或 商品) 列表中的单条数据, 已格式化过, 如 product.commodityLabels 为标签Object{  name: "123"  _id: 'xxx'}
productFields(产品 或 商品) 对象字段描述Object{  name: {       label: '产品'         }}

Events

事件名称说明回调参数
on-action触发操作函数示例: this.$emit('on-action', { type, product });/**     * 处理商品卡片上的各种操作事件     * @param {string} type - 操作类型,可选值:     *   - 'CART': 加入购物车     *   - 'DETAIL': 查看商品详情     *   - 'COLLECTION': 收藏/取消收藏     *   - 'SPEC': 选择规格     *   - 'BOM': 选择配置     *   - 'ATTR': 选择属性     * @param{Object}product- 商品对象*/ 
配置入口
商品 或  产品的 列表页布局中

商城卡片替换demo示例

1.   入口main.js文件
import Card from './card'; export default class Plugin { apply() { return [ { event: "dht.shopmall.list.render.before", functional: this.shopmalRenderBefore.bind(this) } ]; } shopmalRenderBefore() { return Promise.resolve({ shoppingCard: Card }); } }
2.  定制的商城卡片
card.vue
注: pwc模板文件, 在vcrm中 vcrm\src\widgets\product-card\pwcCardDemo.vue  有一份
<!-- pwc自定义商品卡片组件, 此组件可作为提供给实施客开模板 --> <template> <div class="dht-card-item"> <div class="dht-card-item-content"> <div class="dht-card-base-content"> <!-- 商品图片 --> <div class="dht-card-item-img" @click="onAction('DETAIL', product)"> <img :src="pictureUrl" class="img"> </div> <!-- 商品信息 --> <div class="dht-card-item-info"> <!-- 商品名称 --> <div class="item item-name">{{ product.display_name || product.name }}</div> <!-- 商品价格 --> <div class="item item-price"> <span class="price-prefix">{{ product.mc_currency__r || '¥' }}</span> {{ displayPrice }} <span class="dht-price-unit" v-if="isShowPriceUnit">/{{ priceUnitName }}</span> </div> <!-- 字段列表 --> <ul class="field-list"> <li class="field-item" v-for="field in showFields" :key="field.api_name" > <span class="field-item-label dht-card-ellipsis">{{ field.label }}</span> <span class="field-item-symbol">:</span> <span class="field-item-value dht-card-ellipsis">{{ field.value }}</span> </li> </ul> <!-- 商品标签 --> <div class="item item-tag"> <span class="product-tag" v-if="product.hasPricePolicy" style="color: #ff8000"> {{ $t('促销') }} </span> <span v-for="option in commodityLabels" :key="option.value" class="product-tag" :style="{color: option.font_color}"> {{ option.label }} </span> </div> </div> </div> <!-- 操作按钮区域 --> <div class="dht-card-item-operate"> <div class="single-spec-operate"> <div class="card-item-input-wrap"> <input ref="fkInput" class="fk-input-number-input" :value="localQuantity" @input="e => localQuantity = e.target.value" @keydown.enter="onAction('CART', product)" /> <span class="product-unit">{{ priceUnitName }}</span> </div> <i class="el-icon-shopping-cart-2 add-cart-btn" @click="onAction('CART', product)" :class="{active: localQuantity}"> </i> </div> </div> <!-- 收藏按钮 --> <div class="dht-collection-btn" @click="onAction('COLLECTION', product)"> <i :class="[product.is_in_collection ? 'el-icon-star-on' : 'el-icon-star-off']"></i> <span>{{ product.is_in_collection ? $t('已收藏') : $t('收藏') }}</span> </div> </div> </div> </template> <script> // import InputNumber from './input-number.vue'; export default { name: 'customPwcCard', components: { // InputNumber }, props: { product: { type: Object, required: true }, productFields: { type: Object, default: () => ({}) } }, data() { return { localQuantity: '' } }, computed: { // 图片地址 pictureUrl() { const picture = this.product.picture || this.product.picture_path; if (!picture) { return $dht.config.placeholderImg.path ? `${this.getImgHost()}/image/o/${$dht.config.placeholderImg.path}/350*350/jpg/FSAID_11490c84` : 'https://a9.fspage.com/FSR/weex/avatar/object_list/images/list_default_icon.png'; } let strNPath = typeof picture === 'string' ? picture : ''; if (Array.isArray(picture)) { const imageData = picture[0]; if (!imageData || !imageData.path) { return 'https://a9.fspage.com/FSR/weex/avatar/object_list/images/list_default_icon.png'; } strNPath = imageData.path; } return `${this.getImgHost()}/image/o/${strNPath}/350*350/jpg/FSAID_11490c84`; }, getImgHost() { return () => { const host = window.location.host; const defaultHost = host.replace(/^(dht|www|crm)/, 'img'); return `//${defaultHost}`; }; }, // 产品 标签 commodityLabels() { let commodityOptions = this.product.commodityOptions || []; return commodityOptions.filter((option) => option.value !== 'option1'); }, // 是否新品 isNew() { return (this.product.commodityOptions || []).findIndex(option => option.value === 'option1') !== -1; }, // 单位 priceUnitName() { return this.product.unit__r; }, // 是否显示多单位 isShowPriceUnit() { return this.product.is_multiple_unit && !this.product.is_common_unit; }, // 显示价格 displayPrice() { const priceBookPrice = this.product.virtual_price_book_price; return priceBookPrice != null ? priceBookPrice : this.product.price; }, // 显示其它产品字段 showFields() { // 这里可以根据需要配置要显示的字段 const show_fields_apiname = ['is_saleable', 'virtual_available_stock']; const showFields = []; show_fields_apiname.forEach(api_name => { const field = this.productFields[api_name]; if (field) { showFields.push({ api_name, label: field.label, value: this.getFormatFieldValue(field, api_name), }); } }); return showFields; } }, methods: { /** * 处理商品卡片上的各种操作事件 * @param {string} type - 操作类型,可选值: * - 'CART': 加入购物车 * - 'DETAIL': 查看商品详情 * - 'COLLECTION': 收藏/取消收藏 * - 'SPEC': 选择规格 * - 'BOM': 选择配置 * - 'ATTR': 选择属性 * @param {Object} product - 商品对象 */ onAction(type, product) { // 加入购物车时,需要获取输入框的位置信息用于动画效果 if (type === 'CART') { // 创建一个新的商品对象,包含本地数量 const productWithQuantity = { ...product, quantity: this.localQuantity }; this.$emit('on-action', { type, product: productWithQuantity }); // 清空本地数量 this.localQuantity = ''; } else { this.$emit('on-action', { type, product }); } }, /** * 获取格式化字段值 * @param field 字段 * @param api_name 字段api_name * @return string */ getFormatFieldValue(field, api_name) { const DEFAULT_VALUE = '--'; const fieldValueGetter = $dht.services.metaRender.fieldValueGetter; const value = this.product[api_name]; const defaultValue = this.productFields[api_name]?.default_value; const getter = fieldValueGetter[field.type]; const formatValue = getter && getter(field, value || defaultValue, this.product, {}); return _.isEmpty(formatValue) ? DEFAULT_VALUE : formatValue; } }, mounted() { console.log('customPwcCard mounted'); // console.log(this.product); // console.log(this.productFields); } } </script> <style lang="less" scoped> .dht-card-item { width: 216px; min-height: 338px; box-sizing: border-box; margin: 0 0 16px 16px; flex: none; &-content { position: relative; width: 100%; height: 100%; box-sizing: border-box; border-radius: 4px; overflow: hidden; background-color: white; transition: all .2s ease-in-out; box-shadow: -2px 2px 16px rgba(0, 0, 0, .15); &:hover { .dht-collection-btn, .dht-card-item-operate { opacity: 1; } } } &-img { position: relative; height: 214px; width: 100%; box-sizing: border-box; cursor: pointer; .img { height: 100%; width: 100%; object-fit: cover; } } &-info { padding: 10px; text-align: left; font-size: 14px; .item { &-name { color: #333333; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } &-price { position: relative; display: flex; align-items: center; margin-top: 2px; font-size: 18px; font-weight: bold; color: #333; line-height: 28px; .price-prefix { font-size: 18px; } > .price-unit { flex-basis: 0; flex-grow: 1; font-size: 12px; color: #999; } .stock-info { font-size: 12px; color: #999; font-weight: normal; position: absolute; right: 0; } } &-tag { margin-top: 8px; .product-tag { display: inline-block; padding: 2px 6px; margin-right: 8px; font-size: 12px; border-radius: 2px; background: #f5f5f5; } } } .field-list { margin-top: 8px; padding: 0; list-style: none; .field-item { display: flex; align-items: center; margin-bottom: 4px; font-size: 12px; color: #666; &-label { flex: 0 0 auto; max-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } &-symbol { margin: 0 4px; color: #999; } &-value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } } } &-operate { box-sizing: border-box; position: absolute; right: 0; bottom: 8px; z-index: 1; display: flex; justify-content: flex-end; align-items: center; width: 100%; padding: 8px 10px 0 10px; background-color: #fff; opacity: 0; transition: all .25s ease-in-out; .single-spec-operate { display: flex; align-items: center; .product-unit { margin-left: 4px; font-size: 12px; color: #666; } .card-item-input-wrap { display: flex; align-items: center; position: relative; width: 80px; height: 32px; border: 1px solid #d9d9d9; border-radius: 4px; transition: all .3s; &:hover { border-color: #40a9ff; } &:focus-within { border-color: #40a9ff; box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); } .fk-input-number-input { width: 100%; height: 30px; padding: 0 11px; text-align: left; background-color: transparent; border: 0; border-radius: 4px; outline: 0; transition: all .3s linear; -moz-appearance: textfield!important; box-sizing: border-box; font-size: 14px; color: rgba(0,0,0,.65); line-height: 1.5; &::-webkit-inner-spin-button, &::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } } } .add-cart-btn { margin-left: 5px; font-size: 16px; background: #F5F7FA; width: 24px; min-width: 24px; height: 24px; position: relative; line-height: 20px; cursor: pointer; transition: all .1s ease-in-out; border-radius: 4px; &:active { transform: scale(0.95); } &:hover { background-color: #ff8d1a !important; color: white; } &.active { background-color: #ff8d1a; color: white; } &::before { content: '\e74f'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } } } } .dht-collection-btn { display: inline-flex; position: absolute; background: rgba(0,0,0, .5); top: 10px; right: 10px; border-radius: 2px; height: 24px; padding: 0 5px; justify-content: center; align-items: center; color: white; font-size: 12px; box-sizing: border-box; z-index: 1; transition: all .25s ease-in-out; opacity: 0; cursor: pointer; i { margin-right: 4px; } .el-icon-star-on { color: #ffd700; font-size: 16px; } } } .dht-left-tag { position: absolute; top: 5px; left: 0px; display: flex; align-items: center; font-size: 10px; color: #fff; overflow: hidden; > .dht-left-tag-text { height: 18px; line-height: 18px; padding: 0 5px 0 10px; } > .dht-left-tag-icon { height: 18px; width: 11px; transform: translateX(-4px) skewX(-20deg); border-top-right-radius: 3px; border-bottom-right-radius: 5px; } } @media (min-width: 1440px) { .dht-card-item { width: 230px; min-height: 338px; &-img { height: 228px; } } } @media (min-width: 1920px) { .dht-card-item { width: 240px; min-height: 349px; &-img { height: 238px; } } } </style>
2025-05-26
1 0