Merge branch 'dev' of https://github.com/youzan/vant into dev

This commit is contained in:
陈嘉涵 2018-02-07 11:33:28 +08:00
commit 6c7f8342b9
45 changed files with 916 additions and 98 deletions

View File

@ -31,6 +31,8 @@ Vue.component('demo-section', DemoSection);
Locale.add({
'zh-CN': {
add: '增加',
decrease: '减少',
red: '红色',
orange: '橙色',
yellow: '黄色',
@ -56,6 +58,8 @@ Locale.add({
passwordPlaceholder: '请输入密码'
},
'en-US': {
add: 'Add',
decrease: 'Decrease',
red: 'Red',
orange: 'Orange',
yellow: 'Yellow',

View File

@ -31,6 +31,7 @@ export default {
'cell-swipe': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/cell-swipe'), 'cell-swipe')), 'cell-swipe')),
'cell': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/cell'), 'cell')), 'cell')),
'checkbox': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/checkbox'), 'checkbox')), 'checkbox')),
'circle': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/circle'), 'circle')), 'circle')),
'contact': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/contact'), 'contact')), 'contact')),
'coupon': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/coupon'), 'coupon')), 'coupon')),
'datetime-picker': asyncWrapper(r => require.ensure([], () => r(componentWrapper(require('./views/datetime-picker'), 'datetime-picker')), 'datetime-picker')),

View File

@ -0,0 +1,78 @@
<template>
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-circle
v-model="currentRate1"
:rate="rate"
:speed="100"
size="120px"
:text="currentRate1.toFixed(0) + '%'"
/>
<van-circle
v-model="currentRate2"
color="#13ce66"
fill="#fff"
:rate="rate"
size="120px"
layer-color="#eee"
:speed="100"
:stroke-width="60"
:clockwise="false"
:text="currentRate2.toFixed(0) + '%'"
/>
<div>
<van-button :text="$t('add')" type="primary" size="small" @click="add" />
<van-button :text="$t('decrease')" type="danger" size="small" @click="reduce" />
</div>
</demo-block>
</demo-section>
</template>
<script>
const format = rate => Math.min(Math.max(rate, 0), 100);
export default {
i18n: {
'zh-CN': {
title2: '样式定制'
},
'en-US': {
title2: 'Custom Style'
}
},
data() {
return {
rate: 30,
currentRate1: 0,
currentRate2: 0
};
},
methods: {
add() {
this.rate = format(this.rate + 20);
},
reduce() {
this.rate = format(this.rate - 20);
}
}
};
</script>
<style lang="postcss">
.demo-circle {
.van-circle {
margin-left: 15px;
}
.van-button {
margin: 15px 0 0 10px;
&:first-of-type {
margin-left: 15px;
}
}
}
</style>

View File

@ -13,6 +13,7 @@
:reset-stepper-on-hide="true"
:reset-selected-sku-on-hide="true"
:disable-stepper-input="true"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -23,7 +24,7 @@
<demo-block :title="$t('title2')">
<div class="sku-container">
<van-sku
v-model="showBase"
v-model="showStepper"
:sku="$t('sku').sku"
:goods="$t('sku').goods_info"
:goods-id="$t('sku').goods_id"
@ -34,7 +35,7 @@
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
<van-button type="primary" @click="showBase = true" block>{{ $t('title2') }}</van-button>
<van-button type="primary" @click="showStepper = true" block>{{ $t('title2') }}</van-button>
</div>
</demo-block>
@ -94,6 +95,7 @@ export default {
return {
showBase: false,
showCustom: false,
showStepper: false,
initialSku: {
s1: '30349',
s2: '1193'
@ -113,6 +115,14 @@ export default {
}
}
}
},
messageConfig: {
uploadImg: () => {
return new Promise((resolve) => {
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
});
},
uploadMaxSize: 3
}
};
},

View File

@ -61,6 +61,7 @@ Actionsheet will get another style if there is a `title` prop.
| cancel-text | Text of cancel button | `String` | - | - |
| overlay | Whether to show overlay | `Boolean` | - | - |
| close-on-click-overlay | Whether to close when click overlay | `Boolean` | - | - |
| get-container | Return the mount node for actionsheet | `Function` | - | `() => HTMLElement` |
### Data struct of actions

View File

@ -0,0 +1,69 @@
## Circle
### Install
``` javascript
import { Circle } from 'vant';
Vue.use(Circle);
```
### Usage
#### Basic Usage
```html
<van-circle
v-model="currentRate"
:rate="30"
:speed="100"
:text="text"
/>
```
``` javascript
export default {
data() {
return {
currentRate: 0
};
},
computed: {
text() {
return this.currentRate.toFixed(0) + '%'
}
}
};
```
#### Custom style
```html
<van-circle
v-model="currentRate"
color="#13ce66"
fill="#fff"
size="120px"
layer-color="#eee"
:text="text"
:rate="rate"
:speed="100"
:clockwise="false"
:stroke-width="60"
/>
```
### API
| Attribute | Description | Type | Default | Accepted Values |
|-----------|-----------|-----------|-------------|-------------|
| v-model | Current rate | `Number` | - | - |
| rate | Target rate | `Number` | `100` | - |
| size | Circle size | `String` | `100px` | - |
| color | Progress bar color | `String` | `#38f` | - |
| layer-color | Layer color | `String` | `#fff` | - |
| fill | Fill color | `String` | `none` | - |
| speed | Animate speedrate/s| `Number` | - | - |
| text | Text | `String` | - | - |
| stroke-width | Stroke width | `Number` | `40` | - |
| clockwise | Is clockwise | `Boolean` | `true` | - |

View File

@ -48,3 +48,4 @@ Use `position` prop to set popup display position
| close-on-click-overlay | Close popup when click overlay | `Boolean` | `true` | - |
| transition | Transition | `String` | `popup-slide` | - |
| prevent-scroll | Prevent background scroll | `Boolean` | `false` | - |
| get-container | Return the mount node for Popup | `Function` | - | `() => HTMLElement` |

View File

@ -22,6 +22,7 @@ Vue.use(Sku);
:reset-stepper-on-hide="resetStepperOnHide"
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
:disable-stepper-input="disableStepperInput"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -90,6 +91,8 @@ Vue.use(Sku);
| disable-stepper-input | Whether to disable stepper input | `Boolean` | `false` | - |
| 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 | `Function` | - | `() => HTMLElement` |
### Event
@ -200,6 +203,26 @@ customStepperConfig: {
}
```
#### messageConfig Data Structure
```javascript
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,
// placehold config
placeholderMap: {
text: 'xxx',
tel: 'xxx',
...
}
}
```
#### Event Params Data Structure
```javascript

View File

@ -25,6 +25,7 @@ export default {
'zh-CN/changelog-generated': wrapper(r => require.ensure([], () => r(require('./zh-CN/changelog-generated.md')), 'zh-CN/changelog-generated')),
'zh-CN/changelog': wrapper(r => require.ensure([], () => r(require('./zh-CN/changelog.md')), 'zh-CN/changelog')),
'zh-CN/checkbox': wrapper(r => require.ensure([], () => r(require('./zh-CN/checkbox.md')), 'zh-CN/checkbox')),
'zh-CN/circle': wrapper(r => require.ensure([], () => r(require('./zh-CN/circle.md')), 'zh-CN/circle')),
'zh-CN/contact': wrapper(r => require.ensure([], () => r(require('./zh-CN/contact.md')), 'zh-CN/contact')),
'zh-CN/coupon': wrapper(r => require.ensure([], () => r(require('./zh-CN/coupon.md')), 'zh-CN/coupon')),
'zh-CN/datetime-picker': wrapper(r => require.ensure([], () => r(require('./zh-CN/datetime-picker.md')), 'zh-CN/datetime-picker')),
@ -77,6 +78,7 @@ export default {
'en-US/cell': wrapper(r => require.ensure([], () => r(require('./en-US/cell.md')), 'en-US/cell')),
'en-US/changelog': wrapper(r => require.ensure([], () => r(require('./en-US/changelog.md')), 'en-US/changelog')),
'en-US/checkbox': wrapper(r => require.ensure([], () => r(require('./en-US/checkbox.md')), 'en-US/checkbox')),
'en-US/circle': wrapper(r => require.ensure([], () => r(require('./en-US/circle.md')), 'en-US/circle')),
'en-US/contact': wrapper(r => require.ensure([], () => r(require('./en-US/contact.md')), 'en-US/contact')),
'en-US/coupon': wrapper(r => require.ensure([], () => r(require('./en-US/coupon.md')), 'en-US/coupon')),
'en-US/datetime-picker': wrapper(r => require.ensure([], () => r(require('./en-US/datetime-picker.md')), 'en-US/datetime-picker')),

View File

@ -72,6 +72,7 @@ export default {
| cancel-text | 取消按钮文案 | `String` | - | - |
| overlay | 是否显示遮罩 | `Boolean` | - | - |
| close-on-click-overlay | 点击遮罩是否关闭`Actionsheet` | `Boolean` | - | - |
| get-container | 指定挂载的 HTML 节点 | `Function` | - | `() => HTMLElement` |
### actions

View File

@ -0,0 +1,70 @@
## Circle 环形进度条
### 使用指南
``` javascript
import { Circle } from 'vant';
Vue.use(Circle);
```
### 代码演示
#### 基础用法
通过 `rate` 指定目标进度,`v-model` 代表当前进度,`speed` 控制动画速度
```html
<van-circle
v-model="currentRate"
:rate="30"
:speed="100"
:text="text"
/>
```
``` javascript
export default {
data() {
return {
currentRate: 0
};
},
computed: {
text() {
return this.currentRate.toFixed(0) + '%'
}
}
};
```
#### 样式定制
```html
<van-circle
v-model="currentRate"
color="#13ce66"
fill="#fff"
size="120px"
layer-color="#eee"
:text="text"
:rate="rate"
:speed="100"
:clockwise="false"
:stroke-width="60"
/>
```
### API
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
|-----------|-----------|-----------|-------------|-------------|
| v-model | 当前进度 | `Number` | - | - |
| rate | 目标进度 | `Number` | `100` | - |
| size | 圆环直径 | `String` | `100px` | - |
| color | 进度条颜色 | `String` | `#38f` | - |
| layer-color | 轨道颜色 | `String` | `#fff` | - |
| fill | 填充颜色 | `String` | `none` | - |
| speed | 动画速度(单位为 rate/s| `Number` | - | - |
| text | 文字 | `String` | - | - |
| stroke-width | 进度条宽度 | `Number` | `40` | - |
| clockwise | 是否顺时针增加 | `Boolean` | `true` | - |

View File

@ -48,3 +48,4 @@ export default {
| close-on-click-overlay | 点击蒙层是否关闭 Popup | `Boolean` | `true` | - |
| transition | transition 名称 | `String` | `popup-slide` | - |
| prevent-scroll | 是否防止滚动穿透 | `Boolean` | `false` | - |
| get-container | 指定弹出层挂载的 HTML 节点 | `Function` | - | `() => HTMLElement` |

View File

@ -22,6 +22,7 @@ Vue.use(Sku);
:reset-stepper-on-hide="resetStepperOnHide"
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
:disable-stepper-input="disableStepperInput"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -91,6 +92,8 @@ Vue.use(Sku);
| disable-stepper-input | 是否禁用sku中stepper的input框 | `Boolean` | `false` | - |
| stepper-title | 数量选择组件左侧文案 | `String` | `购买数量` | - |
| custom-stepper-config | 步进器相关自定义配置 | `Object` | `{}` | - |
| message-config | 留言相关配置 | `Object` | `{}` | - |
| get-container | 指定挂载的 HTML 节点 | `Function` | - | `() => HTMLElement` |
### Event
@ -206,6 +209,26 @@ customStepperConfig: {
}
```
#### messageConfig Data Structure
```javascript
messageConfig: {
// 图片上传回调需要返回一个promisepromise正确执行的结果需要是一个图片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,
// placehold配置
placeholderMap: {
text: 'xxx',
tel: 'xxx',
...
}
}
```
#### 添加购物车和点击购买回调函数接收的 skuData 对象结构
```javascript
skuData: {

View File

@ -80,6 +80,10 @@ module.exports = {
path: '/cell',
title: 'Cell - 单元格'
},
{
path: '/circle',
title: 'Circle - 环形进度条'
},
{
path: '/icon',
title: 'Icon - 图标'
@ -358,6 +362,10 @@ module.exports = {
path: '/cell',
title: 'Cell'
},
{
path: '/circle',
title: 'Circle'
},
{
path: '/icon',
title: 'Icon'

View File

@ -60,10 +60,6 @@ export default create({
}
},
mounted() {
this.value && this.open();
},
methods: {
onClickItem(item) {
if (typeof item.callback === 'function') {

126
packages/circle/index.vue Normal file
View File

@ -0,0 +1,126 @@
<template>
<div class="van-circle" :style="style">
<svg viewBox="0 0 1060 1060">
<path class="van-circle__hover" :style="hoverStyle" :d="path" />
<path class="van-circle__layer" :style="layerStyle" :d="path" />
</svg>
<slot>
<div class="van-circle__text">{{ text }}</div>
</slot>
</div>
</template>
<script>
import { create } from '../utils';
import { raf, cancel } from '../utils/raf';
export default create({
name: 'van-circle',
props: {
text: String,
value: Number,
speed: Number,
size: {
type: String,
default: '100px'
},
fill: {
type: String,
default: 'none'
},
rate: {
type: Number,
default: 100
},
layerColor: {
type: String,
default: '#fff'
},
color: {
type: String,
default: '#38f'
},
strokeWidth: {
type: Number,
default: 40
},
clockwise: {
type: Boolean,
default: true
}
},
beforeCreate() {
this.perimeter = Math.PI * 1000;
this.path = 'M 530 530 m -500, 0 a 500, 500 0 1, 1 1000, 0 a 500, 500 0 1, 1 -1000, 0';
},
computed: {
style() {
return {
width: this.size,
height: this.size
};
},
layerStyle() {
let offset = this.perimeter * (100 - this.value) / 100;
offset = this.clockwise ? offset : this.perimeter * 2 - offset;
return {
stroke: `${this.color}`,
strokeDashoffset: `${offset}px`,
strokeWidth: `${this.strokeWidth + 1}px`
};
},
hoverStyle() {
return {
fill: `${this.fill}`,
stroke: `${this.layerColor}`,
strokeWidth: `${this.strokeWidth}px`
};
}
},
mounted() {
this.render();
},
watch: {
rate() {
this.render();
}
},
methods: {
render() {
this.startTime = Date.now();
this.startRate = this.value;
this.endRate = this.format(this.rate);
this.increase = this.endRate > this.startRate;
this.duration = Math.abs((this.startRate - this.endRate) * 1000 / this.speed);
if (this.speed) {
cancel(this.rafId);
this.rafId = raf(this.animate);
} else {
this.$emit('input', this.endRate);
}
},
animate() {
const now = Date.now();
const progress = Math.min((now - this.startTime) / this.duration, 1);
const rate = progress * (this.endRate - this.startRate) + this.startRate;
this.$emit('input', this.format(parseFloat(rate.toFixed(1))));
if (this.increase ? rate < this.endRate : rate > this.endRate) {
this.rafId = raf(this.animate);
}
},
format(rate) {
return Math.min(Math.max(rate, 0), 100);
}
}
});
</script>

View File

@ -53,7 +53,6 @@ import Cell from '../cell';
import CellGroup from '../cell-group';
import CouponItem from './Item';
import Field from '../field';
import Popup from '../popup';
import VanButton from '../button';
export default create({
@ -64,7 +63,6 @@ export default create({
Cell,
CellGroup,
Field,
Popup,
CouponItem
},

View File

@ -12,6 +12,7 @@ import CellGroup from './cell-group';
import CellSwipe from './cell-swipe';
import Checkbox from './checkbox';
import CheckboxGroup from './checkbox-group';
import Circle from './circle';
import Col from './col';
import ContactCard from './contact-card';
import ContactEdit from './contact-edit';
@ -77,6 +78,7 @@ const components = [
CellSwipe,
Checkbox,
CheckboxGroup,
Circle,
Col,
ContactCard,
ContactEdit,
@ -149,6 +151,7 @@ export {
CellSwipe,
Checkbox,
CheckboxGroup,
Circle,
Col,
ContactCard,
ContactEdit,

View File

@ -113,10 +113,12 @@ export default {
},
vanSkuMessages: {
fill: 'Please fill',
upload: 'Please upload',
number: 'Please fill in the correct number format message',
email: 'Please fill in the correct email message',
idcard: 'Please fill in the correct ID number message',
overlimit: 'not more than 200 words',
onePic: 'only one picture',
placeholder: {
'id_no': 'Idcard Number',
text: 'Text',
@ -127,6 +129,15 @@ export default {
textarea: 'Text'
}
},
vanSkuImgUploader: {
or: 'Or',
uploading: 'Uploading...',
rephoto: 'Take Again',
photo: 'Take',
reselect: 'Reselect',
select: 'Select Photo',
maxSize: maxSize => `The upload limit is up to ${maxSize}MBplease try to compress the photo`
},
vanSkuStepper: {
title: 'Quantity',
remain: count => `Remain ${count} items`,

View File

@ -117,10 +117,12 @@ export default {
},
vanSkuMessages: {
fill: '请填写',
upload: '请上传',
number: '请填写正确的数字格式留言',
email: '请填写正确的邮箱',
'id_no': '请填写正确的身份证号码',
overlimit: '写的太多了不要超过200字',
onePic: '仅限一张',
placeholder: {
'id_no': '输入18位身份证号码',
text: '输入文本',
@ -131,6 +133,15 @@ export default {
textarea: '点击填写段落文本'
}
},
vanSkuImgUploader: {
or: '或',
uploading: '正在上传...',
rephoto: '重拍',
photo: '拍照',
reselect: '重新选择照片',
select: '选择照片',
maxSize: maxSize => `最大可上传图片为${maxSize}MB请尝试压缩图片尺寸`
},
vanSkuStepper: {
title: '购买数量',
remain: count => `剩余${count}`,

View File

@ -19,6 +19,8 @@ export default {
zIndex: [String, Number],
// prevent touchmove scroll
preventScroll: Boolean,
// return the mount node for popup
getContainer: Function,
// prevent body scroll
lockOnScroll: {
type: Boolean,
@ -26,17 +28,8 @@ export default {
}
},
watch: {
value(val) {
this[val ? 'open' : 'close']();
}
},
beforeMount() {
this._popupId = 'popup-' + context.plusKey('idSeed');
},
data() {
this._popupId = 'popup-' + context.plusKey('idSeed');
return {
opened: false,
pos: {
@ -46,6 +39,29 @@ export default {
};
},
watch: {
value(val) {
this[val ? 'open' : 'close']();
},
getContainer() {
this.move();
}
},
mounted() {
if (this.getContainer) {
this.move();
}
if (this.value) {
this.open();
}
},
beforeDestroy() {
this.doAfterClose();
},
methods: {
recordPosition(e) {
this.pos = {
@ -65,12 +81,14 @@ export default {
let status = '11';
/* istanbul ignore next */
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01';
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10';
}
/* istanbul ignore next */
if (
status !== '11' &&
isVertical &&
@ -82,6 +100,7 @@ export default {
},
open() {
/* istanbul ignore next */
if (this.opened || this.$isServer) {
return;
}
@ -137,10 +156,14 @@ export default {
off(document, 'touchstart', this.recordPosition);
off(document, 'touchmove', this.watchTouchMove);
}
}
},
},
beforeDestroy() {
this.doAfterClose();
move() {
if (this.getContainer) {
this.getContainer().appendChild(this.$el);
} else if (this.$parent) {
this.$parent.$el.appendChild(this.$el);
}
}
}
};

View File

@ -41,6 +41,7 @@ const manager = {
const { id, dom } = config;
const exist = context.stack.some(item => item.id === id);
/* istanbul ignore next */
if (!exist) {
const targetNode = dom && dom.parentNode && dom.parentNode.nodeType !== 11 ? dom.parentNode : document.body;
context.stack.push({ instance, id, config, targetNode });

View File

@ -13,6 +13,7 @@
<li
v-for="(option, index) in options"
v-text="getOptionText(option)"
class="van-ellipsis"
:class="{
'van-picker-column--disabled': isDisabled(option),
'van-picker-column--selected': index === currentIndex

View File

@ -3,8 +3,8 @@
<div class="van-picker__toolbar van-hairline--top-bottom" v-if="showToolbar">
<slot>
<div class="van-picker__cancel" @click="emit('cancel')">{{ cancelButtonText || $t('cancel') }}</div>
<div class="van-picker__title van-ellipsis" v-if="title" v-text="title" />
<div class="van-picker__confirm" @click="emit('confirm')">{{ confirmButtonText || $t('confirm') }}</div>
<div class="van-picker__title" v-if="title" v-text="title" />
</slot>
</div>
<div class="van-picker__columns" @touchmove.prevent>

View File

@ -41,12 +41,6 @@ export default create({
currentValue: false,
currentTransition: transition
};
},
mounted() {
if (this.value) {
this.open();
}
}
});
</script>

View File

@ -1,5 +1,12 @@
<template>
<popup v-model="show" v-if="!isSkuEmpty" position="bottom" lock-on-scroll prevent-scroll>
<popup
v-if="!isSkuEmpty"
v-model="show"
position="bottom"
lock-on-scroll
prevent-scroll
:get-container="getContainer"
>
<div class="van-sku-container">
<div class="van-sku-layout">
<!-- sku-header -->
@ -74,7 +81,7 @@
<sku-messages
ref="skuMessages"
:goods-id="goodsId"
:message-placeholder-map="messagePlaceholderMap"
:message-config="messageConfig"
:messages="sku.messages"
/>
</slot>
@ -134,6 +141,7 @@ export default create({
goodsId: [Number, String],
stepperTitle: String,
hideStock: Boolean,
getContainer: Function,
resetStepperOnHide: Boolean,
resetSelectedSkuOnHide: Boolean,
disableStepperInput: Boolean,
@ -157,9 +165,13 @@ export default create({
type: Number,
default: 200
},
messagePlaceholderMap: {
messageConfig: {
type: Object,
default: () => ({})
default: () => ({
placeholderMap: {},
uploadImg: () => Promise.resolve(),
uploadMaxSize: 5
})
},
customStepperConfig: {
type: Object,

View File

@ -4,7 +4,7 @@
<img class="van-sku__goods-img" :src="goodsImg" >
</div>
<div class="van-sku-header__goods-info">
<div class="van-sku__goods-name">{{ goods.title }}</div>
<div class="van-sku__goods-name van-ellipsis">{{ goods.title }}</div>
<div class="van-sku__goods-price"><span class="van-sku__price-symbol"></span><span class="van-sku__price-num">{{ price }}</span></div>
<span class="van-sku__close-icon" @click="skuEventBus.$emit('sku:close')" />
</div>

View File

@ -0,0 +1,122 @@
<template>
<div class="van-sku-img-uploader">
<!-- 头部 -->
<van-uploader
:disabled="!canUpload"
:before-read="beforeReadFile"
:after-read="afterReadFile"
accept="image/*">
<div class="van-sku-img-uploader__header">
<div v-if="paddingImg">{{ $t('uploading') }}</div>
<template v-else>
<van-icon name="photograph" />
<span class="label">{{ getPhotoText(value) }}</span> {{ $t('or') }}
<van-icon name="photo" />
<span class="label">{{ getPicText(value) }}</span>
</template>
</div>
</van-uploader>
<!-- 图片列表区域 -->
<div class="van-sku-img-uploader__imglist" v-if="paddingImg || imgList.length > 0">
<!-- 已有的图片,图片右上角显示删除按钮 -->
<div
v-for="(img, index) in imgList"
:key="index"
class="van-sku-img-uploader__img-container">
<span class="van-sku-img-uploader__delete-picture" @click="deleteImg(index)">
<van-icon name="clear" />
</span>
<img :src="img">
</div>
<!-- 正在上传的图片,有上传等待提示 -->
<div
v-if="paddingImg"
class="van-sku-img-uploader__img-container">
<img :src="paddingImg">
<van-loading class="van-sku-img-uploader__uploading" type="spinner" color="black" />
</div>
</div>
</div>
</template>
<script>
import Icon from '../../icon';
import Uploader from '../../uploader';
import Loading from '../../loading';
import { create } from '../../utils';
export default create({
name: 'van-sku-img-uploader',
components: {
'van-uploader': Uploader,
'van-icon': Icon,
'van-loading': Loading
},
props: {
value: String,
uploadImg: {
type: Function,
required: true
},
maxSize: {
type: Number,
default: 6
}
},
data() {
return {
// base 64
paddingImg: ''
};
},
computed: {
imgList() {
return this.value && !this.paddingImg ? [this.value] : [];
},
canUpload() {
//
if (this.paddingImg) return false;
return true;
}
},
methods: {
getPhotoText(value) {
return value ? this.$t('rephoto') : this.$t('photo');
},
getPicText(value) {
return value ? this.$t('reselect') : this.$t('select');
},
beforeReadFile(file) {
//
if (file.size > this.maxSize * 1024 * 1024) {
Toast(this.$t('maxSize', this.maxSize));
return false;
}
return true;
},
afterReadFile(file) {
//
this.paddingImg = file.content;
this.uploadImg(file.file).then(img => {
this.updateImg(img);
this.$nextTick(() => {
this.paddingImg = '';
});
}).catch(() => {
this.paddingImg = '';
});
},
deleteImg() {
this.$emit('input', '');
},
updateImg(img) {
this.$emit('input', img);
}
}
});
</script>

View File

@ -1,14 +1,29 @@
<template>
<cell-group class="van-sku-messages">
<field
v-for="(message, index) in internalMessages"
v-model="messageValues[index]"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:label="message.name"
:placeholder="getPlaceholder(message)"
:type="getType(message)"
/>
<template v-for="(message, index) in messages">
<cell
v-if="message.type === 'image'"
class="van-sku-messages__image-cell"
:label="$t('onePic')"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:title="message.name">
<sku-img-uploader
v-model="messageValues[index].value"
:upload-img="messageConfig.uploadImg"
:max-size="messageConfig.uploadMaxSize"
/>
</cell>
<field
v-else
v-model="messageValues[index].value"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:label="message.name"
:placeholder="getPlaceholder(message)"
:type="getType(message)"
/>
</template>
</cell-group>
</template>
@ -16,30 +31,36 @@
import { create } from '../../utils';
import Field from '../../field';
import CellGroup from '../../cell-group';
import Cell from '../../cell';
import validateEmail from '../../utils/validate/email';
import validateNumber from '../../utils/validate/number';
import SkuImgUploader from './SkuImgUploader';
export default create({
name: 'van-sku-messages',
components: {
SkuImgUploader,
Field,
Cell,
CellGroup
},
props: {
messages: Array,
messagePlaceholderMap: Object,
messageConfig: Object,
goodsId: [Number, String]
},
computed: {
internalMessages() {
return Array.isArray(this.messages) ? this.messages.filter(message => message.type !== 'image') : [];
},
data() {
return {
messageValues: this.messages.map(() => ({ value: '' }))
};
},
messageValues() {
return this.internalMessages.map(() => '');
computed: {
messagePlaceholderMap() {
return this.messageConfig.placeholderMap || {};
}
},
@ -57,8 +78,9 @@ export default create({
getMessages() {
const messages = {};
this.messageValues.forEach((value, index) => {
if (this.internalMessages[index].datetime > 0) {
this.messageValues.forEach((item, index) => {
let value = item.value;
if (this.messages[index].datetime > 0) {
value = value.replace(/T/g, ' ');
}
messages[`message_${index}`] = value;
@ -70,8 +92,9 @@ export default create({
getCartMessages() {
const messages = {};
this.messageValues.forEach((value, index) => {
const message = this.internalMessages[index];
this.messageValues.forEach((item, index) => {
let value = item.value;
const message = this.messages[index];
if (message.datetime > 0) {
value = value.replace(/T/g, ' ');
}
@ -90,17 +113,16 @@ export default create({
const values = this.messageValues;
for (let i = 0; i < values.length; i++) {
const value = values[i];
const message = this.internalMessages[i];
const value = values[i].value;
const message = this.messages[i];
if (value === '') {
//
if (message.required == '1') { // eslint-disable-line
if (message.type === 'image') {
continue;
} else {
return this.$t('fill') + message.name;
}
const textType = message.type === 'image'
? 'upload'
: 'fill';
return this.$t(textType) + message.name;
}
} else {
if (message.type === 'tel' && !validateNumber(value)) {

View File

@ -9,6 +9,7 @@
:max="stepperLimit"
:disable-input="disableStepperInput"
@overlimit="onOverLimit"
@change="onChange"
/>
</div>
<div v-if="!hideStock" class="van-sku__stock">{{ $t('remain', stock) }}</div>
@ -104,7 +105,6 @@ export default create({
setCurrentNum(num) {
this.currentNum = num;
},
onOverLimit(action) {
this.skuEventBus.$emit('sku:overLimit', {
action,
@ -112,6 +112,10 @@ export default create({
quota: this.quota,
quotaUsed: this.quotaUsed
});
},
onChange(currentValue) {
const { handleStepperChange } = this.customStepperConfig;
handleStepperChange && handleStepperChange(currentValue);
}
}
});

View File

@ -8,7 +8,7 @@
</div>
<div class="van-steps__message">
<div class="van-steps__title" v-text="title" />
<div class="van-steps__desc" v-text="description" />
<div class="van-steps__desc van-ellipsis" v-text="description" />
</div>
<slot name="message-extra" />
</div>

View File

@ -22,7 +22,7 @@
@click="onClick(index)"
>
<van-node v-if="tab.$slots.title" :node="tab.$slots.title" />
<span v-else>{{ tab.title }}</span>
<span class="van-ellipsis" v-else>{{ tab.title }}</span>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
<div class="van-tree-select__nav">
<div
v-for="(item, index) in items"
class="van-tree-select__nitem"
class="van-tree-select__nitem van-ellipsis"
:class="{ 'van-tree-select__nitem--active': mainActiveIndex === index }"
@click="$emit('navclick', index)">
{{ item.text }}
@ -13,7 +13,7 @@
<div
v-for="item in subItems"
:key="item.id"
class="van-tree-select__item"
class="van-tree-select__item van-ellipsis"
:class="{ 'van-tree-select__item--active': activeId === item.id }"
@click="onItemSelect(item)">
{{ item.text }}

View File

@ -4,5 +4,6 @@
@import "./common/var.css";
@import "./common/normalize.css";
@import "./common/ellipsis.css";
@import "./common/hairline.css";
@import "./common/animation.css";

View File

@ -0,0 +1,32 @@
@import './common/var.css';
.van-circle {
position: relative;
text-align: center;
display: inline-block;
svg {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
}
&__layer {
fill: none;
stroke-dasharray: 3140px;
stroke-dashoffset: 3140px;
transform: rotate(90deg);
transform-origin: 530px 530px;
}
&__text {
top: 50%;
left: 0;
width: 100%;
color: $text-color;
position: absolute;
transform: translateY(-50%);
}
}

View File

@ -0,0 +1,5 @@
@import '../mixins/ellipsis.css';
.van-ellipsis {
@mixin ellipsis;
}

View File

@ -12,7 +12,7 @@
@import './badge.css';
@import './button.css';
@import './cell.css';
@import './card.css';
@import './circle.css';
@import './loading.css';
@import './nav-bar.css';
@import './notice-bar.css';
@ -53,6 +53,7 @@
/* business components */
@import './address-edit.css';
@import './address-list.css';
@import './card.css';
@import './contact-card.css';
@import './contact-list.css';
@import './contact-edit.css';

View File

@ -1,5 +1,4 @@
@import './common/var.css';
@import './mixins/ellipsis.css';
.van-picker {
overflow: hidden;
@ -7,8 +6,10 @@
background-color: $white;
&__toolbar {
display: flex;
height: 40px;
line-height: 40px;
justify-content: space-between;
}
&__cancel,
@ -21,17 +22,9 @@
}
}
&__cancel {
float: left;
}
&__confirm {
float: right;
}
&__title {
max-width: 50%;
text-align: center;
@mixin ellipsis;
}
&__columns {
@ -52,13 +45,12 @@
&-column {
flex: 1;
overflow: hidden;
font-size: 18px;
font-size: 17px;
text-align: center;
li {
padding: 0 10px;
padding: 0 5px;
color: $gray-darker;
@mixin ellipsis;
}
li&--selected {

View File

@ -1,4 +1,5 @@
@import './common/var.css';
@import './mixins/clearfix.css';
.van-sku {
&-container {
@ -67,9 +68,6 @@
&__goods-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__price-symbol {
@ -147,7 +145,7 @@
margin-right: 20px;
}
}
&__stepper {
top: 7px;
left: 4px;
@ -166,13 +164,85 @@
color: $gray-dark;
font-size: 12px;
}
&__quota {
display: inline-block;
color: $red;
font-size: 12px;
}
&-messages {
&__image-cell {
.van-cell__title {
width: 90px;
}
.van-cell__value {
text-align: left;
}
}
}
&-img-uploader {
display: inline-block;
&__header {
padding: 0 10px;
border: 1px solid #e5e5e5;
line-height: 24px;
border-radius: 3px;
font-size: 12px;
.van-icon {
top: 3px;
margin-right: 5px;
font-size: 14px;
}
}
&__imglist {
@mixin clearfix;
}
&__img-container {
height: 60px;
width: 60px;
margin-top: 10px;
margin-right: 10px;
float: left;
position: relative;
border: #e5e5e5 1px solid;
img {
max-width: 100%;
max-height: 100%;
top: 50%;
position: relative;
transform: translateY(-50%);
}
}
&__delete-picture {
position: absolute;
color: $red;
top: -10px;
right: -17px;
z-index: 1;
width: 22px;
height: 22px;
}
&__uploading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: 20px;
height: 20px;
}
}
/* sku actions */
&-actions {
display: flex;

View File

@ -1,5 +1,4 @@
@import './common/var.css';
@import './mixins/ellipsis.css';
.van-steps {
overflow: hidden;
@ -49,7 +48,6 @@
font-size: 12px;
line-height: 1.5;
color: $gray-dark;
@mixin ellipsis;
}
}

View File

@ -1,5 +1,4 @@
@import './common/var.css';
@import './mixins/ellipsis.css';
$van-tabs-line-height: 44px;
$van-tabs-card-height: 30px;
@ -118,7 +117,6 @@ $van-tabs-card-height: 30px;
span {
display: block;
@mixin ellipsis;
}
&:active {

View File

@ -22,7 +22,6 @@
line-height: 44px;
padding: 0 15px;
background-color: $white;
@mixin ellipsis;
&--active {
background-color: $background-color;
@ -41,7 +40,6 @@
line-height: 44px;
padding-left: 5px;
padding-right: 18px;
@mixin ellipsis;
&--active {
color: $button-danger-background-color;
@ -54,5 +52,6 @@
right: 0;
top: 0;
bottom: 0;
line-height: inherit;
}
}

View File

@ -0,0 +1,65 @@
import { mount } from 'avoriaz';
import Circle from 'packages/circle';
describe('Circle', () => {
let wrapper;
afterEach(() => {
wrapper && wrapper.destroy();
});
it('create a circle', () => {
wrapper = mount(Circle, {
propsData: {
text: 'test'
}
});
expect(wrapper.hasClass('van-circle')).to.be.true;
expect(wrapper.find('.van-circle__text')[0].text()).to.equal('test');
});
it('circle rate', done => {
let currentRate = 0;
wrapper = mount(Circle, {
propsData: {
rate: 0,
value: 0,
clockwise: false
}
});
wrapper.vm.$on('input', rate => {
currentRate = rate;
});
wrapper.vm.rate = 50;
setTimeout(() => {
expect(currentRate).to.equal(50);
done();
}, 100);
});
it('circle animation', done => {
let currentRate = 0;
wrapper = mount(Circle, {
propsData: {
rate: 0,
value: 0,
speed: 500,
clockwise: false
}
});
wrapper.vm.$on('input', rate => {
currentRate = rate;
});
wrapper.vm.rate = 50;
setTimeout(() => {
expect(currentRate === 50).to.be.false;
setTimeout(() => {
expect(currentRate === 50).to.be.true;
done();
}, 200);
}, 50);
});
});

View File

@ -22,7 +22,10 @@ describe('Popup', () => {
it('create a show popup', (done) => {
wrapper = mount(Popup, {
propsData: {
value: false
value: false,
zIndex: 100,
overlay: false,
lockOnScroll: true
}
});
@ -125,4 +128,23 @@ describe('Popup', () => {
expect(wrapper.vm.lockOnScroll).to.be.true;
});
it('get container prop', done => {
const testNode = document.createElement('div');
document.body.appendChild(testNode);
wrapper = mount(Popup, {
propsData: {
getContainer: () => testNode
}
});
expect(wrapper.vm.$el.parentNode === testNode).to.be.true;
wrapper.vm.getContainer = () => document.body;
setTimeout(() => {
expect(wrapper.vm.$el.parentNode === document.body).to.be.true;
done();
}, 100);
});
});

View File

@ -1,4 +1,5 @@
import Sku from 'packages/sku';
import Uploader from 'packages/uploader';
import Toast from 'packages/toast';
import { mount } from 'avoriaz';
import { DOMChecker } from '../utils';
@ -13,6 +14,13 @@ const initialSku = {
};
goods.picture = goods.picture[0];
const File = function() {
this.name = 'test';
this.size = 10000;
};
const mockFile = new File([], '/Users');
describe('Sku', (done) => {
let wrapper;
afterEach(() => {
@ -239,7 +247,15 @@ describe('Sku', (done) => {
value: true,
sku: data.sku,
goodsId: data.goods_id,
goods: goods
goods: goods,
messageConfig: {
uploadImg: () => {
return new Promise((resolve) => {
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
});
},
uploadMaxSize: 3
}
}
});
@ -247,12 +263,15 @@ describe('Sku', (done) => {
const skuMessages = wrapper.find('.van-sku-messages')[0];
const inputs = skuMessages.find('input');
const textarea = skuMessages.find('textarea')[0];
const uploader = wrapper.find(Uploader)[0];
// 修改留言内容
inputs[0].element.value = 123;
// 测试身份证号
inputs[1].element.value = 234;
inputs[0].trigger('input');
inputs[1].trigger('input');
// 测试图片
uploader.vm.onChange({ target: { files: [mockFile] }});
wrapper.vm.$nextTick(() => {
// 点击购买
@ -276,9 +295,9 @@ describe('Sku', (done) => {
textarea.element.value = '';
// 测试数字留言
inputs[2].element.value = 'abc';
inputs[3].element.value = 'abc';
textarea.trigger('input');
inputs[2].trigger('input');
inputs[3].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
@ -286,10 +305,10 @@ describe('Sku', (done) => {
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('请填写正确的数字格式留言');
inputs[2].element.value = 0;
inputs[3].element.value = 345;
inputs[2].trigger('input');
inputs[3].element.value = 0;
inputs[4].element.value = 345;
inputs[3].trigger('input');
inputs[4].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');