chat-app/src/uni_modules/tmui/components/tm-scrolly/tm-scrolly.vue

462 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- #ifndef APP-NVUE -->
<scroll-view
class="scroyy"
:scroll-top="scrollTop"
v-passive-touch="onTouchStart"
v-passive-move="onTouchMove"
v-passive-end="onTouchEnd"
@scroll="onScroll"
@scrolltoupper="onScrollToTop"
@scrolltolower="onScrollToBottom"
scroll-y
:enable-flex="true"
enable-back-to-top
enhanced
scroll-with-animation
:bounces="false"
:style="[props.height ? { height: props.height + 'rpx' } : '', _style]"
:class="_class"
>
<view
:class="['scroyy__track', 'scroyy__track--' + (loosing ? 'loosing' : '')]"
:style="{ transform: `translate3d(0, ${_barHeight}rpx, 0)` }"
>
<view class="scroyy__tips" :class="['scroyy__track--' + (loosing ? 'loosing' : '')]" :style="{ height: _barHeight + 'rpx' }">
<slot name="pull" :status="{ refreshStatus }">
<view v-if="refreshStatus === 2" class="flex flex-row flex-row-center-center">
<tm-icon :font-size="24" color="primary" name="tmicon-shuaxin" spin></tm-icon>
<tm-text color="grey" _class="pl-16" :label="loadingTexts[refreshStatus]"></tm-text>
</view>
<view
v-if="refreshStatus != -1 && refreshStatus != 2"
class="flex flex-row flex-row-center-center srrryration"
:style="{
opacity: `${refreshStatus == 0 ? _barHeight / props.loadBarHeight : 1}`
}"
>
<view :class="refreshStatus == 0 ? 'srrryration srrryrationOn' : 'srrryration srrryrationOf'">
<tm-icon :font-size="24" color="primary" name="tmicon-long-arrow-down"></tm-icon>
</view>
<tm-text color="grey" _class="pl-16" :label="loadingTexts[refreshStatus]"></tm-text>
</view>
</slot>
</view>
<slot></slot>
<view :class="['scroyy__track--loosing ']" :style="{ height: (isBootRefresh ? props.loadBarHeight : 0) + 'rpx' }">
<slot name="bottom" :status="{ isBootRefresh }">
<view
v-if="isBootRefresh"
class="flex flex-row flex-row-center-center"
:style="{ height: (isBootRefresh ? props.loadBarHeight : 0) + 'rpx' }"
>
<tm-icon :font-size="24" color="primary" name="tmicon-shuaxin" spin></tm-icon>
<tm-text color="grey" _class="pl-16" label="数据加载中"></tm-text>
</view>
</slot>
</view>
</view>
</scroll-view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<scroll-view
enableBackToTop="true"
alwaysScrollableVertical="true"
scroll-y="true"
@scrolltolower="onScrollToBottom"
:style="[props.height ? { height: props.height + 'rpx' } : '', _style]" :class="_class">
<refresh @refresh="onrefresh" @pullingdown="onpullingdown" :display="showLoading ? 'show' : 'hide'" style="width: 750rpx">
<view :style="{ height: _barHeight / 2 + 'px' }">
<slot name="pull" :status="{ refreshStatus }">
<view
class="flex flex-row flex-row-center-center"
:style="{
height: _barHeight / 2 + 'px',
opacity: refreshStatus == 2 || refreshStatus == 3 ? 1 : 0
}"
>
<tm-icon :font-size="24" color="primary" name="tmicon-shuaxin" spin></tm-icon>
<tm-text color="grey" _class="pl-16" :label="loadingTexts[refreshStatus]"></tm-text>
</view>
<view
v-if="refreshStatus == 0 || refreshStatus == 1"
class="flex flex-row flex-row-center-center srrryration"
:style="{
opacity: `${refreshStatus == 0 ? _barHeight / nowEvt.viewHeight : 1}`,
height: _barHeight / 2 + 'px',
top: -(_barHeight / 2) + 'px'
}"
>
<view :class="refreshStatus == 0 ? 'srrryration srrryrationOn' : 'srrryration srrryrationOf'">
<tm-icon :font-size="24" color="primary" name="tmicon-long-arrow-down"></tm-icon>
</view>
<tm-text color="grey" _class="pl-16" :label="loadingTexts[refreshStatus]"></tm-text>
</view>
</slot>
</view>
</refresh>
<slot></slot>
</scroll-view>
<!-- #endif -->
</template>
<!-- #ifndef APP-NVUE -->
<script lang="ts" setup>
import tmIcon from '../tm-icon/tm-icon.vue'
import tmText from '../tm-text/tm-text.vue'
import { getCurrentInstance, nextTick, onMounted, ref, Ref, watch, computed } from 'vue'
import { propsdetail } from './propsdetail'
const proxy = getCurrentInstance()?.proxy ?? null
const emits = defineEmits(['bottom', 'change', 'refresh', 'timeout', 'update:modelValue', 'update:bottomValue'])
const props = defineProps({
...propsdetail
})
// 下拉开始的起点,主要用于计算下拉高度
const startPoint: Ref<{
pageX: number
pageY: number
} | null> = ref(null)
const isPulling = ref(false) // 是否下拉中
const _maxBarHeight = ref(props.maxBarHeight) // 最大下拉高度,单位 rpx
const _refresher = computed(()=>props.refresher)
// 触发刷新的下拉高度单位rpx
// 松开时下拉高度大于这个值即会触发刷新,触发刷新后松开,会恢复到这个高度并保持,直到刷新结束
const _barHeight = ref(0)
/** 开始刷新 - 刷新成功/失败 最大间隔时间setTimeout句柄 */
let maxRefreshAnimateTimeFlag: any | null = 0
/** 关闭动画耗时setTimeout句柄 */
let closingAnimateTimeFlag: any | null = 0
//加载框的高度
const refreshStatus = ref(-1)
const loosing = ref(false)
const enableToRefresh = ref(true)
const scrollTop = ref(0)
const loadingTexts = computed(() => props.loadingTexts)
/** 触底下拉刷新参数。 */
const isBootRefresh = ref(props.bottomValue)
const _class = computed(() => props._class)
const _style = computed(() => props._style)
watch(
() => props.modelValue,
() => {
if (!props.modelValue) {
if (maxRefreshAnimateTimeFlag != null) {
clearTimeout(maxRefreshAnimateTimeFlag)
}
refreshStatus.value = 3
close()
}
}
)
watch(
() => props.bottomValue,
() => {
isBootRefresh.value = props.bottomValue
}
)
onMounted(() => {
clearTimeout(maxRefreshAnimateTimeFlag)
clearTimeout(closingAnimateTimeFlag)
nextTick(() => setDefault())
})
function setDefault() {
if (props.defaultValue) {
setRefreshBarHeight(props.loadBarHeight)
refreshStatus.value = 2
loosing.value = true
isPulling.value = true
enableToRefresh.value = false
startPoint.value = null
}
}
function onScrollToBottom() {
if (isBootRefresh.value) return
emits('update:bottomValue')
emits('bottom')
}
function onScrollToTop() {
enableToRefresh.value = true
}
function onScroll(e: any) {
enableToRefresh.value = e.detail?.scrollTop === 0
}
function setScrollTop(tp: number) {
scrollTop.value = tp
}
function scrollToTop() {
setScrollTop(0)
}
function onTouchStart(e: TouchEvent) {
if (isPulling.value || !enableToRefresh.value || !_refresher.value) return
const { touches } = e
if (touches.length !== 1) return
const { pageX, pageY } = touches[0]
loosing.value = false
startPoint.value = {
pageX,
pageY
}
isPulling.value = true
}
function onTouchMove(e: TouchEvent) {
if (!startPoint.value || !_refresher.value) return
const { touches } = e
if (touches.length !== 1) return
const { pageY } = touches[0]
const offset = pageY - startPoint.value.pageY
const barsHeight = uni.$tm.u.torpx(offset)
if (barsHeight > 0) {
if (barsHeight > _maxBarHeight.value) {
// 限高
setRefreshBarHeight(_maxBarHeight.value)
// this.startPoint.pageY = pageY - this.toPx(this.maxBarHeight); // 限高的同时修正起点,避免触摸点上移时无效果
} else {
setRefreshBarHeight(barsHeight)
}
}
}
function onTouchEnd(e: TouchEvent) {
if (!startPoint.value || !_refresher.value) return
const { changedTouches } = e
if (changedTouches.length !== 1) return
const { pageY } = changedTouches[0]
const barsHeight = uni.$tm.u.torpx(pageY - startPoint.value.pageY)
startPoint.value = null // 清掉起点之后将忽略touchMove、touchEnd事件
loosing.value = true
isBootRefresh.value = false
// 松开时高度超过阈值则触发刷新
if (barsHeight > props.loadBarHeight) {
_barHeight.value = props.loadBarHeight
refreshStatus.value = 2
emits('change', true)
emits('update:modelValue', true)
emits('refresh')
maxRefreshAnimateTimeFlag = setTimeout(() => {
maxRefreshAnimateTimeFlag = null
if (refreshStatus.value === 2) {
// 超时回调
emits('timeout')
close() // 超时仍未被回调,则直接结束下拉
}
}, props.refreshTimeout as any) as any as number
} else {
close()
}
}
function setRefreshBarHeight(barsHeight: number) {
if(!_refresher.value) return;
if (barsHeight >= props.loadBarHeight) {
refreshStatus.value = 1
} else {
refreshStatus.value = 0
}
return new Promise((resolve) => {
_barHeight.value = barsHeight
nextTick(() => {
resolve(barsHeight)
})
})
}
function close() {
const animationDuration = 350
_barHeight.value = 0
emits('change', false)
emits('update:modelValue', false)
closingAnimateTimeFlag = setTimeout(() => {
closingAnimateTimeFlag = null
refreshStatus.value = -1
isPulling.value = false // 退出下拉状态
loosing.value = false
enableToRefresh.value = true
}, animationDuration) as any as number
}
</script>
<style scoped>
.scroyy {
overflow: hidden;
/* #ifndef APP-NVUE */
max-height: 100%;
/* #endif */
}
.scroyy__track {
position: relative;
}
.scroyy__track--loosing {
transition-property: transform, height, opacity;
transition-timing-function: ease;
transition-duration: 0.35s;
}
.scroyy__tips {
position: absolute;
color: #bbb;
font-size: 24rpx;
top: 0;
width: 100%;
transform: translateY(-100%);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
overflow: hidden;
}
.scroyy__text {
margin: 16rpx 0 0;
}
.scroyy__wrap {
position: relative;
}
</style>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<script lang="ts" setup>
import tmIcon from '../tm-icon/tm-icon.vue'
import tmText from '../tm-text/tm-text.vue'
import { getCurrentInstance, nextTick, onMounted, ref, Ref, watch } from 'vue'
import { propsdetail } from './propsdetail'
const proxy = getCurrentInstance()?.proxy ?? null
const emits = defineEmits(['bottom', 'change', 'refresh', 'timeout', 'update:modelValue', 'update:bottomValue'])
const props = defineProps({
...propsdetail
})
interface pullevent {
dy: number
pullingDistance: number
viewHeight: number
type: string
}
const showLoading = ref(false)
const refreshing = ref(false)
const isBootRefresh = ref(false)
const refreshStatus = ref(-1)
/** 开始刷新 - 刷新成功/失败 最大间隔时间setTimeout句柄 */
let maxRefreshAnimateTimeFlag: number | null = 0
const _barHeight = ref(uni.$tm.u.topx(props.loadBarHeight))
const nowEvt: Ref<pullevent> = ref({
dy: 0,
pullingDistance: 0,
viewHeight: 0,
type: ''
})
onMounted(() => {
if (maxRefreshAnimateTimeFlag != null) {
clearTimeout(maxRefreshAnimateTimeFlag)
}
})
watch(
() => props.modelValue,
() => {
if (!props.modelValue) {
if (maxRefreshAnimateTimeFlag != null) {
clearTimeout(maxRefreshAnimateTimeFlag)
}
close()
}
}
)
watch(
() => props.bottomValue,
() => {
isBootRefresh.value = props.bottomValue
}
)
function onScrollToBottom() {
if (isBootRefresh.value) return
emits('update:bottomValue', true)
isBootRefresh.value = true
emits('bottom')
}
function onrefresh() {
if (nowEvt.value.pullingDistance >= _barHeight.value) {
refreshStatus.value = 2
showLoading.value = true
emits('refresh')
emits('change', true)
emits('update:modelValue', true)
maxRefreshAnimateTimeFlag = setTimeout(() => {
maxRefreshAnimateTimeFlag = null
if (refreshStatus.value === 2) {
// 超时回调
emits('timeout')
close() // 超时仍未被回调,则直接结束下拉
}
}, props.refreshTimeout as any) as any as number
return
}
showLoading.value = true
nextTick(() => {
showLoading.value = false
emits('change', false)
})
}
/**
* dy: 前后两次回调滑动距离的差值
pullingDistance: 下拉的距离
viewHeight: refresh 组件高度
type: “pullingdown” 常数字符串
*/
function onpullingdown(evt: { dy: number; pullingDistance: number; viewHeight: number; type: string }) {
evt.pullingDistance = Math.abs(evt.pullingDistance)
nowEvt.value = evt
if (evt.pullingDistance <= 1) {
refreshStatus.value = -1
} else if (evt.pullingDistance > 1 && evt.pullingDistance < _barHeight.value) {
refreshStatus.value = 0
} else if (evt.pullingDistance >= _barHeight.value) {
refreshStatus.value = 1
}
}
function close() {
showLoading.value = false
refreshStatus.value = -1
emits('change', false)
emits('update:modelValue', false)
}
</script>
<!-- #endif -->
<style>
.srrryration {
transition-property: transform, height, opacity;
transition-timing-function: ease;
transition-duration: 0.25s;
}
.srrryrationOn {
transform: rotate(0deg);
}
.srrryrationOf {
transform: rotate(180deg);
}
</style>