基本思路
外层复用标准对象的, 选对象列表页的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>