mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-05-23 23:19:15 +08:00
chore: temporaily remove sku
This commit is contained in:
parent
63bd4700ab
commit
1e2f55db5a
@ -1,397 +0,0 @@
|
|||||||
# Sku
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
```js
|
|
||||||
import Vue from 'vue';
|
|
||||||
import { Sku } from 'vant';
|
|
||||||
|
|
||||||
Vue.use(Sku);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
:reset-stepper-on-hide="resetStepperOnHide"
|
|
||||||
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
|
|
||||||
:disable-stepper-input="disableStepperInput"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: false,
|
|
||||||
sku: {},
|
|
||||||
goods: {},
|
|
||||||
messageConfig: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Stepper
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
:custom-stepper-config="customStepperConfig"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom By Slot
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
stepper-title="Stepper title"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
show-add-cart-btn
|
|
||||||
reset-stepper-on-hide
|
|
||||||
:initial-sku="initialSku"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
>
|
|
||||||
<!-- custom sku-header-price -->
|
|
||||||
<template #sku-header-price="props">
|
|
||||||
<div class="van-sku__goods-price">
|
|
||||||
<span class="van-sku__price-symbol">¥</span
|
|
||||||
><span class="van-sku__price-num">{{ props.price }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- custom sku actions -->
|
|
||||||
<template #sku-actions="props">
|
|
||||||
<div class="van-sku-actions">
|
|
||||||
<van-button square size="large" type="warning" @click="onPointClicked">
|
|
||||||
Button
|
|
||||||
</van-button>
|
|
||||||
<!-- trigger sku inner event -->
|
|
||||||
<van-button
|
|
||||||
square
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
@click="props.skuEventBus.$emit('sku:buy')"
|
|
||||||
>
|
|
||||||
Button
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</van-sku>
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### Props
|
|
||||||
|
|
||||||
| Attribute | Description | Type | Default |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| v-model | Whether to show sku | _boolean_ | `false` |
|
|
||||||
| sku | Sku data | _object_ | - |
|
|
||||||
| goods | Goods info | _object_ | - |
|
|
||||||
| goods-id | Goods id | `string | _number_ | - |
|
|
||||||
| price-tag | Tag behind the price | _string_ | - |
|
|
||||||
| hide-stock | Whether to hide stock | _boolean_ | `false` |
|
|
||||||
| hide-quota-text | Whether to hide quota text | _boolean_ | `false` |
|
|
||||||
| hide-selected-text | Whether to hide selected text | _boolean_ | `false` |
|
|
||||||
| stock-threshold | stock threshold | _boolean_ | `50` |
|
|
||||||
| show-add-cart-btn | Whether to show cart button | _boolean_ | `true` |
|
|
||||||
| buy-text | Buy button text | _string_ | - | - |
|
|
||||||
| add-cart-text | Add cart button text | _string_ | - | - |
|
|
||||||
| quota | Quota (0 as no limit) | _number_ | `0` |
|
|
||||||
| quota-used | Used quota | _number_ | `0` |
|
|
||||||
| reset-stepper-on-hide | Whether to reset stepper when hide | _boolean_ | `false` |
|
|
||||||
| reset-selected-sku-on-hide | Whether to reset selected sku when hide | _boolean_ | `false` |
|
|
||||||
| disable-stepper-input | Whether to disable stepper input | _boolean_ | `false` |
|
|
||||||
| close-on-click-overlay | Whether to close sku popup when click overlay | _boolean_ | `true` |
|
|
||||||
| stepper-title | Quantity title | _string_ | `Quantity` |
|
|
||||||
| custom-stepper-config | Custom stepper related config | _object_ | `{}` |
|
|
||||||
| message-config | Message related config | _object_ | `{}` |
|
|
||||||
| get-container | Return the mount node for sku | _string \| () => Element_ | - |
|
|
||||||
| safe-area-inset-bottom `v2.2.1` | Whether to enable bottom safe area adaptation | _boolean_ | `true` |
|
|
||||||
| 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` |
|
|
||||||
| show-header-image `v2.9.0` | Whether to display header image | _boolean_ | `true` |
|
|
||||||
| lazy-load | Whether to enable lazy load,should register [Lazyload](#/en-US/lazyload) component | _boolean_ | `false` |
|
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
| Event | Description | Arguments |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| add-cart | Triggered when click cart button | data: object |
|
|
||||||
| buy-clicked | Triggered when click buy button | data: object |
|
|
||||||
| stepper-change | Triggered when stepper value changed | value: number |
|
|
||||||
| sku-selected | Triggered when select sku | { skuValue, selectedSku, selectedSkuComb } |
|
|
||||||
| sku-prop-selected | Triggered when select property | { propValue, selectedProp, selectedSkuComb } |
|
|
||||||
| open-preview | Triggered when open image preview | data: object |
|
|
||||||
| close-preview | Triggered when close image preview | data: object |
|
|
||||||
| sku-reset `v2.8.1` | Triggered when reset sku and property | { selectedSku, selectedProp, selectedSkuComb } |
|
|
||||||
|
|
||||||
### Methods
|
|
||||||
|
|
||||||
Use [ref](https://vuejs.org/v2/api/#ref) to get Sku instance and call instance methods
|
|
||||||
|
|
||||||
| Name | Description | Attribute | Return value |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| getSkuData | Get current skuData | - | skuData |
|
|
||||||
| resetSelectedSku `v2.3.0` | Reset selected sku to initial sku | - | - |
|
|
||||||
|
|
||||||
### Slots
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
| ------------------------------- | --------------------------------- |
|
|
||||||
| sku-header | Custom header |
|
|
||||||
| sku-header-price | Custom header price area |
|
|
||||||
| sku-header-origin-price | Custom header origin price area |
|
|
||||||
| sku-header-extra | Extra header area |
|
|
||||||
| sku-header-image-extra `v2.5.2` | Custom header image extra area |
|
|
||||||
| sku-body-top | Custom content before sku-group |
|
|
||||||
| sku-group | Custom sku |
|
|
||||||
| extra-sku-group | Extra custom content |
|
|
||||||
| sku-stepper | Custom stepper |
|
|
||||||
| sku-messages | Custom messages |
|
|
||||||
| sku-actions-top `v2.4.7` | Custom content before sku-actions |
|
|
||||||
| sku-actions | Custom button actions |
|
|
||||||
|
|
||||||
### Sku Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
sku: {
|
|
||||||
tree: [
|
|
||||||
{
|
|
||||||
k: 'Color',
|
|
||||||
k_s: 's1',
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Red',
|
|
||||||
imgUrl: 'https://img.yzcdn.cn/1.jpg',
|
|
||||||
previewImgUrl: 'https://img.yzcdn.cn/1p.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Blue',
|
|
||||||
imgUrl: 'https://img.yzcdn.cn/2.jpg',
|
|
||||||
previewImgUrl: 'https://img.yzcdn.cn/2p.jpg',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
largeImageMode: true, // whether to enable large image mode
|
|
||||||
}
|
|
||||||
],
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
id: 2259,
|
|
||||||
s1: '1',
|
|
||||||
s2: '1',
|
|
||||||
price: 100,
|
|
||||||
stock_num: 110
|
|
||||||
}
|
|
||||||
],
|
|
||||||
price: '1.00',
|
|
||||||
stock_num: 227,
|
|
||||||
collection_id: 2261,
|
|
||||||
none_sku: false,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
multiple: '0',
|
|
||||||
name: 'Message',
|
|
||||||
type: 'text',
|
|
||||||
required: '1',
|
|
||||||
placeholder: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hide_stock: false,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
k_id: 123,
|
|
||||||
k: 'More',
|
|
||||||
is_multiple: true,
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1222,
|
|
||||||
name: 'Tea',
|
|
||||||
price: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1223,
|
|
||||||
name: 'Water',
|
|
||||||
price: 1,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### properties Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
[
|
|
||||||
{
|
|
||||||
k_id: 123,
|
|
||||||
k: 'More',
|
|
||||||
is_multiple: true,
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1222,
|
|
||||||
name: 'Tea',
|
|
||||||
price: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1223,
|
|
||||||
name: 'Water',
|
|
||||||
price: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### initialSku Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
// Key:skuKeyStr
|
|
||||||
// Value:skuValueId
|
|
||||||
s1: '30349',
|
|
||||||
s2: '1193',
|
|
||||||
selectedNum: 3,
|
|
||||||
selectedProp: {
|
|
||||||
123: [1222]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Goods Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
goods: {
|
|
||||||
picture: 'https://img.yzcdn.cn/1.jpg';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### customStepperConfig Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
customStepperConfig: {
|
|
||||||
// custom quota text
|
|
||||||
quotaText: 'only 5 can buy',
|
|
||||||
// custom callback when over limit
|
|
||||||
handleOverLimit: (data) => {
|
|
||||||
const { action, limitType, quota, quotaUsed, startSaleNum } = data;
|
|
||||||
|
|
||||||
if (action === 'minus') {
|
|
||||||
Toast(`at least select ${startSaleNum > 1 ? startSaleNum : 'one'}`);
|
|
||||||
} else if (action === 'plus') {
|
|
||||||
// const { LIMIT_TYPE } = Sku.skuConstants;
|
|
||||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
|
||||||
let msg = `Buy up to ${quota}`;
|
|
||||||
if (quotaUsed > 0) msg += `,you already buy ${quotaUsed}`;
|
|
||||||
Toast(msg);
|
|
||||||
} else {
|
|
||||||
Toast('not enough stock');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// custom callback when stepper value change
|
|
||||||
handleStepperChange: currentValue => {},
|
|
||||||
// stock
|
|
||||||
stockNum: 1999,
|
|
||||||
// stock fomatter
|
|
||||||
stockFormatter: stockNum => {},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### messageConfig Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
messageConfig: {
|
|
||||||
// the upload image callback
|
|
||||||
uploadImg: () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// max file size (MB)
|
|
||||||
uploadMaxSize: 3,
|
|
||||||
// placeholder config
|
|
||||||
placeholderMap: {
|
|
||||||
text: 'xxx',
|
|
||||||
tel: 'xxx',
|
|
||||||
...
|
|
||||||
},
|
|
||||||
// Key:message name
|
|
||||||
// Value:message value
|
|
||||||
initialMessages: {
|
|
||||||
message: 'message value'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Events Params Data Structure
|
|
||||||
|
|
||||||
```js
|
|
||||||
skuData: {
|
|
||||||
goodsId: '946755',
|
|
||||||
messages: {
|
|
||||||
message_0: '12',
|
|
||||||
message_1: ''
|
|
||||||
},
|
|
||||||
cartMessages: {
|
|
||||||
'Message 1': 'xxxx'
|
|
||||||
},
|
|
||||||
selectedNum: 1,
|
|
||||||
selectedSkuComb: {
|
|
||||||
id: 2257,
|
|
||||||
price: 100,
|
|
||||||
s1: '30349',
|
|
||||||
s2: '1193',
|
|
||||||
s3: '0',
|
|
||||||
stock_num: 111,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
k_id: 123,
|
|
||||||
k: 'More',
|
|
||||||
is_multiple: true,
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1223,
|
|
||||||
name: 'Water',
|
|
||||||
price: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
property_price: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,401 +0,0 @@
|
|||||||
# Sku 商品规格
|
|
||||||
|
|
||||||
### 引入
|
|
||||||
|
|
||||||
```js
|
|
||||||
import Vue from 'vue';
|
|
||||||
import { Sku } from 'vant';
|
|
||||||
|
|
||||||
Vue.use(Sku);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 代码演示
|
|
||||||
|
|
||||||
### 基础用法
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: false,
|
|
||||||
sku: {
|
|
||||||
// 数据结构见下方文档
|
|
||||||
},
|
|
||||||
goods: {
|
|
||||||
// 数据结构见下方文档
|
|
||||||
},
|
|
||||||
messageConfig: {
|
|
||||||
// 数据结构见下方文档
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义步进器
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
:custom-stepper-config="customStepperConfig"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 通过插槽定制
|
|
||||||
|
|
||||||
```html
|
|
||||||
<van-sku
|
|
||||||
v-model="show"
|
|
||||||
stepper-title="我要买"
|
|
||||||
:sku="sku"
|
|
||||||
:goods="goods"
|
|
||||||
:goods-id="goodsId"
|
|
||||||
:quota="quota"
|
|
||||||
:quota-used="quotaUsed"
|
|
||||||
:hide-stock="sku.hide_stock"
|
|
||||||
show-add-cart-btn
|
|
||||||
reset-stepper-on-hide
|
|
||||||
:initial-sku="initialSku"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
>
|
|
||||||
<!-- 自定义 sku-header-price -->
|
|
||||||
<template #sku-header-price="props">
|
|
||||||
<div class="van-sku__goods-price">
|
|
||||||
<span class="van-sku__price-symbol">¥</span
|
|
||||||
><span class="van-sku__price-num">{{ props.price }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 自定义 sku actions -->
|
|
||||||
<template #sku-actions="props">
|
|
||||||
<div class="van-sku-actions">
|
|
||||||
<van-button square size="large" type="warning" @click="onPointClicked">
|
|
||||||
积分兑换
|
|
||||||
</van-button>
|
|
||||||
<!-- 直接触发 sku 内部事件,通过内部事件执行 onBuyClicked 回调 -->
|
|
||||||
<van-button
|
|
||||||
square
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
@click="props.skuEventBus.$emit('sku:buy')"
|
|
||||||
>
|
|
||||||
买买买
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</van-sku>
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### Props
|
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| v-model | 是否显示商品规格弹窗 | _boolean_ | `false` |
|
|
||||||
| sku | 商品 sku 数据 | _object_ | - |
|
|
||||||
| goods | 商品信息 | _object_ | - |
|
|
||||||
| goods-id | 商品 id | _number \| string_ | - |
|
|
||||||
| price-tag | 显示在价格后面的标签 | _string_ | - |
|
|
||||||
| hide-stock | 是否显示商品剩余库存 | _boolean_ | `false` |
|
|
||||||
| hide-quota-text | 是否显示限购提示 | _boolean_ | `false` |
|
|
||||||
| hide-selected-text | 是否隐藏已选提示 | _boolean_ | `false` |
|
|
||||||
| stock-threshold | 库存阈值。低于这个值会把库存数高亮显示 | _boolean_ | `50` |
|
|
||||||
| show-add-cart-btn | 是否显示加入购物车按钮 | _boolean_ | `true` |
|
|
||||||
| buy-text | 购买按钮文字 | _string_ | `立即购买` |
|
|
||||||
| add-cart-text | 加入购物车按钮文字 | _string_ | `加入购物车` |
|
|
||||||
| quota | 限购数,0 表示不限购 | _number_ | `0` |
|
|
||||||
| quota-used | 已经购买过的数量 | _number_ | `0` |
|
|
||||||
| reset-stepper-on-hide | 隐藏时重置选择的商品数量 | _boolean_ | `false` |
|
|
||||||
| reset-selected-sku-on-hide | 隐藏时重置已选择的 sku | _boolean_ | `false` |
|
|
||||||
| disable-stepper-input | 是否禁用步进器输入 | _boolean_ | `false` |
|
|
||||||
| close-on-click-overlay | 是否在点击遮罩层后关闭 | _boolean_ | `true` |
|
|
||||||
| stepper-title | 数量选择组件左侧文案 | _string_ | `购买数量` |
|
|
||||||
| custom-stepper-config | 步进器相关自定义配置 | _object_ | `{}` |
|
|
||||||
| message-config | 留言相关配置 | _object_ | `{}` |
|
|
||||||
| get-container | 指定挂载的节点,[用法示例](#/zh-CN/popup#zhi-ding-gua-zai-wei-zhi) | _string \| () => Element_ | - |
|
|
||||||
| initial-sku | 默认选中的 sku,具体参考高级用法 | _object_ | `{}` |
|
|
||||||
| show-soldout-sku | 是否展示售罄的 sku,默认展示并置灰 | _boolean_ | `true` |
|
|
||||||
| safe-area-inset-bottom `v2.2.1` | 是否开启[底部安全区适配](#/zh-CN/quickstart#di-bu-an-quan-qu-gua-pei) | _boolean_ | `true` |
|
|
||||||
| start-sale-num `v2.3.0` | 起售数量 | _number_ | `1` |
|
|
||||||
| properties `v2.4.2` | 商品属性 | _array_ | - |
|
|
||||||
| preview-on-click-image `v2.5.2` | 是否在点击商品图片时自动预览 | _boolean_ | `true` |
|
|
||||||
| show-header-image `v2.9.0` | 是否展示头部图片 | _boolean_ | `true` |
|
|
||||||
| lazy-load `v2.9.0` | 是否开启图片懒加载,须配合 [Lazyload](#/zh-CN/lazyload) 组件使用 | _boolean_ | `false` |
|
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
| 事件名 | 说明 | 回调参数 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| add-cart | 点击添加购物车回调 | skuData: object |
|
|
||||||
| buy-clicked | 点击购买回调 | skuData: object |
|
|
||||||
| stepper-change | 购买数量变化时触发 | value: number |
|
|
||||||
| sku-selected | 切换规格类目时触发 | { skuValue, selectedSku, selectedSkuComb } |
|
|
||||||
| sku-prop-selected | 切换商品属性时触发 | { propValue, selectedProp, selectedSkuComb } |
|
|
||||||
| open-preview | 打开商品图片预览时触发 | data: object |
|
|
||||||
| close-preview | 关闭商品图片预览时触发 | data: object |
|
|
||||||
| sku-reset `v2.8.1` | 规格和属性被重置时触发 | { selectedSku, selectedProp, selectedSkuComb } |
|
|
||||||
|
|
||||||
### 方法
|
|
||||||
|
|
||||||
通过 ref 可以获取到 Sku 实例并调用实例方法,详见[组件实例方法](#/zh-CN/quickstart#zu-jian-shi-li-fang-fa)
|
|
||||||
|
|
||||||
| 方法名 | 说明 | 参数 | 返回值 |
|
|
||||||
| ------------------------- | ---------------------- | ---- | ------- |
|
|
||||||
| getSkuData | 获取当前 skuData | - | skuData |
|
|
||||||
| resetSelectedSku `v2.3.0` | 重置选中规格到初始状态 | - | - |
|
|
||||||
|
|
||||||
### Slots
|
|
||||||
|
|
||||||
Sku 组件默认划分好了若干区块,这些区块都定义成了插槽,可以按需进行替换。区块顺序见下表:
|
|
||||||
|
|
||||||
| 名称 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| sku-header | 商品信息展示区,包含商品图片、名称、价格等信息 |
|
|
||||||
| sku-header-price | 自定义 sku 头部价格展示 |
|
|
||||||
| sku-header-origin-price | 自定义 sku 头部原价展示 |
|
|
||||||
| sku-header-extra | 额外 sku 头部区域 |
|
|
||||||
| sku-header-image-extra `v2.5.2` | 自定义 sku 头部图片额外的展示 |
|
|
||||||
| sku-body-top | sku 展示区上方的内容,无默认展示内容,按需使用 |
|
|
||||||
| sku-group | 商品 sku 展示区 |
|
|
||||||
| extra-sku-group | 额外商品 sku 展示区,一般用不到 |
|
|
||||||
| sku-stepper | 商品数量选择区 |
|
|
||||||
| sku-messages | 商品留言区 |
|
|
||||||
| sku-actions-top `v2.4.7` | 操作按钮区顶部内容,无默认展示内容,按需使用 |
|
|
||||||
| sku-actions | 操作按钮区 |
|
|
||||||
|
|
||||||
### sku 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
sku: {
|
|
||||||
// 所有sku规格类目与其值的从属关系,比如商品有颜色和尺码两大类规格,颜色下面又有红色和蓝色两个规格值。
|
|
||||||
// 可以理解为一个商品可以有多个规格类目,一个规格类目下可以有多个规格值。
|
|
||||||
tree: [
|
|
||||||
{
|
|
||||||
k: '颜色', // skuKeyName:规格类目名称
|
|
||||||
k_s: 's1', // skuKeyStr:sku 组合列表(下方 list)中当前类目对应的 key 值,value 值会是从属于当前类目的一个规格值 id
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: '1', // skuValueId:规格值 id
|
|
||||||
name: '红色', // skuValueName:规格值名称
|
|
||||||
imgUrl: 'https://img.yzcdn.cn/1.jpg', // 规格类目图片,只有第一个规格类目可以定义图片
|
|
||||||
previewImgUrl: 'https://img.yzcdn.cn/1p.jpg', // 用于预览显示的规格类目图片
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: '蓝色',
|
|
||||||
imgUrl: 'https://img.yzcdn.cn/2.jpg',
|
|
||||||
previewImgUrl: 'https://img.yzcdn.cn/2p.jpg',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
largeImageMode: true, // 是否展示大图模式
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 所有 sku 的组合列表,比如红色、M 码为一个 sku 组合,红色、S 码为另一个组合
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
id: 2259, // skuId
|
|
||||||
s1: '1', // 规格类目 k_s 为 s1 的对应规格值 id
|
|
||||||
s2: '1', // 规格类目 k_s 为 s2 的对应规格值 id
|
|
||||||
price: 100, // 价格(单位分)
|
|
||||||
stock_num: 110 // 当前 sku 组合对应的库存
|
|
||||||
}
|
|
||||||
],
|
|
||||||
price: '1.00', // 默认价格(单位元)
|
|
||||||
stock_num: 227, // 商品总库存
|
|
||||||
collection_id: 2261, // 无规格商品 skuId 取 collection_id,否则取所选 sku 组合对应的 id
|
|
||||||
none_sku: false, // 是否无规格商品
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
// 商品留言
|
|
||||||
datetime: '0', // 留言类型为 time 时,是否含日期。'1' 表示包含
|
|
||||||
multiple: '0', // 留言类型为 text 时,是否多行文本。'1' 表示多行
|
|
||||||
name: '留言', // 留言名称
|
|
||||||
type: 'text', // 留言类型,可选: id_no(身份证), text, tel, date, time, email
|
|
||||||
required: '1', // 是否必填 '1' 表示必填
|
|
||||||
placeholder: '' // 可选值,占位文本
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hide_stock: false // 是否隐藏剩余库存
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### properties 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
[
|
|
||||||
// 商品属性
|
|
||||||
{
|
|
||||||
k_id: 123, // 属性id
|
|
||||||
k: '加料', // 属性名
|
|
||||||
is_multiple: true, // 是否可多选
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1222, // 属性值id
|
|
||||||
name: '珍珠', // 属性值名
|
|
||||||
price: 1, // 属性值加价
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1223,
|
|
||||||
name: '椰果',
|
|
||||||
price: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### initialSku 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
// 键:skuKeyStr(sku 组合列表中当前类目对应的 key 值)
|
|
||||||
// 值:skuValueId(规格值 id)
|
|
||||||
s1: '1',
|
|
||||||
s2: '1',
|
|
||||||
// 初始选中数量
|
|
||||||
selectedNum: 3,
|
|
||||||
// 初始选中的商品属性
|
|
||||||
// 键:属性id
|
|
||||||
// 值:属性值id列表
|
|
||||||
selectedProp: {
|
|
||||||
123: [1222]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### goods 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
goods: {
|
|
||||||
// 默认商品 sku 缩略图
|
|
||||||
picture: 'https://img.yzcdn.cn/1.jpg';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### customStepperConfig 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
customStepperConfig: {
|
|
||||||
// 自定义限购文案
|
|
||||||
quotaText: '每次限购xxx件',
|
|
||||||
// 自定义步进器超过限制时的回调
|
|
||||||
handleOverLimit: (data) => {
|
|
||||||
const { action, limitType, quota, quotaUsed, startSaleNum } = data;
|
|
||||||
|
|
||||||
if (action === 'minus') {
|
|
||||||
Toast(startSaleNum > 1 ? `${startSaleNum}件起售` : '至少选择一件商品');
|
|
||||||
} else if (action === 'plus') {
|
|
||||||
// const { LIMIT_TYPE } = Sku.skuConstants;
|
|
||||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
|
||||||
let msg = `单次限购${quota}件`;
|
|
||||||
if (quotaUsed > 0) msg += `,你已购买${quotaUsed}`;
|
|
||||||
Toast(msg);
|
|
||||||
} else {
|
|
||||||
Toast('库存不够了');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 步进器变化的回调
|
|
||||||
handleStepperChange: currentValue => {},
|
|
||||||
// 库存
|
|
||||||
stockNum: 1999,
|
|
||||||
// 格式化库存
|
|
||||||
stockFormatter: stockNum => {},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### messageConfig 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
messageConfig: {
|
|
||||||
// 图片上传回调,需要返回一个promise,promise正确执行的结果需要是一个图片url
|
|
||||||
uploadImg: () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 最大上传体积 (MB)
|
|
||||||
uploadMaxSize: 3,
|
|
||||||
// placeholder 配置
|
|
||||||
placeholderMap: {
|
|
||||||
text: 'xxx',
|
|
||||||
tel: 'xxx',
|
|
||||||
...
|
|
||||||
},
|
|
||||||
// 初始留言信息
|
|
||||||
// 键:留言 name
|
|
||||||
// 值:留言内容
|
|
||||||
initialMessages: {
|
|
||||||
留言: '留言信息'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 添加购物车和点击购买回调函数接收的 skuData 对象结构
|
|
||||||
|
|
||||||
```js
|
|
||||||
skuData: {
|
|
||||||
// 商品 id
|
|
||||||
goodsId: '946755',
|
|
||||||
// 留言信息
|
|
||||||
messages: {
|
|
||||||
message_0: '12',
|
|
||||||
message_1: ''
|
|
||||||
},
|
|
||||||
// 另一种格式的留言,key 不同
|
|
||||||
cartMessages: {
|
|
||||||
'留言1': 'xxxx'
|
|
||||||
},
|
|
||||||
// 选择的商品数量
|
|
||||||
selectedNum: 1,
|
|
||||||
// 选择的 sku 组合
|
|
||||||
selectedSkuComb: {
|
|
||||||
id: 2257,
|
|
||||||
price: 100,
|
|
||||||
s1: '30349',
|
|
||||||
s2: '1193',
|
|
||||||
s3: '0',
|
|
||||||
stock_num: 111,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
k_id: 123,
|
|
||||||
k: '加料',
|
|
||||||
is_multiple: true,
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1223,
|
|
||||||
name: '椰果',
|
|
||||||
price: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
property_price: 1
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
807
src/sku/Sku.js
807
src/sku/Sku.js
@ -1,807 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import Popup from '../popup';
|
|
||||||
import Toast from '../toast';
|
|
||||||
import ImagePreview from '../image-preview';
|
|
||||||
import SkuHeader from './components/SkuHeader';
|
|
||||||
import SkuHeaderItem from './components/SkuHeaderItem';
|
|
||||||
import SkuRow from './components/SkuRow';
|
|
||||||
import SkuRowItem from './components/SkuRowItem';
|
|
||||||
import SkuRowPropItem from './components/SkuRowPropItem';
|
|
||||||
import SkuStepper from './components/SkuStepper';
|
|
||||||
import SkuMessages from './components/SkuMessages';
|
|
||||||
import SkuActions from './components/SkuActions';
|
|
||||||
import { createNamespace, isDef } from '../utils';
|
|
||||||
import {
|
|
||||||
isAllSelected,
|
|
||||||
isSkuChoosable,
|
|
||||||
getSkuComb,
|
|
||||||
getSelectedSkuValues,
|
|
||||||
getSelectedPropValues,
|
|
||||||
getSelectedProperties,
|
|
||||||
} from './utils/sku-helper';
|
|
||||||
import { LIMIT_TYPE, UNSELECTED_SKU_VALUE_ID } from './constants';
|
|
||||||
|
|
||||||
const namespace = createNamespace('sku');
|
|
||||||
const [createComponent, bem, t] = namespace;
|
|
||||||
const { QUOTA_LIMIT } = LIMIT_TYPE;
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
sku: Object,
|
|
||||||
goods: Object,
|
|
||||||
value: Boolean,
|
|
||||||
buyText: String,
|
|
||||||
goodsId: [Number, String],
|
|
||||||
priceTag: String,
|
|
||||||
lazyLoad: Boolean,
|
|
||||||
hideStock: Boolean,
|
|
||||||
properties: Array,
|
|
||||||
addCartText: String,
|
|
||||||
stepperTitle: String,
|
|
||||||
getContainer: [String, Function],
|
|
||||||
hideQuotaText: Boolean,
|
|
||||||
hideSelectedText: Boolean,
|
|
||||||
resetStepperOnHide: Boolean,
|
|
||||||
customSkuValidator: Function,
|
|
||||||
disableStepperInput: Boolean,
|
|
||||||
resetSelectedSkuOnHide: Boolean,
|
|
||||||
quota: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
quotaUsed: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
startSaleNum: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
initialSku: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
stockThreshold: {
|
|
||||||
type: Number,
|
|
||||||
default: 50,
|
|
||||||
},
|
|
||||||
showSoldoutSku: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showAddCartBtn: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
customStepperConfig: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
showHeaderImage: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
previewOnClickImage: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
safeAreaInsetBottom: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
closeOnClickOverlay: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
bodyOffsetTop: {
|
|
||||||
type: Number,
|
|
||||||
default: 200,
|
|
||||||
},
|
|
||||||
messageConfig: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
initialMessages: {},
|
|
||||||
placeholderMap: {},
|
|
||||||
uploadImg: () => Promise.resolve(),
|
|
||||||
uploadMaxSize: 5,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedSku: {},
|
|
||||||
selectedProp: {},
|
|
||||||
selectedNum: 1,
|
|
||||||
show: this.value,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
show(val) {
|
|
||||||
this.$emit('input', val);
|
|
||||||
|
|
||||||
if (!val) {
|
|
||||||
this.$emit('sku-close', {
|
|
||||||
selectedSkuValues: this.selectedSkuValues,
|
|
||||||
selectedNum: this.selectedNum,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.resetStepperOnHide) {
|
|
||||||
this.resetStepper();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.resetSelectedSkuOnHide) {
|
|
||||||
this.resetSelectedSku();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
value(val) {
|
|
||||||
this.show = val;
|
|
||||||
},
|
|
||||||
|
|
||||||
skuTree: 'resetSelectedSku',
|
|
||||||
|
|
||||||
initialSku() {
|
|
||||||
this.resetStepper();
|
|
||||||
this.resetSelectedSku();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
skuGroupClass() {
|
|
||||||
return [
|
|
||||||
'van-sku-group-container',
|
|
||||||
{
|
|
||||||
'van-sku-group-container--hide-soldout': !this.showSoldoutSku,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
bodyStyle() {
|
|
||||||
if (this.$isServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxHeight = window.innerHeight - this.bodyOffsetTop;
|
|
||||||
|
|
||||||
return {
|
|
||||||
maxHeight: maxHeight + 'px',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
isSkuCombSelected() {
|
|
||||||
// SKU 未选完
|
|
||||||
if (this.hasSku && !isAllSelected(this.skuTree, this.selectedSku)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 属性未全选
|
|
||||||
return !this.propList.some(
|
|
||||||
(it) => (this.selectedProp[it.k_id] || []).length < 1
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
isSkuEmpty() {
|
|
||||||
return Object.keys(this.sku).length === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasSku() {
|
|
||||||
return !this.sku.none_sku;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasSkuOrAttr() {
|
|
||||||
return this.hasSku || this.propList.length > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedSkuComb() {
|
|
||||||
let skuComb = null;
|
|
||||||
if (this.isSkuCombSelected) {
|
|
||||||
if (this.hasSku) {
|
|
||||||
skuComb = getSkuComb(this.skuList, this.selectedSku);
|
|
||||||
} else {
|
|
||||||
skuComb = {
|
|
||||||
id: this.sku.collection_id,
|
|
||||||
price: Math.round(this.sku.price * 100),
|
|
||||||
stock_num: this.sku.stock_num,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skuComb) {
|
|
||||||
skuComb.properties = getSelectedProperties(
|
|
||||||
this.propList,
|
|
||||||
this.selectedProp
|
|
||||||
);
|
|
||||||
skuComb.property_price = this.selectedPropValues.reduce(
|
|
||||||
(acc, cur) => acc + (cur.price || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return skuComb;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedSkuValues() {
|
|
||||||
return getSelectedSkuValues(this.skuTree, this.selectedSku);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedPropValues() {
|
|
||||||
return getSelectedPropValues(this.propList, this.selectedProp);
|
|
||||||
},
|
|
||||||
|
|
||||||
price() {
|
|
||||||
if (this.selectedSkuComb) {
|
|
||||||
return (
|
|
||||||
(this.selectedSkuComb.price + this.selectedSkuComb.property_price) /
|
|
||||||
100
|
|
||||||
).toFixed(2);
|
|
||||||
}
|
|
||||||
// sku.price是一个格式化好的价格区间
|
|
||||||
return this.sku.price;
|
|
||||||
},
|
|
||||||
|
|
||||||
originPrice() {
|
|
||||||
if (this.selectedSkuComb && this.selectedSkuComb.origin_price) {
|
|
||||||
return (
|
|
||||||
(this.selectedSkuComb.origin_price +
|
|
||||||
this.selectedSkuComb.property_price) /
|
|
||||||
100
|
|
||||||
).toFixed(2);
|
|
||||||
}
|
|
||||||
return this.sku.origin_price;
|
|
||||||
},
|
|
||||||
|
|
||||||
skuTree() {
|
|
||||||
return this.sku.tree || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
skuList() {
|
|
||||||
return this.sku.list || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
propList() {
|
|
||||||
return this.properties || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
imageList() {
|
|
||||||
const imageList = [this.goods.picture];
|
|
||||||
|
|
||||||
if (this.skuTree.length > 0) {
|
|
||||||
this.skuTree.forEach((treeItem) => {
|
|
||||||
if (!treeItem.v) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
treeItem.v.forEach((vItem) => {
|
|
||||||
const imgUrl = vItem.previewImgUrl || vItem.imgUrl || vItem.img_url;
|
|
||||||
|
|
||||||
if (imgUrl && imageList.indexOf(imgUrl) === -1) {
|
|
||||||
imageList.push(imgUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageList;
|
|
||||||
},
|
|
||||||
|
|
||||||
stock() {
|
|
||||||
const { stockNum } = this.customStepperConfig;
|
|
||||||
if (stockNum !== undefined) {
|
|
||||||
return stockNum;
|
|
||||||
}
|
|
||||||
if (this.selectedSkuComb) {
|
|
||||||
return this.selectedSkuComb.stock_num;
|
|
||||||
}
|
|
||||||
return this.sku.stock_num;
|
|
||||||
},
|
|
||||||
|
|
||||||
stockText() {
|
|
||||||
const { stockFormatter } = this.customStepperConfig;
|
|
||||||
if (stockFormatter) {
|
|
||||||
return stockFormatter(this.stock);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
`${t('stock')} `,
|
|
||||||
<span
|
|
||||||
class={bem('stock-num', {
|
|
||||||
highlight: this.stock < this.stockThreshold,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.stock}
|
|
||||||
</span>,
|
|
||||||
` ${t('stockUnit')}`,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedText() {
|
|
||||||
if (this.selectedSkuComb) {
|
|
||||||
const values = this.selectedSkuValues.concat(this.selectedPropValues);
|
|
||||||
return `${t('selected')} ${values.map((item) => item.name).join(' ')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unselectedSku = this.skuTree
|
|
||||||
.filter(
|
|
||||||
(item) => this.selectedSku[item.k_s] === UNSELECTED_SKU_VALUE_ID
|
|
||||||
)
|
|
||||||
.map((item) => item.k);
|
|
||||||
|
|
||||||
const unselectedProp = this.propList
|
|
||||||
.filter((item) => (this.selectedProp[item.k_id] || []).length < 1)
|
|
||||||
.map((item) => item.k);
|
|
||||||
|
|
||||||
return `${t('select')} ${unselectedSku.concat(unselectedProp).join(' ')}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const skuEventBus = new Vue();
|
|
||||||
this.skuEventBus = skuEventBus;
|
|
||||||
|
|
||||||
skuEventBus.$on('sku:select', this.onSelect);
|
|
||||||
skuEventBus.$on('sku:propSelect', this.onPropSelect);
|
|
||||||
skuEventBus.$on('sku:numChange', this.onNumChange);
|
|
||||||
skuEventBus.$on('sku:previewImage', this.onPreviewImage);
|
|
||||||
skuEventBus.$on('sku:overLimit', this.onOverLimit);
|
|
||||||
skuEventBus.$on('sku:stepperState', this.onStepperState);
|
|
||||||
skuEventBus.$on('sku:addCart', this.onAddCart);
|
|
||||||
skuEventBus.$on('sku:buy', this.onBuy);
|
|
||||||
|
|
||||||
this.resetStepper();
|
|
||||||
this.resetSelectedSku();
|
|
||||||
|
|
||||||
// 组件初始化后的钩子,抛出skuEventBus
|
|
||||||
this.$emit('after-sku-create', skuEventBus);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
resetStepper() {
|
|
||||||
const { skuStepper } = this.$refs;
|
|
||||||
const { selectedNum } = this.initialSku;
|
|
||||||
const num = isDef(selectedNum) ? selectedNum : this.startSaleNum;
|
|
||||||
// 用来缓存不合法的情况
|
|
||||||
this.stepperError = null;
|
|
||||||
|
|
||||||
if (skuStepper) {
|
|
||||||
skuStepper.setCurrentNum(num);
|
|
||||||
} else {
|
|
||||||
// 当首次加载(skuStepper 为空)时,传入数量如果不合法,可能会存在问题
|
|
||||||
this.selectedNum = num;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
resetSelectedSku() {
|
|
||||||
this.selectedSku = {};
|
|
||||||
|
|
||||||
// 重置 selectedSku
|
|
||||||
this.skuTree.forEach((item) => {
|
|
||||||
this.selectedSku[item.k_s] = UNSELECTED_SKU_VALUE_ID;
|
|
||||||
});
|
|
||||||
this.skuTree.forEach((item) => {
|
|
||||||
const key = item.k_s;
|
|
||||||
// 规格值只有1个时,优先判断
|
|
||||||
const valueId =
|
|
||||||
item.v.length === 1 ? item.v[0].id : this.initialSku[key];
|
|
||||||
if (
|
|
||||||
valueId &&
|
|
||||||
isSkuChoosable(this.skuList, this.selectedSku, { key, valueId })
|
|
||||||
) {
|
|
||||||
this.selectedSku[key] = valueId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const skuValues = this.selectedSkuValues;
|
|
||||||
|
|
||||||
if (skuValues.length > 0) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$emit('sku-selected', {
|
|
||||||
skuValue: skuValues[skuValues.length - 1],
|
|
||||||
selectedSku: this.selectedSku,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置商品属性
|
|
||||||
this.selectedProp = {};
|
|
||||||
const { selectedProp = {} } = this.initialSku;
|
|
||||||
// 只有一个属性值时,默认选中,且选中外部传入信息
|
|
||||||
this.propList.forEach((item) => {
|
|
||||||
if (item.v && item.v.length === 1) {
|
|
||||||
this.selectedProp[item.k_id] = [item.v[0].id];
|
|
||||||
} else if (selectedProp[item.k_id]) {
|
|
||||||
this.selectedProp[item.k_id] = selectedProp[item.k_id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const propValues = this.selectedPropValues;
|
|
||||||
if (propValues.length > 0) {
|
|
||||||
this.$emit('sku-prop-selected', {
|
|
||||||
propValue: propValues[propValues.length - 1],
|
|
||||||
selectedProp: this.selectedProp,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抛出重置事件
|
|
||||||
this.$emit('sku-reset', {
|
|
||||||
selectedSku: this.selectedSku,
|
|
||||||
selectedProp: this.selectedProp,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.centerInitialSku();
|
|
||||||
},
|
|
||||||
|
|
||||||
getSkuMessages() {
|
|
||||||
return this.$refs.skuMessages ? this.$refs.skuMessages.getMessages() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
getSkuCartMessages() {
|
|
||||||
return this.$refs.skuMessages
|
|
||||||
? this.$refs.skuMessages.getCartMessages()
|
|
||||||
: {};
|
|
||||||
},
|
|
||||||
|
|
||||||
validateSkuMessages() {
|
|
||||||
return this.$refs.skuMessages
|
|
||||||
? this.$refs.skuMessages.validateMessages()
|
|
||||||
: '';
|
|
||||||
},
|
|
||||||
|
|
||||||
validateSku() {
|
|
||||||
if (this.selectedNum === 0) {
|
|
||||||
return t('unavailable');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isSkuCombSelected) {
|
|
||||||
return this.validateSkuMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义sku校验
|
|
||||||
if (this.customSkuValidator) {
|
|
||||||
const err = this.customSkuValidator(this);
|
|
||||||
if (err) return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return t('selectSku');
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelect(skuValue) {
|
|
||||||
// 点击已选中的sku时则取消选中
|
|
||||||
this.selectedSku =
|
|
||||||
this.selectedSku[skuValue.skuKeyStr] === skuValue.id
|
|
||||||
? {
|
|
||||||
...this.selectedSku,
|
|
||||||
[skuValue.skuKeyStr]: UNSELECTED_SKU_VALUE_ID,
|
|
||||||
}
|
|
||||||
: { ...this.selectedSku, [skuValue.skuKeyStr]: skuValue.id };
|
|
||||||
|
|
||||||
this.$emit('sku-selected', {
|
|
||||||
skuValue,
|
|
||||||
selectedSku: this.selectedSku,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onPropSelect(propValue) {
|
|
||||||
const arr = this.selectedProp[propValue.skuKeyStr] || [];
|
|
||||||
const pos = arr.indexOf(propValue.id);
|
|
||||||
|
|
||||||
if (pos > -1) {
|
|
||||||
arr.splice(pos, 1);
|
|
||||||
} else if (propValue.multiple) {
|
|
||||||
arr.push(propValue.id);
|
|
||||||
} else {
|
|
||||||
arr.splice(0, 1, propValue.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedProp = {
|
|
||||||
...this.selectedProp,
|
|
||||||
[propValue.skuKeyStr]: arr,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('sku-prop-selected', {
|
|
||||||
propValue,
|
|
||||||
selectedProp: this.selectedProp,
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onNumChange(num) {
|
|
||||||
this.selectedNum = num;
|
|
||||||
},
|
|
||||||
|
|
||||||
onPreviewImage(selectedValue) {
|
|
||||||
const { imageList } = this;
|
|
||||||
let index = 0;
|
|
||||||
let indexImage = imageList[0];
|
|
||||||
if (selectedValue && selectedValue.imgUrl) {
|
|
||||||
this.imageList.some((image, pos) => {
|
|
||||||
if (image === selectedValue.imgUrl) {
|
|
||||||
index = pos;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
indexImage = selectedValue.imgUrl;
|
|
||||||
}
|
|
||||||
const params = {
|
|
||||||
...selectedValue,
|
|
||||||
index,
|
|
||||||
imageList: this.imageList,
|
|
||||||
indexImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('open-preview', params);
|
|
||||||
|
|
||||||
if (!this.previewOnClickImage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePreview({
|
|
||||||
images: this.imageList,
|
|
||||||
startPosition: index,
|
|
||||||
onClose: () => {
|
|
||||||
this.$emit('close-preview', params);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onOverLimit(data) {
|
|
||||||
const { action, limitType, quota, quotaUsed } = data;
|
|
||||||
const { handleOverLimit } = this.customStepperConfig;
|
|
||||||
|
|
||||||
if (handleOverLimit) {
|
|
||||||
handleOverLimit(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'minus') {
|
|
||||||
if (this.startSaleNum > 1) {
|
|
||||||
Toast(t('minusStartTip', this.startSaleNum));
|
|
||||||
} else {
|
|
||||||
Toast(t('minusTip'));
|
|
||||||
}
|
|
||||||
} else if (action === 'plus') {
|
|
||||||
if (limitType === QUOTA_LIMIT) {
|
|
||||||
if (quotaUsed > 0) {
|
|
||||||
Toast(t('quotaUsedTip', quota, quotaUsed));
|
|
||||||
} else {
|
|
||||||
Toast(t('quotaTip', quota));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast(t('soldout'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onStepperState(data) {
|
|
||||||
this.stepperError = data.valid
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
...data,
|
|
||||||
action: 'plus',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onAddCart() {
|
|
||||||
this.onBuyOrAddCart('add-cart');
|
|
||||||
},
|
|
||||||
|
|
||||||
onBuy() {
|
|
||||||
this.onBuyOrAddCart('buy-clicked');
|
|
||||||
},
|
|
||||||
|
|
||||||
onBuyOrAddCart(type) {
|
|
||||||
// sku 不符合购买条件
|
|
||||||
if (this.stepperError) {
|
|
||||||
return this.onOverLimit(this.stepperError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = this.validateSku();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
Toast(error);
|
|
||||||
} else {
|
|
||||||
this.$emit(type, this.getSkuData());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
getSkuData() {
|
|
||||||
return {
|
|
||||||
goodsId: this.goodsId,
|
|
||||||
messages: this.getSkuMessages(),
|
|
||||||
selectedNum: this.selectedNum,
|
|
||||||
cartMessages: this.getSkuCartMessages(),
|
|
||||||
selectedSkuComb: this.selectedSkuComb,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// 当 popup 完全打开后执行
|
|
||||||
onOpened() {
|
|
||||||
this.centerInitialSku();
|
|
||||||
},
|
|
||||||
|
|
||||||
centerInitialSku() {
|
|
||||||
(this.$refs.skuRows || []).forEach((it) => {
|
|
||||||
const { k_s } = it.skuRow || {};
|
|
||||||
it.centerItem(this.initialSku[k_s]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.isSkuEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
sku,
|
|
||||||
skuList,
|
|
||||||
goods,
|
|
||||||
price,
|
|
||||||
lazyLoad,
|
|
||||||
originPrice,
|
|
||||||
skuEventBus,
|
|
||||||
selectedSku,
|
|
||||||
selectedProp,
|
|
||||||
selectedNum,
|
|
||||||
stepperTitle,
|
|
||||||
selectedSkuComb,
|
|
||||||
showHeaderImage,
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
const slotsProps = {
|
|
||||||
price,
|
|
||||||
originPrice,
|
|
||||||
selectedNum,
|
|
||||||
skuEventBus,
|
|
||||||
selectedSku,
|
|
||||||
selectedSkuComb,
|
|
||||||
};
|
|
||||||
|
|
||||||
const slots = (name) => this.slots(name, slotsProps);
|
|
||||||
|
|
||||||
const Header = slots('sku-header') || (
|
|
||||||
<SkuHeader
|
|
||||||
sku={sku}
|
|
||||||
goods={goods}
|
|
||||||
skuEventBus={skuEventBus}
|
|
||||||
selectedSku={selectedSku}
|
|
||||||
showHeaderImage={showHeaderImage}
|
|
||||||
>
|
|
||||||
<template slot="sku-header-image-extra">
|
|
||||||
{slots('sku-header-image-extra')}
|
|
||||||
</template>
|
|
||||||
{slots('sku-header-price') || (
|
|
||||||
<div class="van-sku__goods-price">
|
|
||||||
<span class="van-sku__price-symbol">¥</span>
|
|
||||||
<span class="van-sku__price-num">{price}</span>
|
|
||||||
{this.priceTag && (
|
|
||||||
<span class="van-sku__price-tag">{this.priceTag}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{slots('sku-header-origin-price') ||
|
|
||||||
(originPrice && (
|
|
||||||
<SkuHeaderItem>
|
|
||||||
{t('originPrice')} ¥{originPrice}
|
|
||||||
</SkuHeaderItem>
|
|
||||||
))}
|
|
||||||
{!this.hideStock && (
|
|
||||||
<SkuHeaderItem>
|
|
||||||
<span class="van-sku__stock">{this.stockText}</span>
|
|
||||||
</SkuHeaderItem>
|
|
||||||
)}
|
|
||||||
{this.hasSkuOrAttr && !this.hideSelectedText && (
|
|
||||||
<SkuHeaderItem>{this.selectedText}</SkuHeaderItem>
|
|
||||||
)}
|
|
||||||
{slots('sku-header-extra')}
|
|
||||||
</SkuHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Group =
|
|
||||||
slots('sku-group') ||
|
|
||||||
(this.hasSkuOrAttr && (
|
|
||||||
<div class={this.skuGroupClass}>
|
|
||||||
{this.skuTree.map((skuTreeItem) => (
|
|
||||||
<SkuRow skuRow={skuTreeItem} ref="skuRows" refInFor>
|
|
||||||
{skuTreeItem.v.map((skuValue) => (
|
|
||||||
<SkuRowItem
|
|
||||||
skuList={skuList}
|
|
||||||
lazyLoad={lazyLoad}
|
|
||||||
skuValue={skuValue}
|
|
||||||
skuKeyStr={skuTreeItem.k_s}
|
|
||||||
selectedSku={selectedSku}
|
|
||||||
skuEventBus={skuEventBus}
|
|
||||||
largeImageMode={skuTreeItem.largeImageMode}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SkuRow>
|
|
||||||
))}
|
|
||||||
{this.propList.map((skuTreeItem) => (
|
|
||||||
<SkuRow skuRow={skuTreeItem}>
|
|
||||||
{skuTreeItem.v.map((skuValue) => (
|
|
||||||
<SkuRowPropItem
|
|
||||||
skuValue={skuValue}
|
|
||||||
skuKeyStr={skuTreeItem.k_id + ''}
|
|
||||||
selectedProp={selectedProp}
|
|
||||||
skuEventBus={skuEventBus}
|
|
||||||
multiple={skuTreeItem.is_multiple}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SkuRow>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
const Stepper = slots('sku-stepper') || (
|
|
||||||
<SkuStepper
|
|
||||||
ref="skuStepper"
|
|
||||||
stock={this.stock}
|
|
||||||
quota={this.quota}
|
|
||||||
quotaUsed={this.quotaUsed}
|
|
||||||
startSaleNum={this.startSaleNum}
|
|
||||||
skuEventBus={skuEventBus}
|
|
||||||
selectedNum={selectedNum}
|
|
||||||
stepperTitle={stepperTitle}
|
|
||||||
skuStockNum={sku.stock_num}
|
|
||||||
disableStepperInput={this.disableStepperInput}
|
|
||||||
customStepperConfig={this.customStepperConfig}
|
|
||||||
hideQuotaText={this.hideQuotaText}
|
|
||||||
onChange={(event) => {
|
|
||||||
this.$emit('stepper-change', event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Messages = slots('sku-messages') || (
|
|
||||||
<SkuMessages
|
|
||||||
ref="skuMessages"
|
|
||||||
goodsId={this.goodsId}
|
|
||||||
messageConfig={this.messageConfig}
|
|
||||||
messages={sku.messages}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Actions = slots('sku-actions') || (
|
|
||||||
<SkuActions
|
|
||||||
buyText={this.buyText}
|
|
||||||
skuEventBus={skuEventBus}
|
|
||||||
addCartText={this.addCartText}
|
|
||||||
showAddCartBtn={this.showAddCartBtn}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popup
|
|
||||||
vModel={this.show}
|
|
||||||
round
|
|
||||||
closeable
|
|
||||||
position="bottom"
|
|
||||||
class="van-sku-container"
|
|
||||||
getContainer={this.getContainer}
|
|
||||||
closeOnClickOverlay={this.closeOnClickOverlay}
|
|
||||||
safeAreaInsetBottom={this.safeAreaInsetBottom}
|
|
||||||
onOpened={this.onOpened}
|
|
||||||
>
|
|
||||||
{Header}
|
|
||||||
<div class="van-sku-body" style={this.bodyStyle}>
|
|
||||||
{slots('sku-body-top')}
|
|
||||||
{Group}
|
|
||||||
{slots('extra-sku-group')}
|
|
||||||
{Stepper}
|
|
||||||
{Messages}
|
|
||||||
</div>
|
|
||||||
{slots('sku-actions-top')}
|
|
||||||
{Actions}
|
|
||||||
</Popup>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,58 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { inherit } from '../../utils/functional';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Button from '../../button';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { DefaultSlots } from '../../utils/types';
|
|
||||||
import Vue, { CreateElement, RenderContext } from 'vue/types';
|
|
||||||
|
|
||||||
export type SkuActionsProps = {
|
|
||||||
buyText?: string;
|
|
||||||
skuEventBus: Vue;
|
|
||||||
addCartText?: string;
|
|
||||||
showAddCartBtn?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [createComponent, bem, t] = createNamespace('sku-actions');
|
|
||||||
|
|
||||||
function SkuActions(
|
|
||||||
h: CreateElement,
|
|
||||||
props: SkuActionsProps,
|
|
||||||
slots: DefaultSlots,
|
|
||||||
ctx: RenderContext<SkuActionsProps>
|
|
||||||
) {
|
|
||||||
const createEmitter = (name: string) => () => {
|
|
||||||
props.skuEventBus.$emit(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={bem()} {...inherit(ctx)}>
|
|
||||||
{props.showAddCartBtn && (
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
type="warning"
|
|
||||||
text={props.addCartText || t('addCart')}
|
|
||||||
onClick={createEmitter('sku:addCart')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
text={props.buyText || t('buy')}
|
|
||||||
onClick={createEmitter('sku:buy')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SkuActions.props = {
|
|
||||||
buyText: String,
|
|
||||||
addCartText: String,
|
|
||||||
skuEventBus: Object,
|
|
||||||
showAddCartBtn: Boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createComponent<SkuActionsProps>(SkuActions);
|
|
@ -1,107 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { stringToDate, dateToString } from '../utils/time-helper';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Popup from '../../popup';
|
|
||||||
import DateTimePicker from '../../datetime-picker';
|
|
||||||
import Field from '../../field';
|
|
||||||
|
|
||||||
const namespace = createNamespace('sku-datetime-field');
|
|
||||||
const createComponent = namespace[0];
|
|
||||||
const t = namespace[2];
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
value: String,
|
|
||||||
label: String,
|
|
||||||
required: Boolean,
|
|
||||||
placeholder: String,
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'date',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showDatePicker: false,
|
|
||||||
currentDate: this.type === 'time' ? '' : new Date(),
|
|
||||||
minDate: new Date(new Date().getFullYear() - 60, 0, 1),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
switch (this.type) {
|
|
||||||
case 'time':
|
|
||||||
this.currentDate = val;
|
|
||||||
break;
|
|
||||||
case 'date':
|
|
||||||
case 'datetime':
|
|
||||||
this.currentDate = stringToDate(val) || new Date();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
title() {
|
|
||||||
return t(`title.${this.type}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.showDatePicker = true;
|
|
||||||
},
|
|
||||||
onConfirm(val) {
|
|
||||||
let data = val;
|
|
||||||
if (this.type !== 'time') {
|
|
||||||
data = dateToString(val, this.type);
|
|
||||||
}
|
|
||||||
this.$emit('input', data);
|
|
||||||
this.showDatePicker = false;
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
this.showDatePicker = false;
|
|
||||||
},
|
|
||||||
formatter(type, val) {
|
|
||||||
const word = t(`format.${type}`);
|
|
||||||
return `${val}${word}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
center
|
|
||||||
value={this.value}
|
|
||||||
label={this.label}
|
|
||||||
required={this.required}
|
|
||||||
placeholder={this.placeholder}
|
|
||||||
onClick={this.onClick}
|
|
||||||
>
|
|
||||||
<Popup
|
|
||||||
vModel={this.showDatePicker}
|
|
||||||
round
|
|
||||||
slot="extra"
|
|
||||||
position="bottom"
|
|
||||||
getContainer="body"
|
|
||||||
>
|
|
||||||
<DateTimePicker
|
|
||||||
type={this.type}
|
|
||||||
title={this.title}
|
|
||||||
value={this.currentDate}
|
|
||||||
minDate={this.minDate}
|
|
||||||
formatter={this.formatter}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
onConfirm={this.onConfirm}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,110 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { inherit } from '../../utils/functional';
|
|
||||||
import { BORDER_BOTTOM } from '../../utils/constant';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Image from '../../image';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import Vue, { CreateElement, RenderContext } from 'vue/types';
|
|
||||||
import { DefaultSlots, ScopedSlot } from '../../utils/types';
|
|
||||||
import { SkuData, SkuGoodsData, SelectedSkuData } from '../../../types/sku';
|
|
||||||
|
|
||||||
export type SkuHeaderProps = {
|
|
||||||
sku: SkuData;
|
|
||||||
goods: SkuGoodsData;
|
|
||||||
skuEventBus: Vue;
|
|
||||||
selectedSku: SelectedSkuData;
|
|
||||||
showHeaderImage: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SkuHeaderSlots = DefaultSlots & {
|
|
||||||
'sku-header-image-extra'?: ScopedSlot;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SelectedValueType = {
|
|
||||||
ks: string;
|
|
||||||
imgUrl: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [createComponent, bem] = createNamespace('sku-header');
|
|
||||||
|
|
||||||
function getSkuImgValue(
|
|
||||||
sku: SkuData,
|
|
||||||
selectedSku: SelectedSkuData
|
|
||||||
): SelectedValueType | undefined {
|
|
||||||
let imgValue;
|
|
||||||
|
|
||||||
sku.tree.some((item) => {
|
|
||||||
const id = selectedSku[item.k_s];
|
|
||||||
|
|
||||||
if (id && item.v) {
|
|
||||||
const matchedSku =
|
|
||||||
item.v.filter((skuValue) => skuValue.id === id)[0] || {};
|
|
||||||
|
|
||||||
const img =
|
|
||||||
matchedSku.previewImgUrl || matchedSku.imgUrl || matchedSku.img_url;
|
|
||||||
if (img) {
|
|
||||||
imgValue = {
|
|
||||||
...matchedSku,
|
|
||||||
ks: item.k_s,
|
|
||||||
imgUrl: img,
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return imgValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SkuHeader(
|
|
||||||
h: CreateElement,
|
|
||||||
props: SkuHeaderProps,
|
|
||||||
slots: SkuHeaderSlots,
|
|
||||||
ctx: RenderContext<SkuHeaderProps>
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
sku,
|
|
||||||
goods,
|
|
||||||
skuEventBus,
|
|
||||||
selectedSku,
|
|
||||||
showHeaderImage = true,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const selectedValue = getSkuImgValue(sku, selectedSku);
|
|
||||||
const imgUrl = selectedValue ? selectedValue.imgUrl : goods.picture;
|
|
||||||
|
|
||||||
const previewImage = () => {
|
|
||||||
skuEventBus.$emit('sku:previewImage', selectedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={[bem(), BORDER_BOTTOM]} {...inherit(ctx)}>
|
|
||||||
{showHeaderImage && (
|
|
||||||
<Image
|
|
||||||
fit="cover"
|
|
||||||
src={imgUrl}
|
|
||||||
class={bem('img-wrap')}
|
|
||||||
onClick={previewImage}
|
|
||||||
>
|
|
||||||
{slots['sku-header-image-extra']?.()}
|
|
||||||
</Image>
|
|
||||||
)}
|
|
||||||
<div class={bem('goods-info')}>{slots.default?.()}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SkuHeader.props = {
|
|
||||||
sku: Object,
|
|
||||||
goods: Object,
|
|
||||||
skuEventBus: Object,
|
|
||||||
selectedSku: Object,
|
|
||||||
showHeaderImage: Boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createComponent<SkuHeaderProps>(SkuHeader);
|
|
@ -1,26 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { inherit } from '../../utils/functional';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { CreateElement, RenderContext } from 'vue/types';
|
|
||||||
import { DefaultSlots } from '../../utils/types';
|
|
||||||
|
|
||||||
export type SkuHeaderItemProps = {};
|
|
||||||
|
|
||||||
const [createComponent, bem] = createNamespace('sku-header-item');
|
|
||||||
|
|
||||||
function SkuHeader(
|
|
||||||
h: CreateElement,
|
|
||||||
props: SkuHeaderItemProps,
|
|
||||||
slots: DefaultSlots,
|
|
||||||
ctx: RenderContext<SkuHeaderItemProps>
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div class={bem()} {...inherit(ctx)}>
|
|
||||||
{slots.default && slots.default()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createComponent<SkuHeaderItemProps>(SkuHeader);
|
|
@ -1,73 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Uploader from '../../uploader';
|
|
||||||
|
|
||||||
const namespace = createNamespace('sku-img-uploader');
|
|
||||||
const createComponent = namespace[0];
|
|
||||||
const t = namespace[2];
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
value: String,
|
|
||||||
uploadImg: Function,
|
|
||||||
maxSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
fileList: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
if (val) {
|
|
||||||
this.fileList = [{ url: val, isImage: true }];
|
|
||||||
} else {
|
|
||||||
this.fileList = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
afterReadFile(file) {
|
|
||||||
file.status = 'uploading';
|
|
||||||
file.message = t('uploading');
|
|
||||||
this.uploadImg(file.file, file.content)
|
|
||||||
.then((img) => {
|
|
||||||
file.status = 'done';
|
|
||||||
this.$emit('input', img);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
file.status = 'failed';
|
|
||||||
file.message = t('fail');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onOversize() {
|
|
||||||
this.$toast(t('oversize', this.maxSize));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDelete() {
|
|
||||||
this.$emit('input', '');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Uploader
|
|
||||||
vModel={this.fileList}
|
|
||||||
maxCount={1}
|
|
||||||
afterRead={this.afterReadFile}
|
|
||||||
maxSize={this.maxSize * 1024 * 1024}
|
|
||||||
onOversize={this.onOversize}
|
|
||||||
onDelete={this.onDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,184 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { isEmail } from '../../utils/validate/email';
|
|
||||||
import { isNumeric } from '../../utils/validate/number';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Cell from '../../cell';
|
|
||||||
import Field from '../../field';
|
|
||||||
import SkuImgUploader from './SkuImgUploader';
|
|
||||||
import SkuDateTimeField from './SkuDateTimeField';
|
|
||||||
|
|
||||||
const [createComponent, bem, t] = createNamespace('sku-messages');
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
messageConfig: Object,
|
|
||||||
goodsId: [Number, String],
|
|
||||||
messages: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
messageValues: this.resetMessageValues(this.messages),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
messages(val) {
|
|
||||||
this.messageValues = this.resetMessageValues(val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
resetMessageValues(messages) {
|
|
||||||
const { messageConfig } = this;
|
|
||||||
const { initialMessages = {} } = messageConfig;
|
|
||||||
return (messages || []).map((message) => ({
|
|
||||||
value: initialMessages[message.name] || '',
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getType(message) {
|
|
||||||
if (+message.multiple === 1) {
|
|
||||||
return 'textarea';
|
|
||||||
}
|
|
||||||
if (message.type === 'id_no') {
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
return message.datetime > 0 ? 'datetime' : message.type;
|
|
||||||
},
|
|
||||||
|
|
||||||
getMessages() {
|
|
||||||
const messages = {};
|
|
||||||
|
|
||||||
this.messageValues.forEach((item, index) => {
|
|
||||||
messages[`message_${index}`] = item.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCartMessages() {
|
|
||||||
const messages = {};
|
|
||||||
|
|
||||||
this.messageValues.forEach((item, index) => {
|
|
||||||
const message = this.messages[index];
|
|
||||||
messages[message.name] = item.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPlaceholder(message) {
|
|
||||||
const type = +message.multiple === 1 ? 'textarea' : message.type;
|
|
||||||
const map = this.messageConfig.placeholderMap || {};
|
|
||||||
return message.placeholder || map[type] || t(`placeholder.${type}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
validateMessages() {
|
|
||||||
const values = this.messageValues;
|
|
||||||
|
|
||||||
for (let i = 0; i < values.length; i++) {
|
|
||||||
const { value } = values[i];
|
|
||||||
const message = this.messages[i];
|
|
||||||
|
|
||||||
if (value === '') {
|
|
||||||
// 必填字段的校验
|
|
||||||
if (String(message.required) === '1') {
|
|
||||||
const textType = t(message.type === 'image' ? 'upload' : 'fill');
|
|
||||||
return textType + message.name;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (message.type === 'tel' && !isNumeric(value)) {
|
|
||||||
return t('invalid.tel');
|
|
||||||
}
|
|
||||||
if (message.type === 'mobile' && !/^\d{6,20}$/.test(value)) {
|
|
||||||
return t('invalid.mobile');
|
|
||||||
}
|
|
||||||
if (message.type === 'email' && !isEmail(value)) {
|
|
||||||
return t('invalid.email');
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
message.type === 'id_no' &&
|
|
||||||
(value.length < 15 || value.length > 18)
|
|
||||||
) {
|
|
||||||
return t('invalid.id_no');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The phone number copied from IOS mobile phone address book
|
|
||||||
* will add spaces and invisible Unicode characters
|
|
||||||
* which cannot pass the /^\d+$/ verification
|
|
||||||
* so keep numbers and dots
|
|
||||||
*/
|
|
||||||
getFormatter(message) {
|
|
||||||
return function formatter(value) {
|
|
||||||
if (message.type === 'mobile' || message.type === 'tel') {
|
|
||||||
return value.replace(/[^\d.]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
genMessage(message, index) {
|
|
||||||
if (message.type === 'image') {
|
|
||||||
return (
|
|
||||||
<Cell
|
|
||||||
key={`${this.goodsId}-${index}`}
|
|
||||||
title={message.name}
|
|
||||||
class={bem('image-cell')}
|
|
||||||
required={String(message.required) === '1'}
|
|
||||||
valueClass={bem('image-cell-value')}
|
|
||||||
>
|
|
||||||
<SkuImgUploader
|
|
||||||
vModel={this.messageValues[index].value}
|
|
||||||
maxSize={this.messageConfig.uploadMaxSize}
|
|
||||||
uploadImg={this.messageConfig.uploadImg}
|
|
||||||
/>
|
|
||||||
<div class={bem('image-cell-label')}>{t('imageLabel')}</div>
|
|
||||||
</Cell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间和日期使用的vant选择器
|
|
||||||
const isDateOrTime = ['date', 'time'].indexOf(message.type) > -1;
|
|
||||||
if (isDateOrTime) {
|
|
||||||
return (
|
|
||||||
<SkuDateTimeField
|
|
||||||
vModel={this.messageValues[index].value}
|
|
||||||
label={message.name}
|
|
||||||
key={`${this.goodsId}-${index}`}
|
|
||||||
required={String(message.required) === '1'}
|
|
||||||
placeholder={this.getPlaceholder(message)}
|
|
||||||
type={this.getType(message)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
vModel={this.messageValues[index].value}
|
|
||||||
maxlength="200"
|
|
||||||
center={!message.multiple}
|
|
||||||
label={message.name}
|
|
||||||
key={`${this.goodsId}-${index}`}
|
|
||||||
required={String(message.required) === '1'}
|
|
||||||
placeholder={this.getPlaceholder(message)}
|
|
||||||
type={this.getType(message)}
|
|
||||||
formatter={this.getFormatter(message)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div class={bem()}>{this.messages.map(this.genMessage)}</div>;
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,122 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { BORDER_BOTTOM } from '../../utils/constant';
|
|
||||||
// Mixins
|
|
||||||
import { ParentMixin } from '../../mixins/relation';
|
|
||||||
import { BindEventMixin } from '../../mixins/bind-event';
|
|
||||||
|
|
||||||
const [createComponent, bem, t] = createNamespace('sku-row');
|
|
||||||
|
|
||||||
export { bem };
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
mixins: [
|
|
||||||
ParentMixin('vanSkuRows'),
|
|
||||||
BindEventMixin(function (bind) {
|
|
||||||
if (this.scrollable && this.$refs.scroller) {
|
|
||||||
bind(this.$refs.scroller, 'scroll', this.onScroll);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
skuRow: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
progress: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
scrollable() {
|
|
||||||
return this.skuRow.largeImageMode && this.skuRow.v.length > 6;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onScroll() {
|
|
||||||
const { scroller, row } = this.$refs;
|
|
||||||
const distance = row.offsetWidth - scroller.offsetWidth;
|
|
||||||
this.progress = scroller.scrollLeft / distance;
|
|
||||||
},
|
|
||||||
|
|
||||||
genTitle() {
|
|
||||||
return (
|
|
||||||
<div class={bem('title')}>
|
|
||||||
{this.skuRow.k}
|
|
||||||
{this.skuRow.is_multiple && (
|
|
||||||
<span class={bem('title-multiple')}>({t('multiple')})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
genIndicator() {
|
|
||||||
if (this.scrollable) {
|
|
||||||
const style = {
|
|
||||||
transform: `translate3d(${this.progress * 20}px, 0, 0)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={bem('indicator-wrapper')}>
|
|
||||||
<div class={bem('indicator')}>
|
|
||||||
<div class={bem('indicator-slider')} style={style} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
genContent() {
|
|
||||||
const nodes = this.slots();
|
|
||||||
|
|
||||||
if (this.skuRow.largeImageMode) {
|
|
||||||
const top = [];
|
|
||||||
const bottom = [];
|
|
||||||
|
|
||||||
nodes.forEach((node, index) => {
|
|
||||||
const group = Math.floor(index / 3) % 2 === 0 ? top : bottom;
|
|
||||||
group.push(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={bem('scroller')} ref="scroller">
|
|
||||||
<div class={bem('row')} ref="row">
|
|
||||||
{top}
|
|
||||||
</div>
|
|
||||||
{bottom.length ? <div class={bem('row')}>{bottom}</div> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
},
|
|
||||||
|
|
||||||
centerItem(selectSkuId) {
|
|
||||||
if (!this.skuRow.largeImageMode || !selectSkuId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { children = [] } = this;
|
|
||||||
const { scroller, row } = this.$refs;
|
|
||||||
const child = children.find((it) => +it.skuValue.id === +selectSkuId);
|
|
||||||
if (scroller && row && child && child.$el) {
|
|
||||||
const target = child.$el;
|
|
||||||
const to =
|
|
||||||
target.offsetLeft - (scroller.offsetWidth - target.offsetWidth) / 2;
|
|
||||||
scroller.scrollLeft = to;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div class={[bem(), BORDER_BOTTOM]}>
|
|
||||||
{this.genTitle()}
|
|
||||||
{this.genContent()}
|
|
||||||
{this.genIndicator()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,109 +0,0 @@
|
|||||||
import { bem } from './SkuRow';
|
|
||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { isSkuChoosable } from '../utils/sku-helper';
|
|
||||||
import { ChildrenMixin } from '../../mixins/relation';
|
|
||||||
import Image from '../../image';
|
|
||||||
|
|
||||||
const [createComponent] = createNamespace('sku-row-item');
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
mixins: [ChildrenMixin('vanSkuRows')],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
lazyLoad: Boolean,
|
|
||||||
skuValue: Object,
|
|
||||||
skuKeyStr: String,
|
|
||||||
skuEventBus: Object,
|
|
||||||
selectedSku: Object,
|
|
||||||
largeImageMode: Boolean,
|
|
||||||
skuList: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
imgUrl() {
|
|
||||||
const url = this.skuValue.imgUrl || this.skuValue.img_url;
|
|
||||||
return this.largeImageMode
|
|
||||||
? url ||
|
|
||||||
'https://img.yzcdn.cn/upload_files/2020/06/24/FmKWDg0bN9rMcTp9ne8MXiQWGtLn.png'
|
|
||||||
: url;
|
|
||||||
},
|
|
||||||
|
|
||||||
choosable() {
|
|
||||||
return isSkuChoosable(this.skuList, this.selectedSku, {
|
|
||||||
key: this.skuKeyStr,
|
|
||||||
valueId: this.skuValue.id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onSelect() {
|
|
||||||
if (this.choosable) {
|
|
||||||
this.skuEventBus.$emit('sku:select', {
|
|
||||||
...this.skuValue,
|
|
||||||
skuKeyStr: this.skuKeyStr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onPreviewImg(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const { skuValue, skuKeyStr } = this;
|
|
||||||
this.skuEventBus.$emit('sku:previewImage', {
|
|
||||||
...skuValue,
|
|
||||||
ks: skuKeyStr,
|
|
||||||
imgUrl: skuValue.imgUrl || skuValue.img_url,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
genImage(classPrefix) {
|
|
||||||
if (this.imgUrl) {
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
fit="cover"
|
|
||||||
src={this.imgUrl}
|
|
||||||
class={`${classPrefix}-img`}
|
|
||||||
lazyLoad={this.lazyLoad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const choosed = this.skuValue.id === this.selectedSku[this.skuKeyStr];
|
|
||||||
const classPrefix = this.largeImageMode ? bem('image-item') : bem('item');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class={[
|
|
||||||
classPrefix,
|
|
||||||
choosed ? `${classPrefix}--active` : '',
|
|
||||||
!this.choosable ? `${classPrefix}--disabled` : '',
|
|
||||||
]}
|
|
||||||
onClick={this.onSelect}
|
|
||||||
>
|
|
||||||
{this.genImage(classPrefix)}
|
|
||||||
<div class={`${classPrefix}-name`}>
|
|
||||||
{this.largeImageMode ? (
|
|
||||||
<span class={{ 'van-multi-ellipsis--l2': this.largeImageMode }}>
|
|
||||||
{this.skuValue.name}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
this.skuValue.name
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{this.largeImageMode && (
|
|
||||||
<img
|
|
||||||
class={`${classPrefix}-img-icon`}
|
|
||||||
src="https://img.yzcdn.cn/upload_files/2020/07/02/Fu4_ya0l0aAt4Mv4PL9jzPzfZnDX.png"
|
|
||||||
onClick={this.onPreviewImg}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,49 +0,0 @@
|
|||||||
import { createNamespace } from '../../utils';
|
|
||||||
|
|
||||||
const [createComponent] = createNamespace('sku-row-prop-item');
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
skuValue: Object,
|
|
||||||
skuKeyStr: String,
|
|
||||||
skuEventBus: Object,
|
|
||||||
selectedProp: Object,
|
|
||||||
multiple: Boolean,
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
choosed() {
|
|
||||||
const { selectedProp, skuKeyStr, skuValue } = this;
|
|
||||||
|
|
||||||
if (selectedProp && selectedProp[skuKeyStr]) {
|
|
||||||
return selectedProp[skuKeyStr].indexOf(skuValue.id) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onSelect() {
|
|
||||||
this.skuEventBus.$emit('sku:propSelect', {
|
|
||||||
...this.skuValue,
|
|
||||||
skuKeyStr: this.skuKeyStr,
|
|
||||||
multiple: this.multiple,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class={[
|
|
||||||
'van-sku-row__item',
|
|
||||||
{ 'van-sku-row__item--active': this.choosed },
|
|
||||||
]}
|
|
||||||
onClick={this.onSelect}
|
|
||||||
>
|
|
||||||
<span class="van-sku-row__item-name">{this.skuValue.name}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,178 +0,0 @@
|
|||||||
import { createNamespace } from '../../utils';
|
|
||||||
import { LIMIT_TYPE } from '../constants';
|
|
||||||
import Stepper from '../../stepper';
|
|
||||||
|
|
||||||
const namespace = createNamespace('sku-stepper');
|
|
||||||
const createComponent = namespace[0];
|
|
||||||
const t = namespace[2];
|
|
||||||
const { QUOTA_LIMIT, STOCK_LIMIT } = LIMIT_TYPE;
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
stock: Number,
|
|
||||||
skuEventBus: Object,
|
|
||||||
skuStockNum: Number,
|
|
||||||
selectedNum: Number,
|
|
||||||
stepperTitle: String,
|
|
||||||
disableStepperInput: Boolean,
|
|
||||||
customStepperConfig: Object,
|
|
||||||
hideQuotaText: Boolean,
|
|
||||||
quota: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
quotaUsed: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
startSaleNum: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentNum: this.selectedNum,
|
|
||||||
// 购买限制类型: 限购/库存
|
|
||||||
limitType: STOCK_LIMIT,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
currentNum(num) {
|
|
||||||
const intValue = parseInt(num, 10);
|
|
||||||
if (intValue >= this.stepperMinLimit && intValue <= this.stepperLimit) {
|
|
||||||
this.skuEventBus.$emit('sku:numChange', intValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stepperLimit(limit) {
|
|
||||||
if (limit < this.currentNum && this.stepperMinLimit <= limit) {
|
|
||||||
this.currentNum = limit;
|
|
||||||
}
|
|
||||||
this.checkState(this.stepperMinLimit, limit);
|
|
||||||
},
|
|
||||||
|
|
||||||
stepperMinLimit(start) {
|
|
||||||
if (start > this.currentNum || start > this.stepperLimit) {
|
|
||||||
this.currentNum = start;
|
|
||||||
}
|
|
||||||
this.checkState(start, this.stepperLimit);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
stepperLimit() {
|
|
||||||
const quotaLimit = this.quota - this.quotaUsed;
|
|
||||||
let limit;
|
|
||||||
|
|
||||||
// 无限购时直接取库存,有限购时取限购数和库存数中小的那个
|
|
||||||
if (this.quota > 0 && quotaLimit <= this.stock) {
|
|
||||||
// 修正负的limit
|
|
||||||
limit = quotaLimit < 0 ? 0 : quotaLimit;
|
|
||||||
this.limitType = QUOTA_LIMIT;
|
|
||||||
} else {
|
|
||||||
limit = this.stock;
|
|
||||||
this.limitType = STOCK_LIMIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return limit;
|
|
||||||
},
|
|
||||||
stepperMinLimit() {
|
|
||||||
return this.startSaleNum < 1 ? 1 : this.startSaleNum;
|
|
||||||
},
|
|
||||||
quotaText() {
|
|
||||||
const { quotaText, hideQuotaText } = this.customStepperConfig;
|
|
||||||
if (hideQuotaText) return '';
|
|
||||||
|
|
||||||
let text = '';
|
|
||||||
|
|
||||||
if (quotaText) {
|
|
||||||
text = quotaText;
|
|
||||||
} else {
|
|
||||||
const textArr = [];
|
|
||||||
if (this.startSaleNum > 1) {
|
|
||||||
textArr.push(t('quotaStart', this.startSaleNum));
|
|
||||||
}
|
|
||||||
if (this.quota > 0) {
|
|
||||||
textArr.push(t('quotaLimit', this.quota));
|
|
||||||
}
|
|
||||||
text = textArr.join(t('comma'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.checkState(this.stepperMinLimit, this.stepperLimit);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
setCurrentNum(num) {
|
|
||||||
this.currentNum = num;
|
|
||||||
this.checkState(this.stepperMinLimit, this.stepperLimit);
|
|
||||||
},
|
|
||||||
|
|
||||||
onOverLimit(action) {
|
|
||||||
this.skuEventBus.$emit('sku:overLimit', {
|
|
||||||
action,
|
|
||||||
limitType: this.limitType,
|
|
||||||
quota: this.quota,
|
|
||||||
quotaUsed: this.quotaUsed,
|
|
||||||
startSaleNum: this.startSaleNum,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onChange(currentValue) {
|
|
||||||
const intValue = parseInt(currentValue, 10);
|
|
||||||
const { handleStepperChange } = this.customStepperConfig;
|
|
||||||
handleStepperChange && handleStepperChange(intValue);
|
|
||||||
this.$emit('change', intValue);
|
|
||||||
},
|
|
||||||
|
|
||||||
checkState(min, max) {
|
|
||||||
// 如果选择小于起售,则强制变为起售
|
|
||||||
if (this.currentNum < min || min > max) {
|
|
||||||
this.currentNum = min;
|
|
||||||
} else if (this.currentNum > max) {
|
|
||||||
// 当前选择数量大于最大可选时,需要重置已选数量
|
|
||||||
this.currentNum = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skuEventBus.$emit('sku:stepperState', {
|
|
||||||
valid: min <= max,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
limitType: this.limitType,
|
|
||||||
quota: this.quota,
|
|
||||||
quotaUsed: this.quotaUsed,
|
|
||||||
startSaleNum: this.startSaleNum,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div class="van-sku-stepper-stock">
|
|
||||||
<div class="van-sku__stepper-title">
|
|
||||||
{this.stepperTitle || t('num')}
|
|
||||||
</div>
|
|
||||||
<Stepper
|
|
||||||
vModel={this.currentNum}
|
|
||||||
integer
|
|
||||||
class="van-sku__stepper"
|
|
||||||
min={this.stepperMinLimit}
|
|
||||||
max={this.stepperLimit}
|
|
||||||
disableInput={this.disableStepperInput}
|
|
||||||
onOverlimit={this.onOverLimit}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
{!this.hideQuotaText && this.quotaText && (
|
|
||||||
<span class="van-sku__stepper-quota">({this.quotaText})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
export const LIMIT_TYPE = {
|
|
||||||
QUOTA_LIMIT: 0,
|
|
||||||
STOCK_LIMIT: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UNSELECTED_SKU_VALUE_ID = '';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
LIMIT_TYPE,
|
|
||||||
UNSELECTED_SKU_VALUE_ID,
|
|
||||||
};
|
|
@ -1,193 +0,0 @@
|
|||||||
export function getSkuData(largeImageMode = false) {
|
|
||||||
return {
|
|
||||||
goods_id: '1',
|
|
||||||
quota: 5,
|
|
||||||
quota_used: 0,
|
|
||||||
start_sale_num: 2,
|
|
||||||
goods_info: {
|
|
||||||
price: 1,
|
|
||||||
title: '测试商品',
|
|
||||||
picture: 'https://b.yzcdn.cn/vant/sku/shoes-1.png',
|
|
||||||
},
|
|
||||||
sku: {
|
|
||||||
price: '1.00',
|
|
||||||
stock_num: 227,
|
|
||||||
none_sku: false,
|
|
||||||
hide_stock: false,
|
|
||||||
collection_id: 2261,
|
|
||||||
tree: [
|
|
||||||
{
|
|
||||||
k: '颜色',
|
|
||||||
k_s: 's1',
|
|
||||||
k_id: '1',
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: '粉色',
|
|
||||||
imgUrl: 'https://b.yzcdn.cn/vant/sku/shoes-1.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: '黄色',
|
|
||||||
imgUrl: 'https://b.yzcdn.cn/vant/sku/shoes-2.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: '蓝色',
|
|
||||||
imgUrl: 'https://b.yzcdn.cn/vant/sku/shoes-3.png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
largeImageMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
k: '尺寸',
|
|
||||||
k_s: 's2',
|
|
||||||
k_id: '2',
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: '大',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: '小',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
id: 2259,
|
|
||||||
s1: '2',
|
|
||||||
s2: '1',
|
|
||||||
price: 100,
|
|
||||||
discount: 100,
|
|
||||||
stock_num: 110,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2260,
|
|
||||||
s1: '3',
|
|
||||||
s2: '1',
|
|
||||||
price: 100,
|
|
||||||
discount: 100,
|
|
||||||
stock_num: 99,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2257,
|
|
||||||
s1: '1',
|
|
||||||
s2: '1',
|
|
||||||
price: 100,
|
|
||||||
discount: 100,
|
|
||||||
stock_num: 111,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2258,
|
|
||||||
s1: '1',
|
|
||||||
s2: '2',
|
|
||||||
price: 100,
|
|
||||||
discount: 100,
|
|
||||||
stock_num: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
multiple: '0',
|
|
||||||
name: '留言1',
|
|
||||||
type: 'text',
|
|
||||||
required: '1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
multiple: 0,
|
|
||||||
name: '留言2',
|
|
||||||
type: 'id_no',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
multiple: 0,
|
|
||||||
name: '留言3',
|
|
||||||
type: 'image',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
multiple: 1,
|
|
||||||
name: '留言4',
|
|
||||||
type: 'text',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
name: '数字',
|
|
||||||
multiple: 0,
|
|
||||||
type: 'tel',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
name: '邮件',
|
|
||||||
multiple: 0,
|
|
||||||
type: 'email',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
name: '日期',
|
|
||||||
multiple: 0,
|
|
||||||
type: 'date',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: '0',
|
|
||||||
disable: false,
|
|
||||||
name: '时间',
|
|
||||||
multiple: 0,
|
|
||||||
type: 'time',
|
|
||||||
required: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
k: '加料',
|
|
||||||
k_id: 124,
|
|
||||||
is_multiple: true,
|
|
||||||
v: [
|
|
||||||
{
|
|
||||||
id: 1224,
|
|
||||||
name: '布丁',
|
|
||||||
price: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1225,
|
|
||||||
name: '波霸',
|
|
||||||
price: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1226,
|
|
||||||
name: '珍珠',
|
|
||||||
price: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialSku = {
|
|
||||||
s1: '1',
|
|
||||||
s2: '1',
|
|
||||||
selectedNum: 3,
|
|
||||||
selectedProp: {
|
|
||||||
124: [1225, 1226],
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,277 +0,0 @@
|
|||||||
<template>
|
|
||||||
<demo-section>
|
|
||||||
<demo-block :title="t('basicUsage')">
|
|
||||||
<div class="sku-container">
|
|
||||||
<van-sku
|
|
||||||
v-model="showBase"
|
|
||||||
:sku="skuData.sku"
|
|
||||||
:quota="skuData.quota"
|
|
||||||
:goods="skuData.goods_info"
|
|
||||||
:goods-id="skuData.goods_id"
|
|
||||||
:quota-used="skuData.quota_used"
|
|
||||||
:properties="skuData.properties"
|
|
||||||
:hide-stock="skuData.sku.hide_stock"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
:start-sale-num="skuData.start_sale_num"
|
|
||||||
:custom-sku-validator="customSkuValidator"
|
|
||||||
disable-stepper-input
|
|
||||||
reset-stepper-on-hide
|
|
||||||
safe-area-inset-bottom
|
|
||||||
reset-selected-sku-on-hide
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
<van-button block type="primary" @click="showBase = true">
|
|
||||||
{{ t('basicUsage') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</demo-block>
|
|
||||||
|
|
||||||
<demo-block :title="t('customStepper')">
|
|
||||||
<div class="sku-container">
|
|
||||||
<van-sku
|
|
||||||
v-model="showStepper"
|
|
||||||
:sku="skuData.sku"
|
|
||||||
:quota="skuData.quota"
|
|
||||||
:goods="skuData.goods_info"
|
|
||||||
:goods-id="skuData.goods_id"
|
|
||||||
:quota-used="skuData.quota_used"
|
|
||||||
:properties="skuData.properties"
|
|
||||||
:hide-stock="skuData.sku.hide_stock"
|
|
||||||
:start-sale-num="skuData.start_sale_num"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
:custom-stepper-config="customStepperConfig"
|
|
||||||
hide-quota-text
|
|
||||||
safe-area-inset-bottom
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
<van-button block type="primary" @click="showStepper = true">
|
|
||||||
{{ t('customStepper') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</demo-block>
|
|
||||||
|
|
||||||
<demo-block :title="t('hideSoldoutSku')">
|
|
||||||
<div class="sku-container">
|
|
||||||
<van-sku
|
|
||||||
v-model="showSoldout"
|
|
||||||
:sku="skuData.sku"
|
|
||||||
:quota="skuData.quota"
|
|
||||||
:goods="skuData.goods_info"
|
|
||||||
:goods-id="skuData.goods_id"
|
|
||||||
:quota-used="skuData.quota_used"
|
|
||||||
:properties="skuData.properties"
|
|
||||||
:hide-stock="skuData.sku.hide_stock"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
:start-sale-num="skuData.start_sale_num"
|
|
||||||
:show-soldout-sku="false"
|
|
||||||
:custom-stepper-config="customStepperConfig"
|
|
||||||
hide-quota-text
|
|
||||||
safe-area-inset-bottom
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
/>
|
|
||||||
<van-button block type="primary" @click="showSoldout = true">
|
|
||||||
{{ t('hideSoldoutSku') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</demo-block>
|
|
||||||
|
|
||||||
<demo-block :title="t('largeImageMode')">
|
|
||||||
<div class="sku-container">
|
|
||||||
<van-sku
|
|
||||||
v-model="showLargePicturePreview"
|
|
||||||
:sku="skuData2.sku"
|
|
||||||
:quota="skuData2.quota"
|
|
||||||
:goods="skuData2.goods_info"
|
|
||||||
:goods-id="skuData2.goods_id"
|
|
||||||
:hide-stock="skuData2.sku.hide_stock"
|
|
||||||
:properties="skuData2.properties"
|
|
||||||
:quota-used="skuData2.quota_used"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
:start-sale-num="skuData2.start_sale_num"
|
|
||||||
:show-header-image="false"
|
|
||||||
:custom-sku-validator="customSkuValidator"
|
|
||||||
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="showLargePicturePreview = true"
|
|
||||||
>
|
|
||||||
{{ t('largeImageMode') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</demo-block>
|
|
||||||
|
|
||||||
<demo-block :title="t('customBySlot')">
|
|
||||||
<div class="sku-container">
|
|
||||||
<van-sku
|
|
||||||
v-model="showCustom"
|
|
||||||
:stepper-title="t('stepperTitle')"
|
|
||||||
: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"
|
|
||||||
:properties="skuData.properties"
|
|
||||||
show-add-cart-btn
|
|
||||||
reset-stepper-on-hide
|
|
||||||
safe-area-inset-bottom
|
|
||||||
:initial-sku="initialSku"
|
|
||||||
:message-config="messageConfig"
|
|
||||||
@buy-clicked="onBuyClicked"
|
|
||||||
@add-cart="onAddCartClicked"
|
|
||||||
>
|
|
||||||
<template #sku-header-price="{ price }">
|
|
||||||
<div class="van-sku__goods-price">
|
|
||||||
<span class="van-sku__price-symbol">¥</span>
|
|
||||||
<span class="van-sku__price-num">{{ price }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #sku-actions-top>
|
|
||||||
<div class="van-sku-header-item text-center">
|
|
||||||
{{ t('actionsTop') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #sku-actions="{ skuEventBus }">
|
|
||||||
<div class="van-sku-actions">
|
|
||||||
<van-button
|
|
||||||
square
|
|
||||||
size="large"
|
|
||||||
type="warning"
|
|
||||||
@click="onPointClicked"
|
|
||||||
>
|
|
||||||
{{ t('button1') }}
|
|
||||||
</van-button>
|
|
||||||
<van-button
|
|
||||||
square
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
@click="skuEventBus.$emit('sku:buy')"
|
|
||||||
>
|
|
||||||
{{ t('button2') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</van-sku>
|
|
||||||
<van-button block type="primary" @click="showCustom = true">
|
|
||||||
{{ t('customBySlot') }}
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</demo-block>
|
|
||||||
</demo-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { initialSku, getSkuData } from './data';
|
|
||||||
import { LIMIT_TYPE } from '../constants';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
i18n: {
|
|
||||||
'zh-CN': {
|
|
||||||
button1: '积分兑换',
|
|
||||||
button2: '买买买',
|
|
||||||
actionsTop: '商品不多,赶快购买吧',
|
|
||||||
stepperTitle: '我要买',
|
|
||||||
customBySlot: '通过插槽定制',
|
|
||||||
customStepper: '自定义步进器',
|
|
||||||
hideSoldoutSku: '隐藏售罄规格',
|
|
||||||
largeImageMode: '大图预览模式',
|
|
||||||
},
|
|
||||||
'en-US': {
|
|
||||||
button1: 'Button',
|
|
||||||
button2: 'Button',
|
|
||||||
actionsTop: 'Action top info',
|
|
||||||
customBySlot: 'Custom By Slot',
|
|
||||||
stepperTitle: 'Stepper title',
|
|
||||||
customStepper: 'Custom Stepper',
|
|
||||||
hideSoldoutSku: 'Hide Soldout Sku',
|
|
||||||
largeImageMode: 'Large Image Mode',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
this.skuData = getSkuData();
|
|
||||||
this.skuData2 = getSkuData(true);
|
|
||||||
this.initialSku = initialSku;
|
|
||||||
|
|
||||||
return {
|
|
||||||
showBase: false,
|
|
||||||
showCustom: false,
|
|
||||||
showStepper: false,
|
|
||||||
showSoldout: false,
|
|
||||||
showLargePicturePreview: false,
|
|
||||||
customSkuValidator: () => '请选择xxx',
|
|
||||||
customStepperConfig: {
|
|
||||||
quotaText: '单次限购100件',
|
|
||||||
stockFormatter: (stock) => `剩余${stock}`,
|
|
||||||
handleOverLimit: (data) => {
|
|
||||||
const { action, limitType, quota, startSaleNum = 1 } = data;
|
|
||||||
|
|
||||||
if (action === 'minus') {
|
|
||||||
this.$toast(
|
|
||||||
startSaleNum > 1 ? `${startSaleNum}件起售` : '至少选择一件商品'
|
|
||||||
);
|
|
||||||
} else if (action === 'plus') {
|
|
||||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
|
||||||
this.$toast(`限购${quota}件`);
|
|
||||||
} else {
|
|
||||||
this.$toast('库存不够了');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messageConfig: {
|
|
||||||
initialMessages: {
|
|
||||||
留言1: '商品留言',
|
|
||||||
},
|
|
||||||
uploadImg: (file, img) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
setTimeout(() => resolve(img), 1000);
|
|
||||||
}),
|
|
||||||
uploadMaxSize: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onBuyClicked(data) {
|
|
||||||
this.$toast('buy:' + JSON.stringify(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
onAddCartClicked(data) {
|
|
||||||
this.$toast('add cart:' + JSON.stringify(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
onPointClicked() {
|
|
||||||
this.$toast('积分兑换');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
@import '../../style/var';
|
|
||||||
|
|
||||||
.demo-sku {
|
|
||||||
background-color: @white;
|
|
||||||
|
|
||||||
.sku-container {
|
|
||||||
padding: 0 @padding-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||||||
// Utils
|
|
||||||
import lang from './lang';
|
|
||||||
import constants from './constants';
|
|
||||||
import skuHelper from './utils/sku-helper';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Sku from './Sku';
|
|
||||||
import Locale from '../locale';
|
|
||||||
import SkuActions from './components/SkuActions';
|
|
||||||
import SkuHeader from './components/SkuHeader';
|
|
||||||
import SkuHeaderItem from './components/SkuHeaderItem';
|
|
||||||
import SkuMessages from './components/SkuMessages';
|
|
||||||
import SkuStepper from './components/SkuStepper';
|
|
||||||
import SkuRow from './components/SkuRow';
|
|
||||||
import SkuRowItem from './components/SkuRowItem';
|
|
||||||
import SkuRowPropItem from './components/SkuRowPropItem';
|
|
||||||
|
|
||||||
Locale.add(lang);
|
|
||||||
|
|
||||||
Sku.SkuActions = SkuActions;
|
|
||||||
Sku.SkuHeader = SkuHeader;
|
|
||||||
Sku.SkuHeaderItem = SkuHeaderItem;
|
|
||||||
Sku.SkuMessages = SkuMessages;
|
|
||||||
Sku.SkuStepper = SkuStepper;
|
|
||||||
Sku.SkuRow = SkuRow;
|
|
||||||
Sku.SkuRowItem = SkuRowItem;
|
|
||||||
Sku.SkuRowPropItem = SkuRowPropItem;
|
|
||||||
Sku.skuHelper = skuHelper;
|
|
||||||
Sku.skuConstants = constants;
|
|
||||||
|
|
||||||
export default Sku;
|
|
@ -1,373 +0,0 @@
|
|||||||
@import '../style/var';
|
|
||||||
@import '../style/mixins/clearfix';
|
|
||||||
|
|
||||||
.van-sku {
|
|
||||||
&-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
min-height: 50%;
|
|
||||||
max-height: 80%;
|
|
||||||
overflow-y: visible;
|
|
||||||
font-size: @font-size-md;
|
|
||||||
background: @white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-body {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 44px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin: 0 @padding-md;
|
|
||||||
|
|
||||||
&__img-wrap {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
margin: @padding-sm @padding-sm @padding-sm 0;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: @border-radius-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__goods-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: @padding-sm 20px @padding-sm 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header-item {
|
|
||||||
margin-top: @padding-xs;
|
|
||||||
color: @gray-6;
|
|
||||||
font-size: @font-size-sm;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price-symbol {
|
|
||||||
font-size: @font-size-lg;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price-num {
|
|
||||||
font-weight: @font-weight-bold;
|
|
||||||
font-size: 22px;
|
|
||||||
vertical-align: bottom;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__goods-price {
|
|
||||||
// for price align
|
|
||||||
margin-left: -2px;
|
|
||||||
color: @red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price-tag {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: @padding-xs;
|
|
||||||
padding: 0 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: @red;
|
|
||||||
font-size: @font-size-sm;
|
|
||||||
line-height: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.1;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-group-container {
|
|
||||||
padding-top: @padding-sm;
|
|
||||||
|
|
||||||
&--hide-soldout {
|
|
||||||
.van-sku-row__item--disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sku row */
|
|
||||||
&-row {
|
|
||||||
margin: 0 @padding-md @padding-sm;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item,
|
|
||||||
&__image-item {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
color: @text-color;
|
|
||||||
border-radius: @border-radius-md;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: @sku-item-background-color;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
color: @red;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 40px;
|
|
||||||
margin: 0 @padding-sm @padding-sm 0;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
z-index: 1;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 4px 0 4px 4px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: @border-radius-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
z-index: 1;
|
|
||||||
padding: @padding-xs;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disabled {
|
|
||||||
color: @gray-5;
|
|
||||||
background: @active-color;
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
.van-sku-row__item-img {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
margin-right: 0;
|
|
||||||
|
|
||||||
&-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 110px;
|
|
||||||
margin: 0 4px 4px 0;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 110px;
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 3;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 40px;
|
|
||||||
padding: @padding-base;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
border-color: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disabled {
|
|
||||||
color: @gray-5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
z-index: 2;
|
|
||||||
background: @active-color;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
padding-bottom: @padding-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title-multiple {
|
|
||||||
color: @gray-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__scroller {
|
|
||||||
margin: 0 -@padding-md;
|
|
||||||
overflow-x: scroll;
|
|
||||||
overflow-y: hidden;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__row {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding: 0 @padding-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__indicator {
|
|
||||||
width: 40px;
|
|
||||||
height: 4px;
|
|
||||||
background: @gray-3;
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slider {
|
|
||||||
width: 50%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: @red;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-stepper-stock {
|
|
||||||
padding: @padding-sm @padding-md;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stepper {
|
|
||||||
float: right;
|
|
||||||
padding-left: @padding-base;
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-quota {
|
|
||||||
float: right;
|
|
||||||
color: @red;
|
|
||||||
font-size: @font-size-sm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stock {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: @padding-xs;
|
|
||||||
color: @gray-6;
|
|
||||||
font-size: @font-size-sm;
|
|
||||||
|
|
||||||
&-num--highlight {
|
|
||||||
color: @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-messages {
|
|
||||||
padding-bottom: @padding-xl;
|
|
||||||
|
|
||||||
&__image-cell {
|
|
||||||
.van-cell__title {
|
|
||||||
max-width: @field-label-width;
|
|
||||||
margin-right: @field-label-margin-right;
|
|
||||||
color: @field-label-color;
|
|
||||||
text-align: left;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.van-cell__value {
|
|
||||||
overflow: visible;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label {
|
|
||||||
color: @cell-label-color;
|
|
||||||
font-size: @cell-label-font-size;
|
|
||||||
line-height: @cell-label-line-height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: @padding-xs @padding-md;
|
|
||||||
|
|
||||||
.van-button {
|
|
||||||
height: 40px;
|
|
||||||
font-weight: @font-weight-bold;
|
|
||||||
font-size: @font-size-md;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-top-left-radius: 20px;
|
|
||||||
border-bottom-left-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-top-right-radius: 20px;
|
|
||||||
border-bottom-right-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--warning {
|
|
||||||
background: @gradient-orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--danger {
|
|
||||||
background: @gradient-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sku only provide zh-CN lang by default
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'zh-CN': {
|
|
||||||
vanSku: {
|
|
||||||
select: '请选择',
|
|
||||||
selected: '已选',
|
|
||||||
selectSku: '请先选择商品规格',
|
|
||||||
soldout: '库存不足',
|
|
||||||
originPrice: '原价',
|
|
||||||
minusTip: '至少选择一件',
|
|
||||||
minusStartTip: (start: number) => `${start}件起售`,
|
|
||||||
unavailable: '商品已经无法购买啦',
|
|
||||||
stock: '剩余',
|
|
||||||
stockUnit: '件',
|
|
||||||
quotaTip: (quota: number) => `每人限购${quota}件`,
|
|
||||||
quotaUsedTip: (quota: number, count: number) =>
|
|
||||||
`每人限购${quota}件,你已购买${count}件`,
|
|
||||||
},
|
|
||||||
vanSkuActions: {
|
|
||||||
buy: '立即购买',
|
|
||||||
addCart: '加入购物车',
|
|
||||||
},
|
|
||||||
vanSkuImgUploader: {
|
|
||||||
oversize: (maxSize: number) =>
|
|
||||||
`最大可上传图片为${maxSize}MB,请尝试压缩图片尺寸`,
|
|
||||||
fail: '上传失败',
|
|
||||||
uploading: '上传中...',
|
|
||||||
},
|
|
||||||
vanSkuStepper: {
|
|
||||||
quotaLimit: (quota: number) => `限购${quota}件`,
|
|
||||||
quotaStart: (start: number) => `${start}件起售`,
|
|
||||||
comma: ',',
|
|
||||||
num: '购买数量',
|
|
||||||
},
|
|
||||||
vanSkuMessages: {
|
|
||||||
fill: '请填写',
|
|
||||||
upload: '请上传',
|
|
||||||
imageLabel: '仅限一张',
|
|
||||||
invalid: {
|
|
||||||
tel: '请填写正确的数字格式留言',
|
|
||||||
mobile: '手机号长度为6-20位数字',
|
|
||||||
email: '请填写正确的邮箱',
|
|
||||||
id_no: '请填写正确的身份证号码',
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
id_no: '请填写身份证号',
|
|
||||||
text: '请填写留言',
|
|
||||||
tel: '请填写数字',
|
|
||||||
email: '请填写邮箱',
|
|
||||||
date: '请选择日期',
|
|
||||||
time: '请选择时间',
|
|
||||||
textarea: '请填写留言',
|
|
||||||
mobile: '请填写手机号',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vanSkuRow: {
|
|
||||||
multiple: '可多选',
|
|
||||||
},
|
|
||||||
vanSkuDatetimeField: {
|
|
||||||
title: {
|
|
||||||
date: '选择年月日',
|
|
||||||
time: '选择时间',
|
|
||||||
datetime: '选择日期时间',
|
|
||||||
},
|
|
||||||
format: {
|
|
||||||
year: '年',
|
|
||||||
month: '月',
|
|
||||||
day: '日',
|
|
||||||
hour: '时',
|
|
||||||
minute: '分',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`renders demo correctly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div class="sku-container">
|
|
||||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block">
|
|
||||||
<div class="van-button__content"><span class="van-button__text">
|
|
||||||
基础用法
|
|
||||||
</span></div>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="sku-container">
|
|
||||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block">
|
|
||||||
<div class="van-button__content"><span class="van-button__text">
|
|
||||||
自定义步进器
|
|
||||||
</span></div>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="sku-container">
|
|
||||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block">
|
|
||||||
<div class="van-button__content"><span class="van-button__text">
|
|
||||||
隐藏售罄规格
|
|
||||||
</span></div>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="sku-container">
|
|
||||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block">
|
|
||||||
<div class="van-button__content"><span class="van-button__text">
|
|
||||||
大图预览模式
|
|
||||||
</span></div>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="sku-container">
|
|
||||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block">
|
|
||||||
<div class="van-button__content"><span class="van-button__text">
|
|
||||||
通过插槽定制
|
|
||||||
</span></div>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,4 +0,0 @@
|
|||||||
import Demo from '../demo';
|
|
||||||
import { snapshotDemo } from '../../../test/demo';
|
|
||||||
|
|
||||||
snapshotDemo(Demo);
|
|
@ -1,87 +0,0 @@
|
|||||||
import { mount } from '../../../test';
|
|
||||||
import Sku from '..';
|
|
||||||
import { getSkuData, initialSku } from '../demo/data';
|
|
||||||
import { stringToDate, dateToString } from '../utils/time-helper';
|
|
||||||
|
|
||||||
const skuData = getSkuData();
|
|
||||||
|
|
||||||
test('resetSelectedSku method', () => {
|
|
||||||
skuData.sku.messages = [];
|
|
||||||
|
|
||||||
const wrapper = mount(Sku, {
|
|
||||||
propsData: {
|
|
||||||
value: true,
|
|
||||||
initialSku,
|
|
||||||
sku: skuData.sku,
|
|
||||||
quota: skuData.quota,
|
|
||||||
goods: skuData.goods_info,
|
|
||||||
goodsId: skuData.goods_id,
|
|
||||||
quotaUsed: skuData.quota_used,
|
|
||||||
hideStock: skuData.sku.hide_stock,
|
|
||||||
startSaleNum: skuData.start_sale_num,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.find('.van-button--danger').trigger('click');
|
|
||||||
expect(wrapper.emitted('buy-clicked').length).toEqual(1);
|
|
||||||
|
|
||||||
wrapper.setProps({ initialSku: {} });
|
|
||||||
wrapper.vm.resetSelectedSku();
|
|
||||||
|
|
||||||
wrapper.find('.van-button--danger').trigger('click');
|
|
||||||
expect(wrapper.emitted('buy-clicked').length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('message formatter', () => {
|
|
||||||
const skuData = getSkuData();
|
|
||||||
|
|
||||||
skuData.sku.messages = skuData.sku.messages.filter(
|
|
||||||
message => message.type === 'tel'
|
|
||||||
);
|
|
||||||
|
|
||||||
const wrapper = mount(Sku, {
|
|
||||||
propsData: {
|
|
||||||
value: true,
|
|
||||||
initialSku,
|
|
||||||
sku: skuData.sku,
|
|
||||||
quota: skuData.quota,
|
|
||||||
goods: skuData.goods_info,
|
|
||||||
goodsId: skuData.goods_id,
|
|
||||||
quotaUsed: skuData.quota_used,
|
|
||||||
hideStock: skuData.sku.hide_stock,
|
|
||||||
startSaleNum: skuData.start_sale_num,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = wrapper.find('input');
|
|
||||||
const correctValue = '15000000000';
|
|
||||||
|
|
||||||
// \u202c
|
|
||||||
input.element.value = '15000000000';
|
|
||||||
input.trigger('input');
|
|
||||||
|
|
||||||
expect(input.element.value).toEqual(correctValue);
|
|
||||||
|
|
||||||
// \u0020
|
|
||||||
input.element.value = '150 0000 0000';
|
|
||||||
input.trigger('input');
|
|
||||||
|
|
||||||
expect(input.element.value).toEqual(correctValue);
|
|
||||||
|
|
||||||
// /[a-zA-z]/
|
|
||||||
input.element.value = 'a-zA-z';
|
|
||||||
input.trigger('input');
|
|
||||||
|
|
||||||
expect(input.element.value).toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('stringToDate', () => {
|
|
||||||
expect(dateToString(stringToDate(''))).toEqual('');
|
|
||||||
expect(dateToString(stringToDate('2020-07-01'))).toEqual('2020-07-01');
|
|
||||||
expect(dateToString(stringToDate('2020-07-01 22:44'), 'datetime')).toEqual(
|
|
||||||
'2020-07-01 22:44'
|
|
||||||
);
|
|
||||||
expect(dateToString(stringToDate('2020-12-31 23:59'), 'datetime')).toEqual(
|
|
||||||
'2020-12-31 23:59'
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,158 +0,0 @@
|
|||||||
import { UNSELECTED_SKU_VALUE_ID } from '../constants';
|
|
||||||
|
|
||||||
/*
|
|
||||||
normalize sku tree
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
count: 2,
|
|
||||||
k: "品种", // 规格名称 skuKeyName
|
|
||||||
k_id: "1200", // skuKeyId
|
|
||||||
k_s: "s1" // skuKeyStr
|
|
||||||
v: [ // skuValues
|
|
||||||
{ // skuValue
|
|
||||||
id: "1201", // skuValueId
|
|
||||||
name: "萌" // 具体的规格值 skuValueName
|
|
||||||
}, {
|
|
||||||
id: "973",
|
|
||||||
name: "帅"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
|
||||||
v
|
|
||||||
{
|
|
||||||
s1: [{
|
|
||||||
id: "1201",
|
|
||||||
name: "萌"
|
|
||||||
}, {
|
|
||||||
id: "973",
|
|
||||||
name: "帅"
|
|
||||||
}],
|
|
||||||
...
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
export const normalizeSkuTree = (skuTree) => {
|
|
||||||
const normalizedTree = {};
|
|
||||||
skuTree.forEach((treeItem) => {
|
|
||||||
normalizedTree[treeItem.k_s] = treeItem.v;
|
|
||||||
});
|
|
||||||
return normalizedTree;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalizePropList = (propList) => {
|
|
||||||
const normalizedProp = {};
|
|
||||||
propList.forEach((item) => {
|
|
||||||
const itemObj = {};
|
|
||||||
item.v.forEach((it) => {
|
|
||||||
itemObj[it.id] = it;
|
|
||||||
});
|
|
||||||
normalizedProp[item.k_id] = itemObj;
|
|
||||||
});
|
|
||||||
return normalizedProp;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 判断是否所有的sku都已经选中
|
|
||||||
export const isAllSelected = (skuTree, selectedSku) => {
|
|
||||||
// 筛选selectedSku对象中key值不为空的值
|
|
||||||
const selected = Object.keys(selectedSku).filter(
|
|
||||||
(skuKeyStr) => selectedSku[skuKeyStr] !== UNSELECTED_SKU_VALUE_ID
|
|
||||||
);
|
|
||||||
return skuTree.length === selected.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据已选择的 sku 获取 skuComb
|
|
||||||
export const getSkuComb = (skuList, selectedSku) => {
|
|
||||||
const skuComb = skuList.filter((item) =>
|
|
||||||
Object.keys(selectedSku).every(
|
|
||||||
(skuKeyStr) => String(item[skuKeyStr]) === String(selectedSku[skuKeyStr])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return skuComb[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取已选择的sku名称
|
|
||||||
export const getSelectedSkuValues = (skuTree, selectedSku) => {
|
|
||||||
const normalizedTree = normalizeSkuTree(skuTree);
|
|
||||||
return Object.keys(selectedSku).reduce((selectedValues, skuKeyStr) => {
|
|
||||||
const skuValues = normalizedTree[skuKeyStr];
|
|
||||||
const skuValueId = selectedSku[skuKeyStr];
|
|
||||||
|
|
||||||
if (skuValueId !== UNSELECTED_SKU_VALUE_ID) {
|
|
||||||
const skuValue = skuValues.filter((value) => value.id === skuValueId)[0];
|
|
||||||
skuValue && selectedValues.push(skuValue);
|
|
||||||
}
|
|
||||||
return selectedValues;
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 判断sku是否可选
|
|
||||||
export const isSkuChoosable = (skuList, selectedSku, skuToChoose) => {
|
|
||||||
const { key, valueId } = skuToChoose;
|
|
||||||
|
|
||||||
// 先假设sku已选中,拼入已选中sku对象中
|
|
||||||
const matchedSku = {
|
|
||||||
...selectedSku,
|
|
||||||
[key]: valueId,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 再判断剩余sku是否全部不可选,若不可选则当前sku不可选中
|
|
||||||
const skusToCheck = Object.keys(matchedSku).filter(
|
|
||||||
(skuKey) => matchedSku[skuKey] !== UNSELECTED_SKU_VALUE_ID
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredSku = skuList.filter((sku) =>
|
|
||||||
skusToCheck.every(
|
|
||||||
(skuKey) => String(matchedSku[skuKey]) === String(sku[skuKey])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const stock = filteredSku.reduce((total, sku) => {
|
|
||||||
total += sku.stock_num;
|
|
||||||
return total;
|
|
||||||
}, 0);
|
|
||||||
return stock > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSelectedPropValues = (propList, selectedProp) => {
|
|
||||||
const normalizeProp = normalizePropList(propList);
|
|
||||||
return Object.keys(selectedProp).reduce((acc, cur) => {
|
|
||||||
selectedProp[cur].forEach((it) => {
|
|
||||||
acc.push({
|
|
||||||
...normalizeProp[cur][it],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSelectedProperties = (propList, selectedProp) => {
|
|
||||||
const list = [];
|
|
||||||
(propList || []).forEach((prop) => {
|
|
||||||
if (selectedProp[prop.k_id] && selectedProp[prop.k_id].length > 0) {
|
|
||||||
const v = [];
|
|
||||||
prop.v.forEach((it) => {
|
|
||||||
if (selectedProp[prop.k_id].indexOf(it.id) > -1) {
|
|
||||||
v.push({ ...it });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
list.push({
|
|
||||||
...prop,
|
|
||||||
v,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
normalizeSkuTree,
|
|
||||||
getSkuComb,
|
|
||||||
getSelectedSkuValues,
|
|
||||||
isAllSelected,
|
|
||||||
isSkuChoosable,
|
|
||||||
getSelectedPropValues,
|
|
||||||
getSelectedProperties,
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
import { padZero } from '../../utils/format/string';
|
|
||||||
|
|
||||||
// 字符串转 Date
|
|
||||||
// 只处理 YYYY-MM-DD 或者 YYYY-MM-DD HH:MM 格式
|
|
||||||
export function stringToDate(timeString) {
|
|
||||||
if (!timeString) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(timeString.replace(/-/g, '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date 转字符串
|
|
||||||
// type: date or datetime
|
|
||||||
export function dateToString(date, type = 'date') {
|
|
||||||
if (!date) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = date.getMonth() + 1;
|
|
||||||
const day = date.getDate();
|
|
||||||
let timeString = `${year}-${padZero(month)}-${padZero(day)}`;
|
|
||||||
if (type === 'datetime') {
|
|
||||||
const hours = date.getHours();
|
|
||||||
const minute = date.getMinutes();
|
|
||||||
timeString += ` ${padZero(hours)}:${padZero(minute)}`;
|
|
||||||
}
|
|
||||||
return timeString;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user