mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
add PullRefresh component
This commit is contained in:
parent
527e12768b
commit
9eb825f54d
115
docs/examples-docs/pull-refresh.md
Normal file
115
docs/examples-docs/pull-refresh.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<style>
|
||||||
|
.demo-pull-refresh {
|
||||||
|
.zan-doc-demo-block__title,
|
||||||
|
.zan-doc-demo-block__subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-pull-refresh {
|
||||||
|
height: 450px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.zan-doc-demo-block__title {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Toast } from 'packages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isLoading() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Toast('刷新成功');
|
||||||
|
this.isLoading = false;
|
||||||
|
this.count++;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const head = document.querySelector('.van-pull-refresh__head');
|
||||||
|
head.insertAdjacentHTML('afterend', '<h1 class="zan-doc-demo-block__title">PullRefresh 下拉刷新</h1>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## PullRefresh 下拉刷新
|
||||||
|
|
||||||
|
### 使用指南
|
||||||
|
``` javascript
|
||||||
|
import { PullRefresh } from 'vant';
|
||||||
|
|
||||||
|
Vue.component(PullRefresh.name, PullRefresh);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
```html
|
||||||
|
<!-- 通过 v-model 控制加载状态 -->
|
||||||
|
<van-pull-refresh v-model="isLoading">
|
||||||
|
<p>刷新次数: {{ count }}</p>
|
||||||
|
</van-pull-refresh>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isLoading() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Toast('刷新成功');
|
||||||
|
this.isLoading = false;
|
||||||
|
this.count++;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
|
||||||
|
|-----------|-----------|-----------|-------------|-------------|
|
||||||
|
| v-model | 是否在加载中 | `Boolean` | - | - |
|
||||||
|
| pullingText | 下拉过程中顶部文案 | `String` | `下拉即可刷新...` | - |
|
||||||
|
| loosingText | 释放过程中顶部文案 | `String` | `释放即可刷新...` | - |
|
||||||
|
| loadingText | 加载过程中顶部文案 | `String` | `加载中...` | - |
|
||||||
|
| animationDuration | 动画时长 | `Number` | `300` | - |
|
||||||
|
| headHeight | 顶部内容高度 | `Number` | `50` | - |
|
||||||
|
|
||||||
|
### Slot
|
||||||
|
|
||||||
|
| name | 描述 |
|
||||||
|
|-----------|-----------|
|
||||||
|
| - | 自定义内容 |
|
||||||
|
| normal | 非下拉状态时顶部内容 |
|
||||||
|
| pulling | 下拉过程中顶部内容 |
|
||||||
|
| loosing | 释放过程中顶部内容 |
|
||||||
|
| loading | 加载过程中顶部内容 |
|
@ -176,6 +176,10 @@ module.exports = {
|
|||||||
"path": "/picker",
|
"path": "/picker",
|
||||||
"title": "Picker 选择器"
|
"title": "Picker 选择器"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/pull-refresh",
|
||||||
|
"title": "PullRefresh 下拉刷新"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/toast",
|
"path": "/toast",
|
||||||
"title": "Toast 轻提示"
|
"title": "Toast 轻提示"
|
||||||
|
@ -23,18 +23,19 @@ window.changePath = function(path) {
|
|||||||
|
|
||||||
function iframeReady(iframe, callback) {
|
function iframeReady(iframe, callback) {
|
||||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const interval = () => {
|
||||||
|
if (iframe.contentWindow.changePath) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
interval();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (doc.readyState === 'complete') {
|
if (doc.readyState === 'complete') {
|
||||||
callback();
|
interval();
|
||||||
} else {
|
} else {
|
||||||
const interval = () => {
|
|
||||||
if (iframe.contentWindow.changePath) {
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
interval();
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
iframe.onload = interval;
|
iframe.onload = interval;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import PayOrder from './pay-order';
|
|||||||
import Picker from './picker';
|
import Picker from './picker';
|
||||||
import Popup from './popup';
|
import Popup from './popup';
|
||||||
import Progress from './progress';
|
import Progress from './progress';
|
||||||
|
import PullRefresh from './pull-refresh';
|
||||||
import Quantity from './quantity';
|
import Quantity from './quantity';
|
||||||
import Radio from './radio';
|
import Radio from './radio';
|
||||||
import RadioGroup from './radio-group';
|
import RadioGroup from './radio-group';
|
||||||
@ -81,6 +82,7 @@ const components = [
|
|||||||
Picker,
|
Picker,
|
||||||
Popup,
|
Popup,
|
||||||
Progress,
|
Progress,
|
||||||
|
PullRefresh,
|
||||||
Quantity,
|
Quantity,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
@ -147,6 +149,7 @@ export {
|
|||||||
Picker,
|
Picker,
|
||||||
Popup,
|
Popup,
|
||||||
Progress,
|
Progress,
|
||||||
|
PullRefresh,
|
||||||
Quantity,
|
Quantity,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
176
packages/pull-refresh/index.vue
Normal file
176
packages/pull-refresh/index.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="van-pull-refresh"
|
||||||
|
:style="style"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
@touchcalcel="onTouchEnd"
|
||||||
|
>
|
||||||
|
<div class="van-pull-refresh__head">
|
||||||
|
<slot name="normal" v-if="status === 'normal'"></slot>
|
||||||
|
<slot name="pulling" v-if="status === 'pulling'">
|
||||||
|
<span class="van-pull-refresh__text">{{ pullingText }}</span>
|
||||||
|
</slot>
|
||||||
|
<slot name="loosing" v-if="status === 'loosing'">
|
||||||
|
<span class="van-pull-refresh__text">{{ loosingText }}</span>
|
||||||
|
</slot>
|
||||||
|
<slot name="loading" v-if="status === 'loading'">
|
||||||
|
<div class="van-pull-refresh__loading">
|
||||||
|
<van-loading />
|
||||||
|
<span>{{ loadingText }}</span>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from '../loading';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'van-pull-refresh',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pullingText: {
|
||||||
|
type: String,
|
||||||
|
default: '下拉即可刷新...'
|
||||||
|
},
|
||||||
|
loosingText: {
|
||||||
|
type: String,
|
||||||
|
default: '释放即可刷新...'
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
type: String,
|
||||||
|
default: '加载中...'
|
||||||
|
},
|
||||||
|
animationDuration: {
|
||||||
|
type: Number,
|
||||||
|
default: 300
|
||||||
|
},
|
||||||
|
headHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Loading.name]: Loading
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: 'normal',
|
||||||
|
height: 0,
|
||||||
|
duration: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
transition: `${this.duration}ms`,
|
||||||
|
transform: `translate3d(0,${this.height}px, 0)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
if (!val) {
|
||||||
|
this.duration = this.animationDuration;
|
||||||
|
this.getStatus(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onTouchStart(event) {
|
||||||
|
if (this.status === 'loading') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.getCeiling()) {
|
||||||
|
this.duration = 0;
|
||||||
|
this.startX = event.touches[0].clientX;
|
||||||
|
this.startY = event.touches[0].clientY;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTouchMove(event) {
|
||||||
|
if (this.status === 'loading') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deltaY = event.touches[0].clientY - this.startY;
|
||||||
|
this.direction = this.getDirection(event.touches[0]);
|
||||||
|
|
||||||
|
if (!this.ceiling && this.getCeiling()) {
|
||||||
|
this.duration = 0;
|
||||||
|
this.startY = event.touches[0].clientY;
|
||||||
|
this.deltaY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ceiling && this.deltaY >= 0) {
|
||||||
|
if (this.direction === 'vertical') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
this.getStatus(this.ease(this.deltaY));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (this.status === 'loading') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ceiling && this.deltaY) {
|
||||||
|
this.duration = this.animationDuration;
|
||||||
|
if (this.status === 'loosing') {
|
||||||
|
this.getStatus(this.headHeight, true);
|
||||||
|
this.$emit('input', true);
|
||||||
|
} else {
|
||||||
|
this.getStatus(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCeiling() {
|
||||||
|
this.ceiling = (window.scrollY || window.pageYOffset) === 0;
|
||||||
|
return this.ceiling;
|
||||||
|
},
|
||||||
|
|
||||||
|
ease(height) {
|
||||||
|
const { headHeight } = this;
|
||||||
|
return height < headHeight
|
||||||
|
? height
|
||||||
|
: height < headHeight * 2
|
||||||
|
? Math.round(headHeight + (height - headHeight) / 2)
|
||||||
|
: Math.round(headHeight * 1.5 + (height - headHeight * 2) / 4);
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus(height, isLoading) {
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
const status = isLoading
|
||||||
|
? 'loading' : height === 0
|
||||||
|
? 'normal' : height < this.headHeight
|
||||||
|
? 'pulling' : 'loosing';
|
||||||
|
|
||||||
|
if (status !== this.status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getDirection(touch) {
|
||||||
|
const distanceX = Math.abs(touch.clientX - this.startX);
|
||||||
|
const distanceY = Math.abs(touch.clientY - this.startY);
|
||||||
|
return distanceX > distanceY ? 'horizontal' : distanceX < distanceY ? 'vertical' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -38,6 +38,7 @@
|
|||||||
@import './actionsheet.css';
|
@import './actionsheet.css';
|
||||||
@import './dialog.css';
|
@import './dialog.css';
|
||||||
@import './picker.css';
|
@import './picker.css';
|
||||||
|
@import './pull-refresh.css';
|
||||||
@import './toast.css';
|
@import './toast.css';
|
||||||
|
|
||||||
/* business components */
|
/* business components */
|
||||||
|
37
packages/vant-css/src/pull-refresh.css
Normal file
37
packages/vant-css/src/pull-refresh.css
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@import './common/var.css';
|
||||||
|
|
||||||
|
.van-pull-refresh {
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: -50px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $gray-dark;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading {
|
||||||
|
.van-loading {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span,
|
||||||
|
.van-loading {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
128
test/unit/specs/pull-refresh.spec.js
Normal file
128
test/unit/specs/pull-refresh.spec.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import PullRefresh from 'packages/pull-refresh';
|
||||||
|
import { mount } from 'avoriaz';
|
||||||
|
import { triggerTouch } from '../utils';
|
||||||
|
|
||||||
|
describe('PullRefresh', () => {
|
||||||
|
let wrapper;
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a PullRefresh', () => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('van-pull-refresh')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('change head content when pulling down', (done) => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 10);
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(wrapper.find('.van-pull-refresh__text')[0].text()).to.equal('下拉即可刷新...');
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 30);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 60);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 100);
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(wrapper.find('.van-pull-refresh__text')[0].text()).to.equal('释放即可刷新...');
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchend', 0, 100);
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(wrapper.find('.van-pull-refresh__loading span')[1].text()).to.equal('加载中...');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('change loading status when pulling down', (done) => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.$on('input', value => {
|
||||||
|
wrapper.vm.value = value;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.vm.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(wrapper.vm.status).to.equal('normal');
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 100);
|
||||||
|
triggerTouch(wrapper, 'touchend', 0, 100);
|
||||||
|
|
||||||
|
expect(wrapper.vm.value).to.be.true;
|
||||||
|
expect(wrapper.vm.status).to.equal('loading');
|
||||||
|
|
||||||
|
// ignore touch event when loading
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 100);
|
||||||
|
triggerTouch(wrapper, 'touchend', 0, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pull a short distance', () => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 10);
|
||||||
|
triggerTouch(wrapper, 'touchend', 0, 10);
|
||||||
|
|
||||||
|
expect(wrapper.vm.value).to.be.false;
|
||||||
|
expect(wrapper.vm.status).to.equal('normal');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not in page top', () => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.pageYOffset = 100;
|
||||||
|
|
||||||
|
// ignore touch event when not at page top
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 100);
|
||||||
|
triggerTouch(wrapper, 'touchend', 0, 100);
|
||||||
|
|
||||||
|
window.pageYOffset = 0;
|
||||||
|
triggerTouch(wrapper, 'touchmove', 0, 100);
|
||||||
|
expect(wrapper.vm.ceiling).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('horizontal direction', () => {
|
||||||
|
wrapper = mount(PullRefresh, {
|
||||||
|
propsData: {
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
triggerTouch(wrapper, 'touchstart', 0, 0);
|
||||||
|
triggerTouch(wrapper, 'touchmove', 10, 0);
|
||||||
|
triggerTouch(wrapper, 'touchend', 10, 0);
|
||||||
|
expect(wrapper.vm.direction).to.equal('horizontal');
|
||||||
|
});
|
||||||
|
});
|
@ -16,6 +16,6 @@ describe('Tag', () => {
|
|||||||
propsData: {
|
propsData: {
|
||||||
type: 'primary'
|
type: 'primary'
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user