init
This commit is contained in:
53
uni_modules/nutui-uni/components/swiper/index.scss
Normal file
53
uni_modules/nutui-uni/components/swiper/index.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.nut-swiper {
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition-property: transform;
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-vertical {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.pagination {
|
||||
width: $swiper-pagination-item-width;
|
||||
height: $swiper-pagination-item-height;
|
||||
margin-right: $swiper-pagination-item-margin-right;
|
||||
border-radius: $swiper-pagination-item-border-radius;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-pagination-vertical {
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
left: 12px;
|
||||
flex-direction: column;
|
||||
transform: translateY(-50%);
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
|
||||
.pagination {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/swiper/index.ts
Normal file
1
uni_modules/nutui-uni/components/swiper/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type * from './swiper'
|
||||
97
uni_modules/nutui-uni/components/swiper/swiper.ts
Normal file
97
uni_modules/nutui-uni/components/swiper/swiper.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CHANGE_EVENT } from '../_constants'
|
||||
import { commonProps, isNumber, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
|
||||
export const swiperProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 轮播卡片的宽度
|
||||
*/
|
||||
width: makeNumericProp(''),
|
||||
/**
|
||||
* @description 轮播卡片的高度
|
||||
*/
|
||||
height: makeNumericProp(''),
|
||||
/**
|
||||
* @description 轮播方向,可选值 `horizontal`, `vertical`
|
||||
*/
|
||||
direction: makeStringProp<'horizontal' | 'vertical'>('horizontal'),
|
||||
/**
|
||||
* @description 分页指示器是否展示
|
||||
*/
|
||||
paginationVisible: Boolean,
|
||||
/**
|
||||
* @description 分页指示器选中的颜色
|
||||
*/
|
||||
paginationColor: makeStringProp('#fff'),
|
||||
/**
|
||||
* @description 是否循环轮播
|
||||
*/
|
||||
loop: truthProp,
|
||||
/**
|
||||
* @description 动画时长(单位是ms)
|
||||
*/
|
||||
duration: makeNumericProp(500),
|
||||
/**
|
||||
* @description 自动轮播时长,0表示不会自动轮播
|
||||
*/
|
||||
autoPlay: makeNumericProp(0),
|
||||
/**
|
||||
* @description 是否自动播放
|
||||
*/
|
||||
isAutoPlay: truthProp,
|
||||
/**
|
||||
* @description 初始化索引值
|
||||
*/
|
||||
initPage: makeNumericProp(0),
|
||||
/**
|
||||
* @description 是否可触摸滑动
|
||||
*/
|
||||
touchable: truthProp,
|
||||
/**
|
||||
* @description 滑动过程中是否禁用默认事件
|
||||
*/
|
||||
isPreventDefault: truthProp,
|
||||
/**
|
||||
* @description 滑动过程中是否禁止冒泡
|
||||
*/
|
||||
isStopPropagation: truthProp,
|
||||
/**
|
||||
* @description 轮播列表数据
|
||||
*/
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* @description 分页指示器没有选中的颜色
|
||||
*/
|
||||
paginationUnselectedColor: makeStringProp('#ddd'),
|
||||
}
|
||||
|
||||
export type SwiperProps = ExtractPropTypes<typeof swiperProps>
|
||||
|
||||
export const swiperEmits = {
|
||||
[CHANGE_EVENT]: (val: number) => isNumber(val),
|
||||
}
|
||||
|
||||
export type SwiperEmits = typeof swiperEmits
|
||||
|
||||
export const SWIPER_KEY = Symbol('swiper')
|
||||
|
||||
export interface SwiperInst {
|
||||
/**
|
||||
* @description 切换到上一页
|
||||
*/
|
||||
prev: () => void
|
||||
/**
|
||||
* @description 切换到下一页
|
||||
*/
|
||||
next: () => void
|
||||
/**
|
||||
* @description 切换到指定轮播
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
to: (index: number) => void
|
||||
}
|
||||
377
uni_modules/nutui-uni/components/swiper/swiper.vue
Normal file
377
uni_modules/nutui-uni/components/swiper/swiper.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user