mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-05-23 15:09:16 +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