[new feature] Sku: update style & add several props (#3875)

This commit is contained in:
codpoe 2019-07-22 10:51:40 +08:00 committed by neverland
parent e8c528a6f6
commit 503fe5f469
13 changed files with 301 additions and 165 deletions

View File

@ -120,9 +120,11 @@ export default {
| v-model | Whether to show sku | `boolean` | `false` |
| sku | Sku data | `object` | - |
| goods | Goods info | `object` | - |
| goods-id | Goods id | `string | number` | - |
| 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` |
| 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` | - | - |
@ -263,7 +265,13 @@ customStepperConfig: {
Toast('not enough stock');
}
}
}
},
// custom callback when stepper value change
handleStepperChange: currentValue => {},
// stock
stockNum: 1999,
// stock fomatter
stockFormatter: stockNum => {},
}
```

View File

@ -123,8 +123,10 @@ export default {
| sku | 商品sku数据 | `object` | - | - |
| goods | 商品信息 | `object` | - | - |
| goods-id | 商品 id | `string | number` | - | - |
| price-tag | 显示在价格后面的标签 | `string` | - | - |
| hide-stock | 是否显示商品剩余库存 | `boolean` | `false` | - |
| hide-quota-text | 是否显示限购提示 | `boolean` | `false` | 1.4.8 |
| hide-selected-text | 是否隐藏已选提示 | `boolean` | `false` | - |
| show-add-cart-btn | 是否显示加入购物车按钮 | `boolean` | `true` | - |
| buy-text | 购买按钮文字 | `string` | `立即购买` | - |
| add-cart-text | 加入购物车按钮文字 | `string` | `加入购物车` | - |
@ -275,7 +277,13 @@ customStepperConfig: {
Toast('库存不够了');
}
}
}
},
// 步进器变化的回调
handleStepperChange: currentValue => {},
// 库存
stockNum: 1999,
// 格式化库存
stockFormatter: stockNum => {},
}
```

View File

