From d0d167f5eabb2115aba292250f5f581a70508eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Mon, 19 Jan 2026 17:05:03 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(carousel):=20=E8=B5=B0=E9=A9=AC?= =?UTF-8?q?=E7=81=AF=E7=9A=84=20draggable=20=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E8=BF=98=E6=9C=AA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/renderless/src/carousel/index.ts | 526 +++++++++++++--------- packages/renderless/src/carousel/vue.ts | 13 +- packages/theme/src/carousel/index.less | 11 + packages/vue/src/carousel/src/index.ts | 6 +- packages/vue/src/carousel/src/pc.vue | 10 +- 5 files changed, 347 insertions(+), 219 deletions(-) diff --git a/packages/renderless/src/carousel/index.ts b/packages/renderless/src/carousel/index.ts index 67026913ae..f81700ec39 100644 --- a/packages/renderless/src/carousel/index.ts +++ b/packages/renderless/src/carousel/index.ts @@ -6,48 +6,48 @@ import { emulate } from '@opentiny/utils' // 鼠标进入幻灯片事件 export const handleMouseEnter = ({ api, state }) => - () => { - state.hover = true - api.pauseTimer() - } + () => { + state.hover = true + api.pauseTimer() + } export const handleMouseLeave = ({ api, state }) => - () => { - state.hover = false - api.startTimer() - } + () => { + state.hover = false + api.startTimer() + } export const itemInStage = (state) => - ({ item, index }) => { - const length = state.items.length - const arr = state.items - - if ( - (index === length - 1 && item.inStage && arr[0].active) || - (item.inStage && arr[index + 1] && arr[index + 1].active) - ) { - return POSITION.Left - } else if ( - (index === 0 && item.inStage && arr[length - 1].active) || - (item.inStage && arr[index - 1] && arr[index - 1].active) - ) { - return POSITION.Right - } + ({ item, index }) => { + const length = state.items.length + const arr = state.items + + if ( + (index === length - 1 && item.inStage && arr[0].active) || + (item.inStage && arr[index + 1] && arr[index + 1].active) + ) { + return POSITION.Left + } else if ( + (index === 0 && item.inStage && arr[length - 1].active) || + (item.inStage && arr[index - 1] && arr[index - 1].active) + ) { + return POSITION.Right + } - return false - } + return false + } export const handleButtonEnter = ({ api, state }) => - (arrow) => { - state.items.forEach((item, index) => { - if (arrow === api.itemInStage({ item, index })) { - item.hover = true - } - }) - } + (arrow) => { + state.items.forEach((item, index) => { + if (arrow === api.itemInStage({ item, index })) { + item.hover = true + } + }) + } export const handleButtonLeave = (state) => () => { state.items.forEach((item) => { @@ -66,22 +66,22 @@ export const resetItemPosition = (state) => (oldIndex) => { // 播放幻灯片 export const playSlides = ({ api, props, state }) => - () => { - let newIndex - let oldIndex = state.activeIndex - - if (state.activeIndex < state.items.length - 1) { - newIndex = state.activeIndex + 1 - } else if (props.loop) { - newIndex = 0 - } - - api.canActive(newIndex, oldIndex).then((result) => { - if (result) { - state.activeIndex = newIndex + () => { + let newIndex + let oldIndex = state.activeIndex + + if (state.activeIndex < state.items.length - 1) { + newIndex = state.activeIndex + 1 + } else if (props.loop) { + newIndex = 0 } - }) - } + + api.canActive(newIndex, oldIndex).then((result) => { + if (result) { + state.activeIndex = newIndex + } + }) + } // 暂停计时器 export const pauseTimer = (state) => () => clearInterval(state.timer) @@ -89,61 +89,61 @@ export const pauseTimer = (state) => () => clearInterval(state.timer) // 启动计时器 export const startTimer = ({ api, props, state }) => - () => { - if (props.interval <= 0 || !props.autoplay) { - return - } + () => { + if (props.interval <= 0 || !props.autoplay) { + return + } - state.timer = setInterval(api.playSlides, props.interval) - } + state.timer = setInterval(api.playSlides, props.interval) + } export const setActiveItem = ({ api, props, state }) => - (index) => { - if (typeof index === 'string') { - const filteredItems = state.items.filter((item) => item.name === index) + (index) => { + if (typeof index === 'string') { + const filteredItems = state.items.filter((item) => item.name === index) - if (filteredItems.length > 0) { - index = state.items.indexOf(filteredItems[0]) + if (filteredItems.length > 0) { + index = state.items.indexOf(filteredItems[0]) + } } - } - index = Number(index) + index = Number(index) - if (isNaN(index) || index !== Math.floor(index)) { - return - } + if (isNaN(index) || index !== Math.floor(index)) { + return + } - const length = state.items.length - const oldIndex = state.activeIndex - let newIndex + const length = state.items.length + const oldIndex = state.activeIndex + let newIndex - if (index < 0) { - newIndex = props.loop ? length - 1 : 0 - } else if (index >= length) { - newIndex = props.loop ? 0 : length - 1 - } else { - newIndex = index - } + if (index < 0) { + newIndex = props.loop ? length - 1 : 0 + } else if (index >= length) { + newIndex = props.loop ? 0 : length - 1 + } else { + newIndex = index + } - const nextProcess = () => { - state.activeIndex = newIndex + const nextProcess = () => { + state.activeIndex = newIndex - if (oldIndex === state.activeIndex) { - api.resetItemPosition(oldIndex) + if (oldIndex === state.activeIndex) { + api.resetItemPosition(oldIndex) + } } - } - if (newIndex === oldIndex) { - nextProcess() - } else { - api.canActive(newIndex, oldIndex).then((result) => { - if (result) { - nextProcess() - } - }) + if (newIndex === oldIndex) { + nextProcess() + } else { + api.canActive(newIndex, oldIndex).then((result) => { + if (result) { + nextProcess() + } + }) + } } - } export const canActive = (props) => (newIndex, oldIndex) => { return new Promise((resolve) => { if (typeof props.beforeSwipe === 'function') { @@ -153,53 +153,53 @@ export const canActive = (props) => (newIndex, oldIndex) => { } }).then((result) => result !== false) } - +//下一张 export const prev = ({ api, state }) => - () => - api.setActiveItem(state.activeIndex - 1) - + () => + api.setActiveItem(state.activeIndex - 1) +//上一张 export const next = ({ api, state }) => - () => - api.setActiveItem(state.activeIndex + 1) + () => + api.setActiveItem(state.activeIndex + 1) export const handleIndicatorClick = ({ api, state }) => - (index) => { - api.canActive(index, state.activeIndex).then((result) => { - if (result) { - state.activeIndex = index - } - }) - } - -export const handleIndicatorHover = - ({ api, props, state }) => - (index) => { - if (props.trigger === 'hover' && index !== state.activeIndex) { + (index) => { api.canActive(index, state.activeIndex).then((result) => { if (result) { state.activeIndex = index } }) } - } + +export const handleIndicatorHover = + ({ api, props, state }) => + (index) => { + if (props.trigger === 'hover' && index !== state.activeIndex) { + api.canActive(index, state.activeIndex).then((result) => { + if (result) { + state.activeIndex = index + } + }) + } + } export const watchItems = ({ props, api }) => - (value) => { - if (value.length) { - api.setActiveItem(props.initialIndex) + (value) => { + if (value.length) { + api.setActiveItem(props.initialIndex) + } } - } export const watchActiveIndex = ({ emit, api }) => - ({ value, oldValue }) => { - api.resetItemPosition(oldValue) - emit('change', value, oldValue) - } + ({ value, oldValue }) => { + api.resetItemPosition(oldValue) + emit('change', value, oldValue) + } export const watchAutoplay = (api) => (value) => (value ? api.startTimer() : api.pauseTimer()) @@ -211,119 +211,119 @@ export const computedHasLabel = (items) => items.some((item) => item.label.toStr export const computedStyle = ({ props }) => - () => { - if (props.height) { - return { 'height': props.height } - } else { - // 低版本浏览器兼容(chrome 58 不支持 aspect-ratio属性) - if (CSS.supports('aspect-ratio', 'auto')) { - return { 'aspect-ratio': props.aspectRatio.replace(':', ' / ') } + () => { + if (props.height) { + return { 'height': props.height } } else { - const ratio = props.aspectRatio.split(':') - const paddingTop = ((ratio[1] / ratio[0]) * 100).toFixed(2) + '%' - - return { 'width': '100%', 'height': 0, 'padding-top': paddingTop } + // 低版本浏览器兼容(chrome 58 不支持 aspect-ratio 属性) + if (CSS.supports('aspect-ratio', 'auto')) { + return { 'aspect-ratio': props.aspectRatio.replace(':', ' / ') } + } else { + const ratio = props.aspectRatio.split(':') + const paddingTop = ((ratio[1] / ratio[0]) * 100).toFixed(2) + '%' + + return { 'width': '100%', 'height': 0, 'padding-top': paddingTop } + } } } - } export const onComplete = ({ api, count, emit, props, state }) => - (total) => { - if (count++ === total) { - state.completed = true + (total) => { + if (count++ === total) { + state.completed = true + + if (props.initialIndex < state.items.length && props.initialIndex >= 0) { + api.canActive(props.initialIndex, state.activeIndex).then((result) => { + if (result) { + state.activeIndex = props.initialIndex + } + }) + } - if (props.initialIndex < state.items.length && props.initialIndex >= 0) { - api.canActive(props.initialIndex, state.activeIndex).then((result) => { - if (result) { - state.activeIndex = props.initialIndex - } - }) + emit('complete') } - - emit('complete') } - } export const touchstart = ({ props, state, api }) => - (event) => { - if (state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + (event) => { + if (state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return - if (!props.swipeable) { - event.preventDefault() - event.stopPropagation() - } + if (!props.swipeable) { + event.preventDefault() + event.stopPropagation() + } - resetTouchStatus(state) - api.pauseTimer() - state.itemsTranslate = state.items.map((item) => item.state.translate) + resetTouchStatus(state) + api.pauseTimer() + state.itemsTranslate = state.items.map((item) => item.state.translate) - state.moving = true - state.touchTime = Date.now() - state.startPos.X = event.touches[0].clientX - state.startPos.Y = event.touches[0].clientY - } + state.moving = true + state.touchTime = Date.now() + state.startPos.X = event.touches[0].clientX + state.startPos.Y = event.touches[0].clientY + } export const touchmove = ({ props, state, vm }) => - (event) => { - if (state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + (event) => { + if (state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return - const touch = event.touches[0] - const itemsLen = state.items.length - const carousel = vm.$refs.carousel + const touch = event.touches[0] + const itemsLen = state.items.length + const carousel = vm.$refs.carousel - state.deltaPos.X = touch.clientX - state.startPos.X - state.deltaPos.Y = touch.clientY - state.startPos.Y - state.offsetPos.X = Math.abs(state.deltaPos.X) - state.offsetPos.Y = Math.abs(state.deltaPos.Y) - state.direction = state.direction || getDirection(state.offsetPos.X, state.offsetPos.Y) - state.isCorrectDirection = state.direction === props.type + state.deltaPos.X = touch.clientX - state.startPos.X + state.deltaPos.Y = touch.clientY - state.startPos.Y + state.offsetPos.X = Math.abs(state.deltaPos.X) + state.offsetPos.Y = Math.abs(state.deltaPos.Y) + state.direction = state.direction || getDirection(state.offsetPos.X, state.offsetPos.Y) + state.isCorrectDirection = state.direction === props.type - if (!state.isCorrectDirection) return + if (!state.isCorrectDirection) return - state.size = state.direction === 'horizontal' ? carousel.offsetWidth : carousel.offsetHeight + state.size = state.direction === 'horizontal' ? carousel.offsetWidth : carousel.offsetHeight - const nextIndex = state.activeIndex === itemsLen - 1 ? 0 : state.activeIndex + 1 - const prevIndex = state.activeIndex === 0 ? itemsLen - 1 : state.activeIndex - 1 + const nextIndex = state.activeIndex === itemsLen - 1 ? 0 : state.activeIndex + 1 + const prevIndex = state.activeIndex === 0 ? itemsLen - 1 : state.activeIndex - 1 - state.delta = state.direction === 'horizontal' ? state.deltaPos.X : state.deltaPos.Y + state.delta = state.direction === 'horizontal' ? state.deltaPos.X : state.deltaPos.Y - state.moveDisable = - !props.loop && - ((state.activeIndex === 0 && state.delta > 0) || - (state.activeIndex === state.items.length - 1 && state.delta < 0)) + state.moveDisable = + !props.loop && + ((state.activeIndex === 0 && state.delta > 0) || + (state.activeIndex === state.items.length - 1 && state.delta < 0)) - if (state.moveDisable) return + if (state.moveDisable) return - state.items[state.activeIndex].setDelta(state.delta) - state.items[nextIndex].setDelta(state.delta) + state.items[state.activeIndex].setDelta(state.delta) + state.items[nextIndex].setDelta(state.delta) - if (itemsLen > 2) { - state.items[prevIndex].setDelta(state.delta) + if (itemsLen > 2) { + state.items[prevIndex].setDelta(state.delta) + } } - } export const touchend = ({ state, api }) => - (event) => { - if (state.moveDisable || state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return - const speed = state.delta / (Date.now() - state.touchTime) - const isShouldMove = Math.abs(speed) > 0.3 || Math.abs(state.delta) > +(state.size / 2).toFixed(2) - state.moving = false - state.itemsTranslate.forEach((item, index) => { - state.items[index].setDelta(0) - }) - if (isShouldMove && state.isCorrectDirection) { - state.delta < 0 ? api.next() : api.prev() - } else if (Math.abs(state.delta) > 1) { - state.items.forEach((item) => { - item.resetAnimatingMf() + (event) => { + if (state.moveDisable || state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + const speed = state.delta / (Date.now() - state.touchTime) + const isShouldMove = Math.abs(speed) > 0.3 || Math.abs(state.delta) > +(state.size / 2).toFixed(2) + state.moving = false + state.itemsTranslate.forEach((item, index) => { + state.items[index].setDelta(0) }) + if (isShouldMove && state.isCorrectDirection) { + state.delta < 0 ? api.next() : api.prev() + } else if (Math.abs(state.delta) > 1) { + state.items.forEach((item) => { + item.resetAnimatingMf() + }) + } + api.startTimer() } - api.startTimer() - } function resetTouchStatus(state) { state.direction = '' @@ -336,33 +336,131 @@ function resetTouchStatus(state) { export const simulateTouch = ({ props, vm }) => - () => { - if (props.swipeable && vm.$refs.carousel) { - emulate() - vm.$refs.carousel.setAttribute('data-tiny-touch-simulate-container', '') + () => { + if (props.swipeable && vm.$refs.carousel) { + emulate() + vm.$refs.carousel.setAttribute('data-tiny-touch-simulate-container', '') + } } - } export const computedHasButtons = ({ props, state, mode }) => - () => { - if (props.lite) { - return false - } else if (mode === 'mobile-first') { - return props.arrow !== 'never' && state.items.length > 1 - } else if (mode === 'pc') { - return props.arrow !== 'never' + () => { + if (props.lite) { + return false + } else if (mode === 'mobile-first') { + return props.arrow !== 'never' && state.items.length > 1 + } else if (mode === 'pc') { + return props.arrow !== 'never' + } } - } export const computedHasIndicators = ({ props, state, mode }) => - () => { - if (props.lite) { - return false - } else if (mode === 'mobile-first') { - return props.indicatorPosition !== 'none' && state.items.length > 1 - } else if (mode === 'pc') { - return props.indicatorPosition !== 'none' + () => { + if (props.lite) { + return false + } else if (mode === 'mobile-first') { + return props.indicatorPosition !== 'none' && state.items.length > 1 + } else if (mode === 'pc') { + return props.indicatorPosition !== 'none' + } + } + + +//鼠标拖动切换走马灯 +export const mousedown = + ({ props, state, api }) => + (event) => { + if (state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + //少于两个和在按钮上面时不干活 + if (!props.draggable) return; + + resetTouchStatus(state) + api.pauseTimer() + //取消自动播放等等 + + state.moving = true + state.itemsTranslate = state.items.map((item) => item.state.translate) + //记录初始位置 + state.startPos.X = event.clientX + state.startPos.Y = event.clientY + state.touchTime = Date.now() + } + + +export const mousemove = + ({ props, state, vm }) => + (event) => { + if (!state.moving || state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + + const carousel = vm.$refs.carousel + if (!carousel) return + + state.deltaPos.X = event.clientX - state.startPos.X + state.deltaPos.Y = event.clientY - state.startPos.Y + state.offsetPos.X = Math.abs(state.deltaPos.X) + state.offsetPos.Y = Math.abs(state.deltaPos.Y) + state.direction = state.direction || getDirection(state.offsetPos.X, state.offsetPos.Y) + state.isCorrectDirection = state.direction === props.type + + if (!state.isCorrectDirection) return + + state.size = state.direction === 'horizontal' ? carousel.offsetWidth : carousel.offsetHeight + + const itemsLen = state.items.length + const nextIndex = state.activeIndex === itemsLen - 1 ? 0 : state.activeIndex + 1 + const prevIndex = state.activeIndex === 0 ? itemsLen - 1 : state.activeIndex - 1 + + state.delta = state.direction === 'horizontal' ? state.deltaPos.X : state.deltaPos.Y + + state.moveDisable = + !props.loop && + ((state.activeIndex === 0 && state.delta > 0) && + (state.activeIndex === state.items.length - 1 && state.delta < 0)) + + if (state.moveDisable) return + + state.items[state.activeIndex].setDelta(state.delta) + state.items[nextIndex].setDelta(state.delta) + + if (itemsLen > 2) { + state.items[prevIndex].setDelta(state.delta) + } + } + +export const mouseup = + ({ state, api }) => + (event) => { + if (!state.moving || state.moveDisable || state.items.length <= 1 || ~state.noTouchNode.indexOf(event.target.nodeName)) return + + const speed = state.delta / (Date.now() - state.touchTime) + const isShouldMove = Math.abs(speed) > 0.3 || Math.abs(state.delta) > +(state.size / 2).toFixed(2) + + state.moving = false + state.itemsTranslate.forEach((item, index) => { + state.items[index].setDelta(0) + }) + + if (isShouldMove && state.isCorrectDirection) { + state.delta < 0 ? api.next() : api.prev() + } else if (Math.abs(state.delta) > 1) { + state.items.forEach((item) => { + item.resetAnimatingMf() + }) + } + + api.startTimer() + } + +export const mouseleave = + ({ state, api }) => + (event) => { + if (state.moving) { + state.moving = false + state.itemsTranslate.forEach((item, index) => { + state.items[index].setDelta(0) + }) + api.startTimer() + } } - } diff --git a/packages/renderless/src/carousel/vue.ts b/packages/renderless/src/carousel/vue.ts index 7e174e562c..fa7a01eeb0 100644 --- a/packages/renderless/src/carousel/vue.ts +++ b/packages/renderless/src/carousel/vue.ts @@ -27,7 +27,11 @@ import { simulateTouch, computedHasButtons, computedHasIndicators, - canActive + canActive, + mousedown, + mousemove, + mouseup, + mouseleave } from './index' import { addResizeListener, removeResizeListener } from '@opentiny/utils' @@ -130,7 +134,12 @@ const initApi = ({ vm, api, state, props, emit, mode }) => { simulateTouch: simulateTouch({ props, vm }), computedHasButtons: computedHasButtons({ props, state, mode }), computedHasIndicators: computedHasIndicators({ props, state, mode }), - canActive: canActive(props) + canActive: canActive(props), + mousedown: mousedown({ props, state, api }), // 新增 + mousemove: mousemove({ props, state, vm }), // 新增 + mouseup: mouseup({ state, api }), // 新增 + mouseleave: mouseleave({ state, api }), // 新增 + //拖动相关的事件 }) } diff --git a/packages/theme/src/carousel/index.less b/packages/theme/src/carousel/index.less index 07d5340e36..4f288d6e41 100644 --- a/packages/theme/src/carousel/index.less +++ b/packages/theme/src/carousel/index.less @@ -252,3 +252,14 @@ } } } + + +.tiny-carousel--draggable { + cursor: grab; + + &.tiny-carousel--dragging { + cursor: grabbing; + user-select: none; + } +} +//拖动时候的样式 \ No newline at end of file diff --git a/packages/vue/src/carousel/src/index.ts b/packages/vue/src/carousel/src/index.ts index 99e394532a..b8b2659d4b 100644 --- a/packages/vue/src/carousel/src/index.ts +++ b/packages/vue/src/carousel/src/index.ts @@ -75,7 +75,11 @@ export default defineComponent({ }, swipeable: Boolean, lite: Boolean, - beforeSwipe: Function + beforeSwipe: Function, + draggable: { + type: Boolean, + default: true + }, }, setup(props, context) { return $setup({ diff --git a/packages/vue/src/carousel/src/pc.vue b/packages/vue/src/carousel/src/pc.vue index cf0643750c..fe88fa6480 100644 --- a/packages/vue/src/carousel/src/pc.vue +++ b/packages/vue/src/carousel/src/pc.vue @@ -11,10 +11,15 @@ -->