diff --git a/examples/sites/demos/apis/carousel.js b/examples/sites/demos/apis/carousel.js
index 4184e25d12..d99c251e54 100644
--- a/examples/sites/demos/apis/carousel.js
+++ b/examples/sites/demos/apis/carousel.js
@@ -22,7 +22,7 @@ export default {
type: 'String',
defaultValue: '',
desc: {
- 'zh-CN': '宽高比,默认为16:2。',
+ 'zh-CN': '宽高比,默认为 16:2。',
'en-US': 'Aspect ratio. The default value is 16:2.'
},
mode: ['mobile-first'],
@@ -199,6 +199,22 @@ export default {
mode: ['pc', 'mobile-first'],
pcDemo: 'swipeable',
mfDemo: ''
+ },
+ //draggable
+ {
+ name: 'draggable',
+ type: 'boolean',
+ defaultValue: 'false',
+ desc: {
+ 'zh-CN': '是否允许拖拽切换幻灯片',
+ 'en-US': 'Whether to allow dragging to switch slides'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'draggable',
+ mfDemo: 'draggable',
+ meta: {
+ stable: '3.29.0'
+ }
}
],
events: [
diff --git a/examples/sites/demos/mobile-first/app/collapse/draggable.vue b/examples/sites/demos/mobile-first/app/collapse/draggable.vue
new file mode 100644
index 0000000000..57556a3230
--- /dev/null
+++ b/examples/sites/demos/mobile-first/app/collapse/draggable.vue
@@ -0,0 +1,45 @@
+
+
+
+ 与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;
+ 在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。
+
+
+ 控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;
+ 页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。
+
+
+ 简化流程:设计简洁直观的操作流程;
+ 清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;
+ 帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。
+
+
+ 用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;
+ 结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。
+
+
+
+
+
diff --git a/examples/sites/demos/mobile-first/app/collapse/webdoc/collapse.js b/examples/sites/demos/mobile-first/app/collapse/webdoc/collapse.js
index 2b2a4ba4bc..7174090810 100644
--- a/examples/sites/demos/mobile-first/app/collapse/webdoc/collapse.js
+++ b/examples/sites/demos/mobile-first/app/collapse/webdoc/collapse.js
@@ -22,7 +22,7 @@ export default {
},
desc: {
'zh-CN':
- '
配置 `accordion` 属性为 true 后,折叠面板将展示手风琴效果。配置 `v-model` 设置当前激活的面板(如果是手风琴模式,绑定值类型需要为 string ,否则为 array );`change` 事件,在当前激活面板改变时触发,参数为当前的 value 值;配置 `name` 属性作为每个 collapse-item 的唯一标志符
',
+ '
配置 `accordion` 属性为 true 后,折叠面板将展示手风琴效果。配置 `v-model` 设置当前激活的面板 (如果是手风琴模式,绑定值类型需要为 string,否则为 array );`change` 事件,在当前激活面板改变时触发,参数为当前的 value 值;配置 `name` 属性作为每个 collapse-item 的唯一标志符
',
'en-US':
'
If the `accordion` attribute is set to true, the collapse panel displays the accordion effect. Configure `v-model` Sets the currently active panel. (In accordion mode, the binding value type must be string. Otherwise, the binding value type is array.) `change` event, which is triggered when the current active panel changes. The parameter is the current value. Configure the `name` attribute as the unique identifier for each collapse-item
'
},
@@ -88,7 +88,7 @@ export default {
'en-US': 'Prevent Collapse Panel Close Event'
},
desc: {
- 'zh-CN': ':before-close 折叠面板关闭前事件,方法里面 return true/false,表示是否可以关闭
',
+ 'zh-CN': '
:before-close 折叠面板关闭前事件,方法里面 return true/false,表示是否可以关闭
',
'en-US':
'
:before-close: indicates the event before the folding panel is closed. In the method, return true or false indicates whether the folding panel can be closed.
'
},
@@ -154,6 +154,19 @@ export default {
'en-US': 'Icons are implemented by configuring the built-in icon slot of Collapse.
'
},
codeFiles: ['slot-icon.vue']
+ },
+ //draggable
+ {
+ demoId: 'draggable',
+ name: {
+ 'zh-CN': '可拖拽',
+ 'en-US': 'Draggable'
+ },
+ desc: {
+ 'zh-CN': '通过配置 Collapse 内置的 draggable 属性实现拖拽
',
+ 'en-US': '
Drag and drop is implemented by configuring the built-in draggable slot of Collapse.
'
+ },
+ codeFiles: ['draggable.vue']
}
]
}
diff --git a/examples/sites/demos/pc/app/carousel/draggable.spec.ts b/examples/sites/demos/pc/app/carousel/draggable.spec.ts
new file mode 100644
index 0000000000..800bc16d36
--- /dev/null
+++ b/examples/sites/demos/pc/app/carousel/draggable.spec.ts
@@ -0,0 +1,36 @@
+import { test, expect } from '@playwright/test'
+
+test('draggable - 基本功能测试', async ({ page }) => {
+ page.on('pageerror', (exception) => expect(exception).toBeNull())
+
+ // 1. 打开页面
+ await page.goto('carousel#draggable')
+
+ // 2. 验证组件加载
+ await expect(page.locator('.tiny-carousel')).toBeVisible()
+
+ // 3. 验证有轮播项
+ const items = page.locator('.tiny-carousel__item')
+ const count = await items.count()
+ expect(count).toBeGreaterThan(0)
+
+ // 4. 验证初始状态
+ await expect(items.nth(0)).toHaveClass(/active/)
+
+ // 5. 测试鼠标事件可以触发(不验证具体切换)
+ const carousel = page.locator('.tiny-carousel').first()
+ await carousel.hover()
+ await carousel.dispatchEvent('mousedown')
+ await page.waitForTimeout(50)
+ await carousel.dispatchEvent('mousemove', { clientX: 100, clientY: 100 })
+ await page.waitForTimeout(50)
+ await carousel.dispatchEvent('mouseup')
+
+ // 6. 测试触摸事件可以触发
+ await carousel.dispatchEvent('touchstart')
+ await page.waitForTimeout(50)
+ await carousel.dispatchEvent('touchend')
+
+ // 7. 验证没有报错即可
+ console.log('draggable 测试通过')
+})
\ No newline at end of file
diff --git a/examples/sites/demos/pc/app/carousel/draggable.vue b/examples/sites/demos/pc/app/carousel/draggable.vue
new file mode 100644
index 0000000000..1ae164d37f
--- /dev/null
+++ b/examples/sites/demos/pc/app/carousel/draggable.vue
@@ -0,0 +1,37 @@
+
+
+
+ {{ item }}
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/carousel/webdoc/carousel.js b/examples/sites/demos/pc/app/carousel/webdoc/carousel.js
index ba0bb6f880..ebdd99ba14 100644
--- a/examples/sites/demos/pc/app/carousel/webdoc/carousel.js
+++ b/examples/sites/demos/pc/app/carousel/webdoc/carousel.js
@@ -198,6 +198,21 @@ export default {
'en-US': 'Embed a carousel scene in the pop-up window.
'
},
codeFiles: ['dialog-show.vue']
+ },
+ {
+ demoId: 'draggable',
+ name: {
+ 'zh-CN': '拖拽',
+ 'en-US': 'Draggable'
+ },
+ desc: {
+ 'zh-CN': '通过配置 draggable 属性为true后,走马灯的幻灯片内容将支持拖拽。
\n',
+ 'en-US': 'After draggable is set to true, the slide content of the walk-through is supported for dragging.
\n'
+ },
+ codeFiles: ['draggable.vue'],
+ meta: {
+ stable: '3.29.0'
+ }
}
],
features: [
diff --git a/packages/renderless/src/carousel/index.ts b/packages/renderless/src/carousel/index.ts
index 67026913ae..0e32264a51 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,134 @@ 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.itemsTranslate = state.items.map((item) => item.state.translate)
+
+ state.moving = true
+ state.touchTime = Date.now()
+ state.startPos.X = event.clientX
+ state.startPos.Y = event.clientY
+ }
+
+// 鼠标拖拽移动
+export const mousemove =
+ ({ props, state, vm }) =>
+ throttle(100, true, (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()
+ }
+ }
\ No newline at end of file
diff --git a/packages/renderless/src/carousel/vue.ts b/packages/renderless/src/carousel/vue.ts
index 7e174e562c..223fe0e018 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'
@@ -58,7 +62,11 @@ export const api = [
'throttledIndicatorHover',
'touchstart',
'touchmove',
- 'touchend'
+ 'touchend',
+ 'mousedown',
+ 'mousemove',
+ 'mouseup',
+ 'mouseleave'
]
const initState = ({ reactive, computed, api }) => {
@@ -130,7 +138,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({ props,state, api }),
+ mouseleave: mouseleave({ props,state, api }),
+ //拖动相关的事件
})
}
@@ -163,12 +176,14 @@ export const renderless = (
provide('CarouselVm', vm)
initWatch({ watch, props, api, state })
+
onMounted(() => {
api.startTimer()
api.onComplete(state.items.length)
api.simulateTouch()
})
+
// 监听子组件 CarouselItem 提交的 complete 事件
parent.$on('updateItems', api.updateItems)
diff --git a/packages/theme/src/carousel/index.less b/packages/theme/src/carousel/index.less
index 07d5340e36..d6391c8f69 100644
--- a/packages/theme/src/carousel/index.less
+++ b/packages/theme/src/carousel/index.less
@@ -252,3 +252,10 @@
}
}
}
+
+
+.tiny-carousel--draggable {
+ //鼠标效果
+ 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..733f49efca 100644
--- a/packages/vue/src/carousel/src/pc.vue
+++ b/packages/vue/src/carousel/src/pc.vue
@@ -11,10 +11,15 @@
-->