fix(pull-refresh): 修复下拉刷新组件频繁触发的问题 (#145)

This commit is contained in:
TC-twwang 2023-04-18 20:09:35 +08:00 committed by GitHub
parent 63f8c16dc5
commit 2d6e893392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 176 additions and 62 deletions

View File

@ -245,5 +245,4 @@ export default {
.page__content {
width: 375px;
height: 48px;
background-color: #fff;
}</style>

View File

@ -1,6 +1,6 @@
<template>
<div>
<tiny-tabs style="height: 150px" v-model="activeName1" tab-style="card" :with-close="true" :with-add="true" @add="add" @close="close">
<tiny-tabs style="height: 150px" v-model="activeName1" tab-style="card" :with-close="true" :with-add="true" @add="add" @close="close" @click="tabsEvent">
<tiny-tab-item :key="item.name" v-for="item in Tabs" :title="item.title" :name="item.name">
{{ item.content }}
</tiny-tab-item>
@ -76,6 +76,9 @@ export default {
}
})
this.Tabs.splice(index, 1)
},
tabsEvent(tabs) {
console.log(tabs)
}
}
}

View File

@ -193,11 +193,10 @@ export const getLabelsStyle = (state) => {
// 不超过总宽度25%的头部下拉项
let widthInfo = over25Labels
const len = state.dataSource.length
if (len >= 4) {
return getStyleConfig(state.labelLevelsInfo, { width: `${((1 / len) * 100).toFixed(2)}%` })
}
if (!widthInfo.length || widthInfo.length === state.labelLevelsInfo.length) {
// 所有下拉项同时不超过或者超过25%宽度
if (!widthInfo.length || widthInfo.length === state.labelLevelsInfo.length || len >= 4) {
// 所有下拉项同时不超过或者超过25%宽度或者列数大于等于4
return getStyleConfig(state.labelLevelsInfo, { maxWidth: `${((1 / len) * 100).toFixed(2)}%` })
}
let fillArr

View File

@ -31,7 +31,7 @@ export const api = [
'loadDefault'
]
const initState = (reactive) => {
const initState = ({ computed, reactive }) => {
const state = reactive({
dataSource: [],
wheelData: [],
@ -44,6 +44,7 @@ const initState = (reactive) => {
headerInfo: [],
defaultSelectedIndexs: [],
defaultSelectedArray: [],
isActive: computed(() => state.headerInfo.some(item => item.isUP))
})
return state
@ -77,9 +78,9 @@ const initWatch = ({ watch, props, state, refs, nextTick }) => {
)
}
export const renderless = (props, { onMounted, reactive, watch }, { emit, nextTick, refs }) => {
export const renderless = (props, { computed, onMounted, reactive, watch }, { emit, nextTick, refs }) => {
const api = {}
const state = initState(reactive)
const state = initState({ computed, reactive })
initApi({ api, props, state, emit, nextTick, refs })

View File

@ -28,19 +28,21 @@ export const initPullRefresh = ({ t, props, state }) => () => {
paddingTop: state.pullDown.headHeight + 'px',
paddingBottom: state.pullUp.footHeight + 'px'
}
state.loosingText = props.loosingText ?? t('ui.pullRefresh.loosing')
state.successText = props.successText ?? t('ui.pullRefresh.success')
state.failedText = props.failedText ?? t('ui.pullRefresh.failed')
}
export const onTouchstart = (state) => (event) => {
state.draggposition = event.touches[0].clientY
}
export const onTouchmove = ({ props, state }) => (event) => {
export const onTouchmove = ({ props, state, refs }) => (event) => {
if (event.touches[0].clientY < state.draggposition) {
// 在滚动条在底部10px内时才触发上拉刷新防止频繁触发
const scrollBottom = refs.content.scrollHeight - refs.content.clientHeight - refs.content.scrollTop
if (scrollBottom > 10) {
return
}
// 上拉刷新
if (!state.pullUp.pullUpDisabled) {
if (!state.pullUp.pullUpDisabled && state.pullUp.handler) {
// 没有更多了
if (props.hasMore) {
state.translate3d = (event.touches[0].clientY - state.draggposition) / 2
@ -49,8 +51,12 @@ export const onTouchmove = ({ props, state }) => (event) => {
}
}
} else {
// 在滚动条在顶部10px内时才触发下拉刷新防止频繁触发
if (refs.content.scrollTop > 10) {
return
}
// 下拉刷新
if (!state.pullDown.pullDownDisabled) {
if (!state.pullDown.pullDownDisabled && state.pullDown.handler) {
state.translate3d = (event.touches[0].clientY - state.draggposition) / 2
state.pullDownReplaces = Math.abs(state.translate3d) > state.pullDown.headHeight ? state.loosingText : state.pullDown.pullingDownText
state.pullUpReplaces = ''

View File

@ -14,7 +14,7 @@ import { mountedHandler, beforeUnmountHandler, handlerModelValue, onTouchstart,
export const api = ['state']
export const renderless = (props, { watch, onMounted, reactive, onBeforeUnmount }, { t, refs }) => {
export const renderless = (props, { watch, onMounted, computed, reactive, onBeforeUnmount }, { t, refs }) => {
const api = {}
const state = reactive({
pullUpReplaces: '',
@ -24,10 +24,10 @@ export const renderless = (props, { watch, onMounted, reactive, onBeforeUnmount
draggposition: 0,
pullUpLoading: false,
pullDownLoading: false,
loosingText: '',
successText: '',
failedText: '',
noMoreText: '',
loosingText: computed(() => props.loosingText || t('ui.pullRefresh.loosing')),
successText: computed(() => props.successText || t('ui.pullRefresh.success')),
failedText: computed(() => props.failedText || t('ui.pullRefresh.failed')),
noMoreText: computed(() => !props.hasMore ? t('ui.pullRefresh.noMore') : ''),
pullUp: null,
pullDown: null,
successDuration: props.successDuration,
@ -37,7 +37,7 @@ export const renderless = (props, { watch, onMounted, reactive, onBeforeUnmount
Object.assign(api, {
state,
onTouchstart: onTouchstart(state),
onTouchmove: onTouchmove({ props, state }),
onTouchmove: onTouchmove({ props, state, refs }),
onTouchend: onTouchend({ api, props, state }),
mountedHandler: mountedHandler({ api, refs }),
beforeUnmountHandler: beforeUnmountHandler({ api, refs }),
@ -50,8 +50,8 @@ export const renderless = (props, { watch, onMounted, reactive, onBeforeUnmount
() => props.hasMore,
(value) => {
if (!value) {
state.pullUpLoading = false
// 没有更多了
state.noMoreText = t('ui.pullRefresh.noMore')
api.clearPullRefresh()
}
},

View File

@ -92,6 +92,7 @@ export const handleTabClick = ({ api, emit, props, refs }) => (pane, tabName, ev
return
}
event.stopPropagation()
api.setCurrentName(tabName)
emit('click', pane, event)

View File

@ -4,7 +4,6 @@ export default {
'tiny-form-item-mini-line-height': '24px',
'tiny-form-item-label-line-height': '30px',
'tiny-form-item-label-font-size': '14px',
'tiny-form-item-label-color': '#333',
'tiny-form-item-error-color': '#f5222d',
'tiny-form-item-error-font-size': '12px'
}

View File

@ -20,9 +20,10 @@
@textarea-prefix-cls: ~'@{css-prefix}mobile-textarea';
@button-prefix-cls: ~'@{css-prefix}mobile-button';
@numeric-prefix-cls: ~'@{css-prefix}mobile-numeric';
@input-form-prefix-cls: ~'@{css-prefix}mobile-input-form';
.@{form-item-prefix-cls} {
margin-bottom: 12px;
margin-bottom: 0px;
.clearfix();
& & {
@ -70,21 +71,65 @@
text-align: right;
vertical-align: middle;
float: left;
font-size: 16px;
color: var(--ti-form-item-label-color, #333);
font-size: 14px;
color: var(--ti-mobile-base-color-common-2, #191919);
line-height: 48px;
padding-right: 8px;
box-sizing: border-box;
}
&__label.is-readonly{
color: var(--ti-mobile-base-color-common-6, #999999);
padding: 4px 0px;
line-height: 14px;
min-height: 14px
}
&__value {
float: right;
font-size: 14px;
color: var(--ti-mobile-base-color-common-5, #595959);
.tiny-mobile-input-form__input{
text-align: left;
font-size: 14px;
color: var(--ti-mobile-base-color-common-5, #595959);
}
.tiny-mobile-textarea__inner{
line-height: 22px;
height: auto;
font-size: 14px;
}
}
&__value.is-readonly{
color: var(--ti-mobile-base-color-common-2, #191919);
.tiny-mobile-input-form__input{
height: 22px;
line-height: 22px;
}
.tiny-mobile-textarea{
padding: 0px 16px;
}
input,textarea {
&.readonly,
&[readonly] {
color: var(--ti-mobile-base-color-common-2, #191919);
}
}
}
.is-readonly {
.@{input-form-prefix-cls} {
height: 22px;
}
}
&__content {
line-height: 48px;
position: relative;
@ -178,6 +223,14 @@
}
}
.@{form-item-prefix-cls}:last-child{
margin-bottom: 0px;
}
.@{form-item-prefix-cls}.is-readonly{
margin-bottom: 12px;
}
.@{css-prefix} {
&mobile-zoom-in-top-enter-active,
&mobile-zoom-in-top-leave-active {

View File

@ -4,7 +4,6 @@
--ti-form-item-mini-line-height: 24px;
--ti-form-item-label-line-height: var(--ti-mobile-base-size-height-minor, 30px);
--ti-form-item-label-font-size: 14px;
--ti-form-item-label-color: var(--ti-mobile-base-color-info-normal, #333);
--ti-form-item-error-color: var(--ti-mobile-base-color-danger-normal, #f5222d);
--ti-form-item-error-font-size: var(--ti-mobile-base-font-size, 12px);
}

View File

@ -20,6 +20,8 @@
.@{form-prefix-cls} {
width: 100%;
overflow: hidden;
padding: 12px;
background: #fff;
div {
outline: none;
@ -27,14 +29,13 @@
.@{form-item-prefix-cls} {
position: relative;
margin-bottom: 0;
}
.@{form-item-prefix-cls}__label {
width: 35%;
min-height: 48px;
line-height: 1.4;
font-size: 16px;
font-size: 14px;
display: flex;
z-index: 1;
padding: 13px 0px;
@ -80,6 +81,14 @@
line-height: 1;
}
.@{form-item-prefix-cls}__label.is-readonly{
margin-bottom: 6px;
}
.@{form-item-prefix-cls}__value .tiny-mobile-textarea{
padding: 0px 16px;
}
.@{form-item-prefix-cls}__value {
width: 100%;
}

View File

@ -1,8 +1,8 @@
export default {
'tiny-mobile-multi-select-bg': '#fff',
'tiny-mobile-multi-select-header-bg': '#fff',
'tiny-mobile-multi-select-bg-active': '#fff',
'tiny-mobile-multi-select-header-item-height': '28px',
'tiny-mobile-multi-select-header-item-bg': 'rgba(0, 0, 0, 0.03)',
'tiny-mobile-multi-select-header-item-bg': '#ededed',
'tiny-mobile-multi-select-header-item-bg-selected': '#f1f5ff',
'tiny-mobile-multi-select-header-item-margin-top': '10px',
'tiny-mobile-multi-select-header-label-text-color': '#595959',
'tiny-mobile-multi-select-header-label-font-size': '12px',

View File

@ -17,9 +17,12 @@
@multi-select-prefix-cls: ~'@{css-prefix}mobile-multi-select';
.@{multi-select-prefix-cls} {
background: var(--ti-mobile-multi-select-bg, #fff);
width: 100%;
border-radius: 0px 0px 16px 16px;
position: relative;
&__active {
background: var(--ti-mobile-multi-select-bg-active, #fff);
}
&__header {
display: flex;
@ -34,7 +37,6 @@
flex: 1;
flex-direction: row;
justify-content: space-between;
background: var(--ti-mobile-multi-select-header-bg, #fff);
box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.06) inset;
}
@ -55,7 +57,7 @@
align-items: center;
justify-content: center;
height: var(--ti-mobile-multi-select-header-item-height, 28px);
background: var(--ti-mobile-multi-select-header-item-bg, rgba(0, 0, 0, 0.03));
background: var(--ti-mobile-multi-select-header-item-bg, #ededed);
border-radius: 4px;
margin-top: var(--ti-mobile-multi-select-header-item-margin-top, 10px);
padding: 5px 8px;
@ -98,17 +100,28 @@
}
}
&__active {
&__selected{
background: var(--ti-mobile-multi-select-header-item-bg-selected, #f1f5ff);
}
&__selected &__label{
color: var(--ti-mobile-base-color-brand-1, #4a79fe);
}
&__selected &__icon{
svg {
fill: var(--ti-mobile-base-color-brand-1, #4a79fe);
}
color: var(--ti-mobile-base-color-brand-1, #4a79fe);
}
}
&__content {
width: 100%;
height: var(--ti-mobile-multi-select-content-height, 226px);
position: absolute;
background: #fff;
z-index: 500;
}
.noFooter {
@ -125,6 +138,10 @@
align-items: center;
border-radius: 0px 0px 16px 16px;
box-shadow: 0px -1px 3px 0px #f8f8f8;
background: #fff;
z-index: 500;
position: absolute;
margin-top: 226px;
button {
width: var(--ti-mobile-multi-select-footer-width, 164px);

View File

@ -1,8 +1,8 @@
:root {
--ti-mobile-multi-select-bg: #fff;
--ti-mobile-multi-select-header-bg: #fff;
--ti-mobile-multi-select-bg-active: #fff;
--ti-mobile-multi-select-header-item-height: 28px;
--ti-mobile-multi-select-header-item-bg: rgba(0, 0, 0, 0.03);
--ti-mobile-multi-select-header-item-bg: #ededed;
--ti-mobile-multi-select-header-item-bg-selected: #f1f5ff;
--ti-mobile-multi-select-header-item-margin-top: 10px;
--ti-mobile-multi-select-header-label-text-color: #595959;
--ti-mobile-multi-select-header-label-font-size: 12px;

View File

@ -31,7 +31,6 @@
font-size: var(--ti-mobile-tabs-font-size, 14px);
color: var(--ti-mobile-tabs-text-color, #333);
background: var(--ti-mobile-tabs-bg-color, #fff);
padding: 15px 24px;
}
&__new-tab {

View File

@ -91,6 +91,13 @@ export default defineComponent({
const formItemClass = `${classPrefix}form-item--${state.sizeClass ? state.sizeClass : ''}`
const isShowError = state.validateState === 'error' && showMessage && state.form.showMessage
const isErrorInline = typeof inlineMessage === 'boolean' ? inlineMessage : (state.formInstance && state.formInstance.inlineMessage) || false
// label
const isReadonly = defaultSlots.some((vnode) => {
const props = parseVnode(vnode)?.props
return props && (props['readonly'] === '' || props['readonly'])
})
let validateMessage = null
const FormContent = defaultSlots
@ -161,7 +168,10 @@ export default defineComponent({
return h(
'div',
{
class: `${classPrefix}form-item__value`,
class: {
[`${classPrefix}form-item__value`]: true,
'is-readonly': isMobile && props && (props['readonly'] === '' || props['readonly'])
},
style: state.valueStyle
},
[item]
@ -227,7 +237,8 @@ export default defineComponent({
{
class: {
[`${classPrefix}form-item__label`]: true,
'is-ellipsis': isMobile && ellipsis
'is-ellipsis': isMobile && ellipsis,
'is-readonly': isMobile && isReadonly
},
style: state.labelStyle,
attrs: {
@ -250,7 +261,8 @@ export default defineComponent({
'is-success': state.validateState === 'success',
'is-required': state.isRequired || required,
'is-no-asterisk': state.formInstance && state.formInstance.hideRequiredAsterisk,
[formItemClass]: true
[formItemClass]: true,
'is-readonly': isMobile && isReadonly
}
},
[

View File

@ -10,18 +10,23 @@
*
-->
<template>
<div class="tiny-mobile-multi-select">
<div :class="['tiny-mobile-multi-select', state.isActive ? 'tiny-mobile-multi-select__active' : '']">
<div class="tiny-mobile-multi-select__header">
<div class="tiny-mobile-multi-select__header__flexCenter" ref="headerBox">
<!-- 用来计算头部每项宽度 -->
<div class="tiny-mobile-multi-select__header__calc">
<div v-for="(item, index) of dataSource" :key="index" class="tiny-mobile-multi-select__header__item" ref="label">
<div :class="['tiny-mobile-multi-select__header__label', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__active' : '']">
<div
v-for="(item, index) of dataSource"
:key="index"
:class="['tiny-mobile-multi-select__header__item', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__selected' : '']"
ref="label"
>
<div class="tiny-mobile-multi-select__header__label">
<span v-if="!state.headerInfo[index]?.isSelected">{{ item.title }}</span>
<span v-else>{{ state.headerInfo[index]?.title }}</span>
</div>
<div
:class="['tiny-mobile-multi-select__header__icon', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__active' : '']"
class="tiny-mobile-multi-select__header__icon"
:style="{
transform: state.headerInfo[index]?.isUP ? 'rotate(180deg)' : 'none'
}"
@ -33,16 +38,16 @@
<div
v-for="(item, index) of dataSource"
:key="index"
class="tiny-mobile-multi-select__header__item"
:class="['tiny-mobile-multi-select__header__item', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__selected' : '']"
@click="handleClick(index)"
:style="state.labelsStyle[index]"
>
<div :class="['tiny-mobile-multi-select__header__label', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__active' : '']">
<div class="tiny-mobile-multi-select__header__label">
<span v-if="!state.headerInfo[index]?.isSelected">{{ item.title }}</span>
<span v-else>{{ state.headerInfo[index]?.title }}</span>
</div>
<div
:class="['tiny-mobile-multi-select__header__icon', state.headerInfo[index]?.isSelected ? 'tiny-mobile-multi-select__header__active' : '']"
class="tiny-mobile-multi-select__header__icon"
:style="{
transform: state.headerInfo[index]?.isUP ? 'rotate(180deg)' : 'none'
}"

View File

@ -11,11 +11,19 @@
-->
<template>
<div class="tiny-mobile-pull-refresh" :style="state.refreshStyle">
<div class="tiny-mobile-pull-refresh__track" ref="track" :style="{
'transition-duration': state.animationDuration + 'ms',
transform: 'translate3d(0px,' + state.translate3d + 'px,0px)'
}">
<div class="tiny-mobile-pull-refresh__tips tiny-mobile-pull-refresh__head" :style="{ height: state.pullDown.headHeight + 'px' }" v-if="state.pullDownLoading || state.pullDownReplaces">
<div
class="tiny-mobile-pull-refresh__track"
ref="track"
:style="{
'transition-duration': state.animationDuration + 'ms',
transform: 'translate3d(0px,' + state.translate3d + 'px,0px)'
}"
>
<div
class="tiny-mobile-pull-refresh__tips tiny-mobile-pull-refresh__head"
:style="{ height: state.pullDown.headHeight + 'px' }"
v-if="state.pullDownLoading || state.pullDownReplaces"
>
<span v-if="!state.pullDownLoading">{{ state.pullDownReplaces }}</span>
<slot name="loading" v-if="state.pullDownLoading">
<ul v-if="state.pullDownLoading" class="tiny-mobile-pull-refresh__loading">
@ -25,10 +33,14 @@
</ul>
</slot>
</div>
<div class="tiny-mobile-pull-refresh__content">
<div class="tiny-mobile-pull-refresh__content" ref="content">
<slot></slot>
</div>
<div class="tiny-mobile-pull-refresh__tips tiny-mobile-pull-refresh__foot" :style="{ height: state.pullUp.footHeight + 'px' }" v-if="state.pullUpLoading || state.pullUpReplaces">
<div
class="tiny-mobile-pull-refresh__tips tiny-mobile-pull-refresh__foot"
:style="{ height: state.pullUp.footHeight + 'px' }"
v-if="state.pullUpLoading || state.pullUpReplaces"
>
<span v-if="!state.pullUpLoading">{{ state.pullUpReplaces }}</span>
<slot name="loading" v-if="state.pullUpLoading">
<ul v-if="state.pullUpLoading" class="tiny-mobile-pull-refresh__loading">
@ -75,7 +87,7 @@ export default defineComponent({
hasMore: {
type: Boolean,
default: true
},
}
},
setup(props, context) {
return setup({ props, context, renderless, api, mono: true })