378 lines
8.3 KiB
Vue
378 lines
8.3 KiB
Vue
<script setup lang="ts">
|
|
import type { ComponentInternalInstance, VNode } from 'vue'
|
|
import { computed, defineComponent, getCurrentInstance, nextTick, onBeforeUnmount, onDeactivated, reactive, watch } from 'vue'
|
|
import { PREFIX } from '../_constants'
|
|
import { useProvide, useRect, useTouch } from '../_hooks'
|
|
import { clamp, getRandomId } from '../_utils'
|
|
import requestAniFrame from '../_utils/raf'
|
|
import { SWIPER_KEY, swiperEmits, swiperProps } from './swiper'
|
|
|
|
const props = defineProps(swiperProps)
|
|
const emit = defineEmits(swiperEmits)
|
|
const instance = getCurrentInstance() as ComponentInternalInstance
|
|
|
|
const containerId = `container-${getRandomId()}`
|
|
const state = reactive({
|
|
active: 0,
|
|
num: 0,
|
|
rect: null as any,
|
|
width: 0,
|
|
height: 0,
|
|
moving: false,
|
|
offset: 0,
|
|
touchTime: 0,
|
|
autoplayTimer: null as any | undefined | null,
|
|
childrenVNode: [] as VNode[],
|
|
style: {},
|
|
})
|
|
|
|
const touch = useTouch()
|
|
|
|
const classes = computed(() => {
|
|
const prefixCls = componentName
|
|
return {
|
|
[prefixCls]: true,
|
|
}
|
|
})
|
|
|
|
const isVertical = computed(() => props.direction === 'vertical')
|
|
|
|
const classesInner = computed(() => {
|
|
const prefixCls = componentName
|
|
return {
|
|
[`${prefixCls}-inner`]: true,
|
|
[`${prefixCls}-vertical`]: isVertical.value,
|
|
}
|
|
})
|
|
|
|
const classesPagination = computed(() => {
|
|
const prefixCls = componentName
|
|
return {
|
|
[`${prefixCls}-pagination`]: true,
|
|
[`${prefixCls}-pagination-vertical`]: isVertical.value,
|
|
}
|
|
})
|
|
|
|
const delTa = computed(() => {
|
|
return isVertical.value ? touch.deltaY.value : touch.deltaX.value
|
|
})
|
|
|
|
const isCorrectDirection = computed(() => {
|
|
return touch.direction.value === props.direction
|
|
})
|
|
|
|
const childCount = computed(() => internalChildren.length)
|
|
|
|
const size = computed(() => state[isVertical.value ? 'height' : 'width'])
|
|
|
|
const trackSize = computed(() => childCount.value * size.value)
|
|
|
|
const minOffset = computed(() => {
|
|
if (state.rect) {
|
|
const base = isVertical.value ? state.rect.height : state.rect.width
|
|
return base - size.value * childCount.value
|
|
}
|
|
return 0
|
|
})
|
|
|
|
const activePagination = computed(() => (state.active + childCount.value) % childCount.value)
|
|
|
|
function getStyle() {
|
|
let offset = 0
|
|
offset = state.offset
|
|
state.style = {
|
|
transitionDuration: `${state.moving ? 0 : props.duration}ms`,
|
|
transform: `translate${isVertical.value ? 'Y' : 'X'}(${offset}px)`,
|
|
[isVertical.value ? 'height' : 'width']: `${size.value * childCount.value}px`,
|
|
[isVertical.value ? 'width' : 'height']: `${isVertical.value ? state.width : state.height}px`,
|
|
}
|
|
}
|
|
|
|
const { internalChildren } = useProvide(SWIPER_KEY, 'nut-form-swiper')(
|
|
{
|
|
props,
|
|
size,
|
|
},
|
|
)
|
|
|
|
function getOffset(active: number, offset = 0) {
|
|
let currentPosition = active * size.value
|
|
if (!props.loop)
|
|
currentPosition = Math.min(currentPosition, -minOffset.value)
|
|
|
|
let targetOffset = offset - currentPosition
|
|
if (!props.loop)
|
|
targetOffset = clamp(targetOffset, minOffset.value, 0)
|
|
|
|
return targetOffset
|
|
}
|
|
|
|
function getActive(pace: number) {
|
|
const { active } = state
|
|
if (pace) {
|
|
if (props.loop)
|
|
return clamp(active + pace, -1, childCount.value)
|
|
|
|
return clamp(active + pace, 0, childCount.value - 1)
|
|
}
|
|
return active
|
|
}
|
|
|
|
function move({ pace = 0, offset = 0, isEmit = false }) {
|
|
if (childCount.value <= 1)
|
|
return
|
|
|
|
const { active } = state
|
|
|
|
const targetActive = getActive(pace)
|
|
const targetOffset = getOffset(targetActive, offset)
|
|
|
|
if (props.loop) {
|
|
if (internalChildren[0] && targetOffset !== minOffset.value) {
|
|
const rightBound = targetOffset < minOffset.value;
|
|
(internalChildren[0] as any).exposed.setOffset(rightBound ? trackSize.value : 0)
|
|
}
|
|
if (internalChildren[childCount.value - 1] && targetOffset !== 0) {
|
|
const leftBound = targetOffset > 0;
|
|
(internalChildren[childCount.value - 1] as any).exposed.setOffset(leftBound ? -trackSize.value : 0)
|
|
}
|
|
}
|
|
|
|
state.active = targetActive
|
|
state.offset = targetOffset
|
|
|
|
if (isEmit && active !== state.active)
|
|
emit('change', activePagination.value)
|
|
|
|
getStyle()
|
|
}
|
|
|
|
function resettPosition() {
|
|
state.moving = true
|
|
|
|
if (state.active <= -1)
|
|
move({ pace: childCount.value })
|
|
|
|
if (state.active >= childCount.value)
|
|
move({ pace: -childCount.value })
|
|
}
|
|
|
|
function stopAutoPlay() {
|
|
if (state.autoplayTimer)
|
|
clearTimeout(state.autoplayTimer)
|
|
}
|
|
|
|
function jump(pace: number) {
|
|
resettPosition()
|
|
touch.reset()
|
|
|
|
requestAniFrame(() => {
|
|
requestAniFrame(() => {
|
|
state.moving = false
|
|
move({
|
|
pace,
|
|
isEmit: true,
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
function prev() {
|
|
jump(-1)
|
|
}
|
|
|
|
function next() {
|
|
jump(1)
|
|
}
|
|
|
|
function to(index: number) {
|
|
resettPosition()
|
|
|
|
touch.reset()
|
|
|
|
requestAniFrame(() => {
|
|
state.moving = false
|
|
let targetIndex
|
|
if (props.loop && childCount.value === index)
|
|
targetIndex = state.active === 0 ? 0 : index
|
|
else
|
|
targetIndex = index % childCount.value
|
|
|
|
move({
|
|
pace: targetIndex - state.active,
|
|
isEmit: true,
|
|
})
|
|
})
|
|
}
|
|
|
|
function autoplay() {
|
|
if (+props.autoPlay <= 0 || childCount.value <= 1)
|
|
return
|
|
stopAutoPlay()
|
|
|
|
state.autoplayTimer = setTimeout(() => {
|
|
next()
|
|
autoplay()
|
|
}, Number(props.autoPlay))
|
|
}
|
|
|
|
async function init(active: number = +props.initPage) {
|
|
stopAutoPlay()
|
|
state.rect = await useRect(containerId, instance)
|
|
if (state.rect) {
|
|
active = Math.min(childCount.value - 1, active)
|
|
state.width = props.width ? +props.width : (state.rect as DOMRect).width
|
|
state.height = props.height ? +props.height : (state.rect as DOMRect).height
|
|
state.active = active
|
|
state.offset = getOffset(state.active)
|
|
state.moving = true
|
|
getStyle()
|
|
|
|
autoplay()
|
|
}
|
|
}
|
|
|
|
function onTouchStart(e: TouchEvent) {
|
|
if (props.isStopPropagation)
|
|
e.stopPropagation()
|
|
if (!props.touchable)
|
|
return
|
|
touch.start(e)
|
|
state.touchTime = Date.now()
|
|
stopAutoPlay()
|
|
resettPosition()
|
|
}
|
|
|
|
function onTouchMove(e: TouchEvent) {
|
|
if (props.touchable && state.moving) {
|
|
touch.move(e)
|
|
if (isCorrectDirection.value) {
|
|
move({
|
|
offset: delTa.value,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function onTouchEnd(e: TouchEvent) {
|
|
if (!props.touchable || !state.moving)
|
|
return
|
|
const speed = delTa.value / (Date.now() - state.touchTime)
|
|
const isShouldMove = Math.abs(speed) > 0.3 || Math.abs(delTa.value) > +(size.value / 2).toFixed(2)
|
|
|
|
if (isShouldMove && isCorrectDirection.value) {
|
|
let pace = 0
|
|
const offset = isVertical.value ? touch.offsetY.value : touch.offsetX.value
|
|
if (props.loop)
|
|
pace = offset > 0 ? (delTa.value > 0 ? -1 : 1) : 0
|
|
else
|
|
pace = -Math[delTa.value > 0 ? 'ceil' : 'floor'](delTa.value / size.value)
|
|
|
|
move({
|
|
pace,
|
|
isEmit: true,
|
|
})
|
|
}
|
|
else if (delTa.value) {
|
|
move({ pace: 0 })
|
|
}
|
|
state.moving = false
|
|
getStyle()
|
|
autoplay()
|
|
}
|
|
|
|
defineExpose({
|
|
prev,
|
|
next,
|
|
to,
|
|
})
|
|
|
|
onDeactivated(() => {
|
|
stopAutoPlay()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
stopAutoPlay()
|
|
})
|
|
|
|
watch(
|
|
() => props.initPage,
|
|
(val) => {
|
|
nextTick(() => {
|
|
init(+val)
|
|
})
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.height,
|
|
(val) => {
|
|
nextTick(() => {
|
|
init(+val)
|
|
})
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => internalChildren.length,
|
|
() => {
|
|
nextTick(() => {
|
|
init()
|
|
})
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.autoPlay,
|
|
(val) => {
|
|
+val > 0 ? autoplay() : stopAutoPlay()
|
|
},
|
|
)
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
const componentName = `${PREFIX}-swiper`
|
|
|
|
export default defineComponent({
|
|
name: componentName,
|
|
options: {
|
|
virtualHost: true,
|
|
addGlobalClass: true,
|
|
styleIsolation: 'shared',
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<view
|
|
:id="containerId"
|
|
:class="[classes, customClass]"
|
|
:catch-move="isPreventDefault"
|
|
:style="[customStyle]"
|
|
@touchstart="(onTouchStart as any)"
|
|
@touchmove="(onTouchMove as any)"
|
|
@touchend="(onTouchEnd as any)"
|
|
@touchcancel="(onTouchEnd as any)"
|
|
>
|
|
<view :class="classesInner" :style="state.style">
|
|
<slot />
|
|
</view>
|
|
<slot name="page" />
|
|
<view v-if="paginationVisible && !$slots.page" :class="classesPagination">
|
|
<i
|
|
v-for="(item, index) in internalChildren.length"
|
|
:key="index"
|
|
class="pagination"
|
|
:style="{
|
|
backgroundColor: activePagination === index ? paginationColor : paginationUnselectedColor,
|
|
}"
|
|
:class="{ active: activePagination === index }"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './index';
|
|
</style>
|