douyin/src/components/slide/SlideVerticalInfinite2.vue
2024-04-24 18:58:02 +08:00

366 lines
11 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.

<script setup lang="tsx">
import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'
import {
getSlideOffset,
slideInit,
slideReset,
slideTouchEnd,
slideTouchMove,
slideTouchStart
} from '@/utils/slide'
import { SlideType } from '@/utils/const_var'
import SlideItem from '@/components/slide/SlideItem.vue'
import bus, { EVENT_KEY } from '../../utils/bus'
import Loading from '@/components/Loading.vue'
import { useBaseStore } from '@/store/pinia'
import { _css } from '@/utils/dom'
const props = defineProps({
index: {
type: Number,
default: () => {
return -1
}
},
render: {
type: Function,
default: () => {
return null
}
},
list: {
type: Array,
default: () => {
return []
}
},
//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},
name: {
type: String,
default: () => ''
},
uniqueId: {
type: String,
default: () => ''
},
loading: {
type: Boolean,
default: () => false
},
active: {
type: Boolean,
default: () => false
}
})
const emit = defineEmits(['update:index', 'loadMore', 'refresh'])
const appInsMap = new Map()
const itemClassName = 'slide-item'
const slideListEl = ref<HTMLDivElement>(null)
const state = reactive({
judgeValue: 20,
type: SlideType.VERTICAL_INFINITE,
name: props.name,
localIndex: props.index,
needCheck: true,
next: false,
isDown: false,
start: { x: 0, y: 0, time: 0 },
move: { x: 0, y: 0 },
wrapper: { width: 0, height: 0, childrenLength: 0 }
})
const baseStore = useBaseStore()
watch(
() => props.list,
(newVal, oldVal) => {
// console.log('watch-list', newVal.length, oldVal.length, newVal)
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻html里面有原本的5个加新增的3个一共8个dom
// 用户往下滑动时只删除前面多余的dom等滑动到临界值virtualTotal/2+1再去执行新增逻辑
// let lastSlideItem = slideListEl.value.querySelector(`.${itemClassName}:last-child`)
// let top = _css(lastSlideItem, 'top')
// let newListStartIndex = Number(lastSlideItem.getAttribute('data-index')) + 1
// // console.log('newListStartIndex', newListStartIndex)
// newVal.slice(newListStartIndex, newListStartIndex + 3).map((item, index) => {
// let el = getInsEl(item, newListStartIndex + index)
// //这里必须要设置个top值不然会把前面的条目给覆盖掉
// //2022-3-27这里不用计算直接用已用slide-item最后一条的top值
// //因为有一条情况当滑动最后一条和二条的时候top值不会继续加。此时新增的数据如果还
// // 计算top值的会和前面的对不上
// _css(el, 'top', top)
// slideListEl.value.appendChild(el)
// state.wrapper.childrenLength++
// })
}
}
}
)
watch(
() => props.index,
(newVal, oldVal) => {
state.localIndex = newVal
// console.log('watch-index', newVal, oldVal)
if (!props.list.length) return
bus.emit(EVENT_KEY.CURRENT_ITEM, props.list[newVal])
bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
uniqueId: props.uniqueId,
index: newVal,
type: EVENT_KEY.ITEM_PLAY
})
setTimeout(() => {
bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
uniqueId: props.uniqueId,
index: oldVal,
type: EVENT_KEY.ITEM_STOP
})
}, 200)
}
)
watch(
() => props.active,
(newVal) => {
//当激活此页时如果list为空那么向上发射事件通知父组件请求数据
if (newVal && !props.list.length) {
return emit('refresh')
}
// console.log('active', 'newVal', newVal, 'oldVal', oldVal)
if (newVal) {
bus.emit(EVENT_KEY.CURRENT_ITEM, props.list[state.localIndex])
}
bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
uniqueId: props.uniqueId,
index: state.localIndex,
type: newVal === false ? EVENT_KEY.ITEM_STOP : EVENT_KEY.ITEM_PLAY
})
},
{ immediate: true }
)
onMounted(() => {
slideInit(slideListEl.value, state)
})
/**
* 插入SlideItem
*/
function insertContent() {
if (!props.list.length) return
//清空SlideList
slideListEl.value.innerHTML = ''
let half = (props.virtualTotal - 1) / 2
//因为我们只渲染 props.virtualTotal 条数据到dom中并且当前index有可能不是0所以需要计算出起始下标和结束下标
let start = 0
if (state.localIndex > half) {
start = state.localIndex - half
}
let end = start + props.virtualTotal
if (end >= props.list.length) {
end = props.list.length
start = end - props.virtualTotal
}
if (start < 0) start = 0
// console.log('start', start, end)
//插入start到end范围内的数据到dom中
props.list.slice(start, end).map((item, index) => {
let el = getInsEl(item, start + index, start + index === state.localIndex)
slideListEl.value.appendChild(el)
})
//设置SlideList的偏移量
_css(
slideListEl.value,
'transform',
`translate3d(0px,${getSlideOffset(state, slideListEl.value)}px, 0px)`
)
//因为index有可能不是0所以要设置Item的top偏移量
if (state.localIndex > 2 && props.list.length > 5) {
let list = slideListEl.value.querySelectorAll(`.${itemClassName}`)
list.forEach((item) => {
if (list.length - state.localIndex > 2) {
_css(item, 'top', (state.localIndex - 2) * state.wrapper.height)
} else {
_css(item, 'top', start * state.wrapper.height)
}
})
}
state.wrapper.childrenLength = slideListEl.value.children.length
// console.log('list[state.localIndex]',list[state.localIndex])
bus.emit(EVENT_KEY.CURRENT_ITEM, props.list[state.localIndex])
}
function dislike() {
// let currentItem = $(slideListEl.value).find(`.${itemClassName}[data-index=${state.localIndex}]`)
// let replaceItem = getInsEl(item, state.localIndex, true)
// $(replaceItem).css('top', currentItem.css('top'))
// currentItem.replaceWith(replaceItem)
}
defineExpose({ dislike })
/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/
function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}
function touchStart(e) {
slideTouchStart(e, slideListEl.value, state)
}
function touchMove(e) {
slideTouchMove(e, slideListEl.value, state, canNext)
}
function touchEnd(e) {
let isNext = state.move.y < 0
if (
state.localIndex === 0 &&
!isNext &&
state.move.y > baseStore.homeRefresh + baseStore.judgeValue
) {
emit('refresh')
}
slideTouchEnd(e, state, canNext, (isNext) => {
let half = (props.virtualTotal - 1) / 2
if (props.list.length > props.virtualTotal) {
//往下滑
if (isNext) {
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}
let addItemIndex = state.localIndex + 2
console.log('addItemIndex', addItemIndex)
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (state.wrapper.childrenLength < props.virtualTotal) {
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}
}
if (
state.wrapper.childrenLength === props.virtualTotal &&
state.localIndex > half &&
state.localIndex <= props.list.length - 3
) {
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - 2) * state.wrapper.height)
})
}
}
if (state.wrapper.childrenLength > props.virtualTotal) {
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
let index = Number(item.getAttribute('data-index'))
if (index < state.localIndex - 2) {
appInsMap.get(index).unmount()
}
_css(item, 'top', (state.localIndex - 2) * state.wrapper.height)
})
}
} else {
let addItemIndex = state.localIndex - 2
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (state.localIndex > 1 && state.localIndex <= props.list.length - 4) {
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addItemIndex], addItemIndex))
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
// $(slideListEl.value).find(".base-slide-item:last").remove()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - 2) * state.wrapper.height)
})
}
}
if (state.wrapper.childrenLength > props.virtualTotal) {
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
}
}
state.wrapper.childrenLength = slideListEl.value.children.length
}
})
slideReset(e, slideListEl.value, state, emit)
}
function canNext(state, isNext) {
return !(
(state.localIndex === 0 && !isNext) ||
(state.localIndex === props.list.length - 1 && isNext)
)
}
</script>
<template>
<div class="slide slide-infinite">
<Loading v-if="props.loading && props.list.length === 0" />
<div
class="slide-list flex-direction-column"
ref="slideListEl"
@click="null"
@pointerdown="touchStart"
@pointermove="touchMove"
@pointerup="touchEnd"
>
<slot></slot>
</div>
</div>
</template>