feat: sku to support large image mode

This commit is contained in:
水墨 2020-06-23 09:48:43 +08:00 committed by neverland
parent 489dde2114
commit 54fca31cc3
10 changed files with 412 additions and 66 deletions

View File

@ -140,6 +140,9 @@ export default {
| start-sale-num `v2.3.0` | Minimum quantity | _number_ | `1` |
| properties `v2.4.2` | Goods properties | _array_ | - |
| preview-on-click-image `v2.5.2` | Whether to preview image when click goods image | _boolean_ | `true` |
| supportBigPicture `v2.9.0` | Whether to display large image mode | _boolean_ | `false` |
| supportBigPictureIndex `v2.9.0` | The index value of the large image mode | _number_ | `0` |
| hasScrollTab `v2.9.0` | Whether the large image mode displays a scroll bar | _boolean_ | `false` |
### Events

View File

@ -144,6 +144,9 @@ export default {
| start-sale-num `v2.3.0` | 起售数量 | _number_ | `1` |
| properties `v2.4.2` | 商品属性 | _array_ | - |
| preview-on-click-image `v2.5.2` | 是否在点击商品图片时自动预览 | _boolean_ | `true` |
| supportBigPicture `v2.9.0` | 是否展示大图模式 | _boolean_ | `false` |
| supportBigPictureIndex `v2.9.0` | 多规格情况下,大图模式的索引值 | _number_ | `0` |
| hasScrollTab `v2.9.0` | 大图模式下,是否展示滚动条 | _boolean_ | `false` |
### Events

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import Popup from '../popup';
import Toast from '../toast';
import Lazyload from '../lazyload';
import ImagePreview from '../image-preview';
import SkuHeader from './components/SkuHeader';
import SkuHeaderItem from './components/SkuHeaderItem';
@ -25,6 +26,8 @@ const namespace = createNamespace('sku');
const [createComponent, bem, t] = namespace;
const { QUOTA_LIMIT } = LIMIT_TYPE;
Vue.use(Lazyload);
export default createComponent({
props: {
sku: Object,
@ -98,6 +101,18 @@ export default createComponent({
type: Boolean,
default: true,
},
supportBigPicture: {
type: Boolean,
default: false,
},
supportBigPictureIndex: {
type: Number,
default: 0,
},
hasScrollTab: {
type: Boolean,
default: false,
},
},
data() {
@ -616,8 +631,10 @@ export default createComponent({
selectedNum,
stepperTitle,
selectedSkuComb,
supportBigPicture,
supportBigPictureIndex,
hasScrollTab,
} = this;
const slotsProps = {
price,
originPrice,
@ -634,6 +651,7 @@ export default createComponent({
goods={goods}
skuEventBus={skuEventBus}
selectedSku={selectedSku}
supportBigPicture={supportBigPicture}
>
<template slot="sku-header-image-extra">
{slots('sku-header-image-extra')}
@ -669,17 +687,44 @@ export default createComponent({
slots('sku-group') ||
(this.hasSkuOrAttr && (
<div class={this.skuGroupClass}>
{this.skuTree.map((skuTreeItem) => (
<SkuRow skuRow={skuTreeItem}>
{skuTreeItem.v.map((skuValue) => (
<SkuRowItem
skuList={sku.list}
skuValue={skuValue}
selectedSku={selectedSku}
skuEventBus={skuEventBus}
skuKeyStr={skuTreeItem.k_s}
/>
))}
{this.skuTree.map((skuTreeItem, index) => (
<SkuRow
skuRow={skuTreeItem}
isShowBigPicture={
supportBigPicture && supportBigPictureIndex === index
}
hasScrollTab={hasScrollTab && skuTreeItem.v.length > 6}
>
{skuTreeItem.v.map((skuValue, itemIndex) => {
return supportBigPicture && supportBigPictureIndex === index ? (
<template
slot={
Math.floor(itemIndex / 3) % 2 === 0
? 'sku-item-group-one'
: 'sku-item-group-two'
}
>
<SkuRowItem
skuList={sku.list}
skuValue={skuValue}
selectedSku={selectedSku}
skuEventBus={skuEventBus}
skuKeyStr={skuTreeItem.k_s}
isShowBigPicture={
supportBigPicture && supportBigPictureIndex === index
}
></SkuRowItem>
</template>
) : (
<SkuRowItem
skuList={sku.list}
skuValue={skuValue}
selectedSku={selectedSku}
skuEventBus={skuEventBus}
skuKeyStr={skuTreeItem.k_s}
></SkuRowItem>
);
})}
</SkuRow>
))}
{this.propList.map((skuTreeItem) => (

View File

@ -13,6 +13,7 @@ export type SkuHeaderProps = {
goods: SkuGoodsData;
skuEventBus: Vue;
selectedSku: SelectedSkuData;
supportBigPicture: boolean;
};
export type SkuHeaderSlots = DefaultSlots & {
@ -49,20 +50,34 @@ function SkuHeader(
slots: SkuHeaderSlots,
ctx: RenderContext<SkuHeaderProps>
) {
const { sku, goods, skuEventBus, selectedSku } = props;
const {
sku,
goods,
skuEventBus,
selectedSku,
supportBigPicture = false,
} = props;
const goodsImg = getSkuImg(sku, selectedSku) || goods.picture;
const previewImage = () => {
skuEventBus.$emit('sku:previewImage', goodsImg);
};
return (
<div class={[bem(), BORDER_BOTTOM]} {...inherit(ctx)}>
<div class={bem('img-wrap')} onClick={previewImage}>
<img src={goodsImg} />
{slots['sku-header-image-extra']?.()}
{!supportBigPicture && (
<div class={bem('img-wrap')} onClick={previewImage}>
<img src={goodsImg} />
{slots['sku-header-image-extra']?.()}
</div>
)}
<div
class={[
bem('goods-info'),
supportBigPicture && bem('goods-info--no-padding'),
]}
>
{slots.default?.()}
</div>
<div class={bem('goods-info')}>{slots.default?.()}</div>
</div>
);
}
@ -72,6 +87,7 @@ SkuHeader.props = {
goods: Object,
skuEventBus: Object,
selectedSku: Object,
supportBigPicture: Boolean,
};
export default createComponent<SkuHeaderProps>(SkuHeader);

View File

@ -0,0 +1,101 @@
// Utils
import { createNamespace } from '../../utils';
import { BORDER_BOTTOM } from '../../utils/constant';
import { BindEventMixin } from '../../mixins/bind-event';
import { getScroller } from '../../utils/dom/scroll';
const [createComponent, bem, t] = createNamespace('sku-row');
export default createComponent({
mixins: [
BindEventMixin(function (bind) {
if (!(this.isShowBigPicture && this.hasScrollTab)) {
return false;
}
if (!this.scrollCon) {
this.scrollCon = getScroller(this.$refs.skuContent);
}
bind(this.scrollCon, 'scroll', this.onScroll);
}),
],
props: {
skuRow: Object,
isShowBigPicture: {
type: Boolean,
default: false,
},
hasScrollTab: {
type: Boolean,
default: false,
},
},
data() {
return {
present: 0,
contentWidth: 0,
contentItemWidth: 0,
scrollLeft: 0,
};
},
computed: {
scrollStyle() {
if (!(this.isShowBigPicture && this.hasScrollTab)) {
return false;
}
this.tranX = this.present * 20;
return {
transform: `translate3d(${this.tranX}px, 0, 0)`,
};
},
},
methods: {
onScroll() {
this.$nextTick(() => {
const contentWidth = this.scrollCon.clientWidth;
const contentItemWidth = this.$refs.skuContentItem.clientWidth;
const distance = contentItemWidth - contentWidth;
this.present = this.scrollCon.scrollLeft / distance;
});
},
},
mounted() {},
render() {
const { skuRow, isShowBigPicture, hasScrollTab } = this;
const multipleNode = skuRow.is_multiple && (
<span class={bem('title-multiple')}>{t('multiple')}</span>
);
const SkuScroll = (
<div class={bem('scroll')}>
<div class={bem('scroll__content')} ref="skuScroll">
<div
class={bem('scroll__content--active')}
style={this.scrollStyle}
></div>
</div>
</div>
);
const SkuContent = (
<div class={bem('content')} ref="skuContent">
<div class={bem('content__top')} ref="skuContentItem">
{this.slots('sku-item-group-one')}
</div>
<div class={bem('content__bottom')}>
{this.slots('sku-item-group-two')}
</div>
</div>
);
return (
<div class={[bem(), BORDER_BOTTOM, isShowBigPicture && bem('picture')]}>
<div class={bem('title')}>
{skuRow.k}
{multipleNode}
</div>
{isShowBigPicture ? SkuContent : this.slots()}
{isShowBigPicture && hasScrollTab && SkuScroll}
</div>
);
},
});

View File

@ -1,41 +0,0 @@
// Utils
import { createNamespace } from '../../utils';
import { inherit } from '../../utils/functional';
import { BORDER_BOTTOM } from '../../utils/constant';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../../utils/types';
import { SkuTreeItemData } from '../../../types/sku';
export type SkuRowProps = {
skuRow: SkuTreeItemData;
};
const [createComponent, bem, t] = createNamespace('sku-row');
function SkuRow(
h: CreateElement,
props: SkuRowProps,
slots: DefaultSlots,
ctx: RenderContext<SkuRowProps>
) {
const multipleNode = props.skuRow.is_multiple && (
<span class={bem('title-multiple')}>{t('multiple')}</span>
);
return (
<div class={[bem(), BORDER_BOTTOM]} {...inherit(ctx)}>
<div class={bem('title')}>
{props.skuRow.k}
{multipleNode}
</div>
{slots.default && slots.default()}
</div>
);
}
SkuRow.props = {
skuRow: Object,
};
export default createComponent<SkuRowProps>(SkuRow);

View File

@ -13,6 +13,7 @@ export default createComponent({
type: Array,
default: () => [],
},
isShowBigPicture: Boolean,
},
computed: {
@ -33,25 +34,42 @@ export default createComponent({
});
}
},
onPreviewImg(event) {
event.stopPropagation();
this.skuEventBus.$emit(
'sku:previewImage',
this.skuValue.imgUrl || this.skuValue.img_url
);
},
},
render() {
const choosed = this.skuValue.id === this.selectedSku[this.skuKeyStr];
const imgUrl = this.skuValue.imgUrl || this.skuValue.img_url;
const BEM_NAME = this.isShowBigPicture
? 'van-sku-row__picture-item'
: 'van-sku-row__item';
return (
<span
class={[
'van-sku-row__item',
{
'van-sku-row__item--active': choosed,
'van-sku-row__item--disabled': !this.choosable,
},
`${BEM_NAME}`,
choosed ? `${BEM_NAME}--active` : '',
!this.choosable ? `${BEM_NAME}--disabled` : '',
]}
onClick={this.onSelect}
>
{imgUrl && <img class="van-sku-row__item-img" src={imgUrl} />}
<span class="van-sku-row__item-name">{this.skuValue.name}</span>
{this.isShowBigPicture && (
<img
class={`${BEM_NAME}-img-icon`}
src="https://img.yzcdn.cn/upload_files/2020/06/18/Fn6Qf0fGRFyuD8xh0Gi1w2ng59G1.png"
onClick={this.onPreviewImg}
/>
)}
{imgUrl && (
<img class={`${BEM_NAME}-img`} src={imgUrl} v-lazy={imgUrl} />
)}
<span class={`${BEM_NAME}-name`}>{this.skuValue.name}</span>
</span>
);
},

View File

@ -29,6 +29,44 @@ export const skuData = {
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
{
id: '1215',
name: '白色',
imgUrl:
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
},
],
k_s: 's1',

View File

@ -140,6 +140,38 @@
</van-button>
</div>
</demo-block>
<!-- 大图模式 -->
<demo-block :title="t('bigPicture')">
<div class="sku-container">
<van-sku
v-model="showBigPictureMode"
:sku="skuData.sku"
:goods="skuData.goods_info"
:goods-id="skuData.goods_id"
:hide-stock="skuData.sku.hide_stock"
:quota="skuData.quota"
:quota-used="skuData.quota_used"
:start-sale-num="skuData.start_sale_num"
:close-on-click-overlay="closeOnClickOverlay"
:message-config="messageConfig"
:custom-sku-validator="customSkuValidator"
:properties="skuData.properties"
:support-big-picture="true"
:has-scroll-tab="true"
disable-stepper-input
reset-stepper-on-hide
safe-area-inset-bottom
reset-selected-sku-on-hide
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
>
</van-sku>
<van-button block type="primary" @click="showBigPictureMode = true">
{{ t('bigPicture') }}
</van-button>
</div>
</demo-block>
</demo-section>
</template>
@ -156,6 +188,7 @@ export default {
button1: '积分兑换',
button2: '买买买',
actionsTop: '商品不多,赶快购买吧',
bigPicture: 'sku大图模式',
},
'en-US': {
title2: 'Custom Stepper Related Config',
@ -164,6 +197,7 @@ export default {
button1: 'Button',
button2: 'Button',
actionsTop: 'Action top info',
bigPicture: 'sku big picture mode',
},
},
@ -176,6 +210,7 @@ export default {
showCustom: false,
showStepper: false,
showSoldout: false,
showBigPictureMode: false,
closeOnClickOverlay: true,
customSkuValidator: () => '请选择xxx',
customStepperConfig: {

View File

@ -56,6 +56,10 @@
min-height: 96px;
padding: @padding-sm 20px @padding-sm @padding-xs;
overflow: hidden;
&--no-padding {
padding-left: 0;
}
}
}
@ -123,6 +127,92 @@
margin-bottom: 0;
}
&__picture {
margin-right: 0;
&-item {
position: relative;
flex: 0 0 110px;
width: 110px;
height: 150px;
margin: 0 4px 6px 0;
color: @text-color;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: @border-radius-md;
cursor: pointer;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: @sku-item-background-color;
border-radius: @border-radius-md;
content: '';
}
&-img {
position: absolute;
top: 0;
left: 0;
z-index: 1;
display: block;
width: 110px;
height: 110px;
object-fit: cover;
border-radius: @border-radius-md @border-radius-md 0 0;
&-icon {
position: absolute;
top: 0;
right: 0;
z-index: 3;
width: 18px;
height: 19px;
opacity: 1 !important;
}
}
&-name {
position: absolute;
top: 110px;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 110px;
height: 40px;
padding: @padding-base;
font-size: 12px;
line-height: 16px;
}
&--active {
color: @red;
border: 1px solid currentColor;
&::before {
background: currentColor;
opacity: 0.1;
}
}
&--disabled {
color: @gray-5;
cursor: not-allowed;
&::before {
z-index: 2;
background: @active-color;
opacity: 0.4;
}
}
}
}
&__title {
padding-bottom: @padding-sm;
}
@ -189,6 +279,44 @@
}
}
}
&__content {
overflow-x: scroll;
::-webkit-scrollbar {
display: none;
}
&__top {
display: inline-flex;
margin-bottom: 4px;
}
&__bottom {
display: inline-flex;
margin-bottom: 4px;
}
}
&__scroll {
display: flex;
justify-content: center;
height: 4px;
padding-bottom: 16px;
&__content {
position: relative;
width: 40px;
height: 4px;
background: #ebedf0;
border-radius: 2px;
&--active {
width: 20px;
height: 4px;
background: #f44;
border-radius: 2px;
}
}
}
}
&-stepper-stock {