Files
2026-01-05 12:47:14 +08:00

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>