@ -4,6 +4,7 @@ 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 SkuStepper from './components/SkuStepper';
@ -19,6 +20,7 @@ const { QUOTA_LIMIT } = LIMIT_TYPE;
export default createComponent({
props: {
sku: Object,
priceTag: String,
goods: Object,
value: Boolean,
buyText: String,
@ -28,6 +30,7 @@ export default createComponent({
stepperTitle: String,
getContainer: Function,
hideQuotaText: Boolean,
hideSelectedText: Boolean,
resetStepperOnHide: Boolean,
customSkuValidator: Function,
closeOnClickOverlay: Boolean,
@ -83,10 +86,8 @@ export default createComponent({
show(val) {
this.$emit('input', val);
if (!val) {
const selectedSkuValues = getSelectedSkuValues(this.sku.tree, this.selectedSku);
this.$emit('sku-close', {
selectedSkuValues,
selectedSkuValues: this.selectedSkuValues,
selectedNum: this.selectedNum,
selectedSkuComb: this.selectedSkuComb
});
@ -114,7 +115,6 @@ export default createComponent({
skuGroupClass() {
return [
'van-sku-group-container',
'van-hairline--bottom',
{
'van-sku-group-container--hide-soldout': !this.showSoldoutSku
}
@ -160,6 +160,10 @@ export default createComponent({
return null;
},
selectedSkuValues() {
return getSelectedSkuValues(this.skuTree, this.selectedSku);
},
price() {
if (this.selectedSkuComb) {
return (this.selectedSkuComb.price / 100).toFixed(2);
@ -168,6 +172,13 @@ export default createComponent({
return this.sku.price;
},
originPrice() {
if (this.selectedSkuComb && this.selectedSkuComb.origin_price) {
return (this.selectedSkuComb.origin_price / 100).toFixed(2);
}
return this.sku.origin_price;
},
skuTree() {
return this.sku.tree || [];
},
@ -191,6 +202,53 @@ export default createComponent({
}
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 `剩余 ${this.stock}`;
},
quotaText() {
const { quotaText, hideQuotaText } = this.customStepperConfig;
if (hideQuotaText) return '';
let text = '';
if (quotaText) {
text = quotaText;
} else if (this.quota > 0) {
text = `每人限购${this.quota}`;
}
return text;
},
selectedText() {
if (this.selectedSkuComb) {
return `已选 ${this.selectedSkuValues.map(item => item.name).join('')}`;
}
const unselected = this.skuTree
.filter(item => this.selectedSku[item.k_s] === UNSELECTED_SKU_VALUE_ID)
.map(item => item.k)
.join('');
return `选择 ${unselected}`;
}
},
@ -378,6 +436,7 @@ export default createComponent({
sku,
goods,
price,
originPrice,
skuEventBus,
selectedSku,
selectedNum,
@ -388,6 +447,7 @@ export default createComponent({
const slotsProps = {
price,
originPrice,
selectedNum,
skuEventBus,
selectedSku,
@ -398,9 +458,25 @@ export default createComponent({
const Header = slots('sku-header') || (
<SkuHeader sku={sku} goods={goods} skuEventBus={skuEventBus} selectedSku={selectedSku}>
{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>
<div>
<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>
{originPrice && (
<SkuHeaderItem>原价 {originPrice}</SkuHeaderItem>
)}
{!this.hideStock && (
<SkuHeaderItem>
<span class="van-sku__stock">{this.stockText}</span>
{!hideQuotaText && this.quotaText && <span class="van-sku__quota">({this.quotaText})</span>}
</SkuHeaderItem>
)}
{this.hasSku && !this.hideSelectedText && (
<SkuHeaderItem>{this.selectedText}</SkuHeaderItem>
)}
{slots('sku-header-extra')}
</div>
)}
</SkuHeader>
@ -429,16 +505,14 @@ export default createComponent({
const Stepper = slots('sku-stepper') || (
<SkuStepper
ref="skuStepper"
stock={this.stock}
quota={this.quota}
hideStock={this.hideStock}
quotaUsed={this.quotaUsed}
skuEventBus={skuEventBus}
selectedNum={selectedNum}
selectedSku={selectedSku}
stepperTitle={stepperTitle}
skuStockNum={sku.stock_num}
hideQuotaText={hideQuotaText}
selectedSkuComb={selectedSkuComb}
disableStepperInput={this.disableStepperInput}
customStepperConfig={this.customStepperConfig}
onChange={event => {
@ -472,6 +546,7 @@ export default createComponent({
class="van-sku-container"
getContainer={this.getContainer}
closeOnClickOverlay={this.closeOnClickOverlay}
round
>
{Header}
<div class="van-sku-body" style={this.bodyStyle}>

View File

@ -50,10 +50,9 @@ function SkuHeader(
<img src={goodsImg} />
</div>
<div class={bem('goods-info')}>
<div class="van-sku__goods-name van-ellipsis">{goods.title}</div>
{slots.default && slots.default()}
<Icon
name="close"
name="clear"
class="van-sku__close-icon"
onClick={() => {
skuEventBus.$emit('sku:close');

View File

@ -0,0 +1,25 @@
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);

View File

@ -18,20 +18,16 @@ export default createComponent({
data() {
return {
// 正在上传的图片 base64
paddingImg: ''
paddingImg: '',
uploadFail: false
};
},
computed: {
imgList() {
return this.value && !this.paddingImg ? [this.value] : [];
}
},
methods: {
afterReadFile(file) {
// 上传文件
this.paddingImg = file.content;
this.uploadFail = false;
this.uploadImg(file.file, file.content)
.then(img => {
this.$emit('input', img);
@ -40,23 +36,53 @@ export default createComponent({
});
})
.catch(() => {
this.paddingImg = '';
this.uploadFail = true;
});
},
onOversize() {
this.$toast(`最大可上传图片为${this.maxSize}MB请尝试压缩图片尺寸`);
},
renderUploader(content, disabled = false) {
return (
<Uploader
class={bem('uploader')}
disabled={disabled}
afterRead={this.afterReadFile}
maxSize={this.maxSize * 1024 * 1024}
onOversize={this.onOversize}
>
<div class={bem('img')}>
{content}
</div>
</Uploader>
);
},
renderMask() {
return (
<div class={bem('mask')}>
{this.uploadFail
? (
[
<Icon name="warning-o" size="20px" />,
<div class={bem('warn-text')}>上传失败<br />重新上传</div>
]
) : (
<Loading type="spinner" size="20px" color="white" />
)}
</div>
);
}
},
render(h) {
const { imgList, paddingImg } = this;
const ImageList = (paddingImg || imgList.length > 0) && (
<div class="van-clearfix">
{imgList.map(img => (
<div class={bem('img')}>
<img src={img} />
return (
<div class={bem()}>
{this.value && this.renderUploader(
[
<img src={this.value} />,
<Icon
name="clear"
class={bem('delete')}
@ -64,40 +90,24 @@ export default createComponent({
this.$emit('input', '');
}}
/>
</div>
))}
{paddingImg && (
<div class={bem('img')}>
<img src={paddingImg} />
<Loading type="spinner" class={bem('uploading')} />
],
true
)}
{this.paddingImg && this.renderUploader(
[
<img src={this.paddingImg} />,
this.renderMask()
],
!this.uploadFail
)}
{!this.value && !this.paddingImg && this.renderUploader(
<div class={bem('trigger')}>
<Icon name="photograph" size="22px" />
</div>
)}
</div>
);
return (
<div class={bem()}>
<Uploader
disabled={!!paddingImg}
afterRead={this.afterReadFile}
maxSize={this.maxSize * 1024 * 1024}
onOversize={this.onOversize}
>
<div class={bem('header')}>
{paddingImg ? (
<div>正在上传...</div>
) : (
[
<Icon name="photograph" />,
<span class="label">{this.value ? '重拍' : '拍照'} </span>,
<Icon name="photo" />,
<span class="label">{this.value ? '重新选择照片' : '选择照片'}</span>
]
)}
</div>
</Uploader>
{ImageList}
</div>
);
}
});

View File

@ -127,6 +127,7 @@ export default createComponent({
{this.messages.map((message, index) => (message.type === 'image' ? (
<Cell
class={bem('image-cell')}
value-class={bem('image-cell-value')}
label="仅限一张"
title={message.name}
key={`${this.goodsId}-${index}`}

View File

@ -19,8 +19,8 @@ function SkuRow(
ctx: RenderContext<SkuRowProps>
) {
return (
<div class={bem()} {...inherit(ctx)}>
<div class={bem('title')}>{props.skuRow.k}</div>
<div class={[bem(), 'van-hairline--bottom']} {...inherit(ctx)}>
<div class={bem('title')}>{props.skuRow.k}</div>
{slots.default && slots.default()}
</div>
);

View File

@ -37,6 +37,7 @@ export default createComponent({
render(h) {
const choosed = this.skuValue.id === this.selectedSku[this.skuKeyStr];
const imgUrl = this.skuValue.imgUrl || this.skuValue.img_url;
return (
<span
@ -49,7 +50,8 @@ export default createComponent({
]}
onClick={this.onSelect}
>
{this.skuValue.name}
{imgUrl && <img class="van-sku-row__item-img" src={imgUrl} />}
<span class="van-sku-row__item-name">{this.skuValue.name}</span>
</span>
);
}

View File

@ -7,14 +7,11 @@ const { QUOTA_LIMIT, STOCK_LIMIT } = LIMIT_TYPE;
export default createComponent({
props: {
hideStock: Boolean,
selectedSku: Object,
stock: Number,
skuEventBus: Object,
skuStockNum: Number,
selectedNum: Number,
stepperTitle: String,
hideQuotaText: Boolean,
selectedSkuComb: Object,
disableStepperInput: Boolean,
customStepperConfig: Object,
quota: {
@ -48,40 +45,6 @@ export default createComponent({
},
computed: {
stock() {
const { stockNum } = this.customStepperConfig;
if (stockNum !== undefined) {
return stockNum;
}
if (this.selectedSkuComb) {
return this.selectedSkuComb.stock_num;
}
return this.skuStockNum;
},
stockText() {
const { stockFormatter } = this.customStepperConfig;
if (stockFormatter) return stockFormatter(this.stock);
return `剩余${this.stock}`;
},
quotaText() {
const { quotaText, hideQuotaText } = this.customStepperConfig;
if (hideQuotaText) return '';
let text = '';
if (quotaText) {
text = quotaText;
} else if (this.quota > 0) {
text = `每人限购${this.quota}`;
}
return text;
},
stepperLimit() {
const quotaLimit = this.quota - this.quotaUsed;
let limit;
@ -125,7 +88,7 @@ export default createComponent({
return (
<div class="van-sku-stepper-stock">
<div class="van-sku-stepper-container">
<div class="van-sku__stepper-title">{this.stepperTitle || '购买数量'}</div>
<div class="van-sku__stepper-title">{this.stepperTitle || '购买数量'}</div>
<Stepper
vModel={this.currentNum}
class="van-sku__stepper"
@ -135,10 +98,6 @@ export default createComponent({
onChange={this.onChange}
/>
</div>
{!this.hideStock && <div class="van-sku__stock">{this.stockText}</div>}
{!this.hideQuotaText && this.quotaText && (
<div class="van-sku__quota">{this.quotaText}</div>
)}
</div>
);
}

View File

@ -1,6 +1,7 @@
import Sku from './Sku';
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';
@ -10,6 +11,7 @@ import constants from './constants';
Sku.SkuActions = SkuActions;
Sku.SkuHeader = SkuHeader;
Sku.SkuHeaderItem = SkuHeaderItem;
Sku.SkuMessages = SkuMessages;
Sku.SkuStepper = SkuStepper;
Sku.SkuRow = SkuRow;

View File

@ -3,6 +3,10 @@
.van-sku {
&-container {
display: flex;
flex-direction: column;
align-items: stretch;
height: 70%;
max-height: max-content; /* avoid androiod keyboard cover fields */
overflow-y: visible;
font-size: 14px;
@ -10,6 +14,7 @@
}
&-body {
flex: 1 1 auto;
max-height: 350px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
@ -26,11 +31,11 @@
&__img-wrap {
position: relative;
float: left;
width: 80px;
height: 80px;
margin-top: -10px;
width: 96px;
height: 96px;
margin: 12px 0 12px;
background: @background-color;
border-radius: 2px;
border-radius: 4px;
img {
position: absolute;
@ -45,37 +50,51 @@
}
&__goods-info {
box-sizing: border-box;
min-height: 82px;
padding: 10px 60px 10px 10px;
min-height: 96px;
padding: 12px 36px 12px 8px;
overflow: hidden;
}
}
&__goods-name {
&-header-item {
margin-top: 8px;
color: @gray-dark;
font-size: 12px;
line-height: 16px;
}
&__price-symbol {
vertical-align: middle;
font-size: 16px;
vertical-align: text-bottom;
}
&__price-num {
font-size: 16px;
font-weight: 500;
font-size: 22px;
vertical-align: middle;
}
&__goods-price {
margin-top: 10px;
color: @red;
}
&__price-tag {
display: inline-block;
margin-left: 8px;
padding: 0 5px;
color: @red;
font-size: 12px;
line-height: 16px;
vertical-align: middle;
background-color: @sku-price-tag-color;
border-radius: 8px;
}
&__close-icon {
position: absolute;
top: 10px;
top: 12px;
right: 15px;
color: @gray-dark;
color: @sku-icon-gray-color;
font-size: 20px;
text-align: center;
}
@ -93,40 +112,53 @@
/* sku row */
&-row {
margin: 0 15px 10px 0;
margin: 0 3px 12px 0;
&:last-child {
margin-bottom: 0;
}
&__title {
padding-bottom: 10px;
padding-bottom: 12px;
}
&__item {
display: inline-block;
box-sizing: border-box;
min-width: 52px;
height: 28px;
margin: 0 10px 10px 0;
padding: 5px 9px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
margin: 0 12px 12px 0;
color: @text-color;
font-size: 12px;
font-size: 13px;
line-height: 16px;
text-align: center;
border: 1px solid @gray-dark;
border-radius: 3px;
vertical-align: middle;
background: @sku-item-background-color;
border-radius: 4px;
&-img {
width: 24px;
height: 24px;
margin: 4px 0 4px 4px;
object-fit: cover;
border-radius: 2px;
}
&-name {
padding: 8px;
}
&--active {
color: @white;
background: @red;
border-color: @red;
color: @red;
background: @sku-item-active-background-color;
}
&--disabled {
color: @gray;
background: @active-color;
border-color: @border-color;
.van-sku-row__item-img {
opacity: .3;
}
}
}
}
@ -176,6 +208,7 @@
}
.van-cell__value {
overflow: visible;
text-align: left;
}
}
@ -184,35 +217,23 @@
&-img-uploader {
display: inline-block;
&__header {
padding: 0 10px;
color: @text-color;
font-size: 12px;
line-height: 24px;
border: 1px solid @border-color;
border-radius: 3px;
.van-icon {
top: 3px;
margin-right: 5px;
font-size: 14px;
}
&__uploader {
vertical-align: middle;
}
&__img {
position: relative;
float: left;
width: 60px;
height: 60px;
margin: 10px 10px 0 0;
border: 1px solid @border-color;
width: 64px;
height: 64px;
margin-right: 8px;
background: @sku-item-background-color;
border-radius: 2px;
img {
position: relative;
top: 50%;
max-width: 100%;
max-height: 100%;
transform: translateY(-50%);
width: 100%;
height: 100%;
object-fit: contain;
}
}
@ -222,7 +243,8 @@
right: -14px;
z-index: 1;
padding: 6px;
color: @red;
color: @sku-upload-mask-color;
opacity: .8;
&::before {
background-color: @white;
@ -230,15 +252,33 @@
}
}
&__uploading {
&__mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 20px;
height: 20px;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: white;
background: @sku-upload-mask-color;
}
&__warn-text {
margin-top: 6px;
font-size: 12px;
line-height: 14px;
}
&__trigger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: @sku-icon-gray-color;
}
}

View File

@ -651,3 +651,10 @@
@uploader-file-icon-color: @gray-darker;
@uploader-file-name-font-size: @font-size-sm;
@uploader-file-name-text-color: @gray-darker;
// Sku
@sku-price-tag-color: rgba(227, 20, 54, .1);
@sku-item-background-color: #f7f8fa;
@sku-item-active-background-color: rgba(227, 20, 54, .1);
@sku-icon-gray-color: #dcdde0;
@sku-upload-mask-color: rgba(50, 50, 51, .8);