You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
6.9 KiB
303 lines
6.9 KiB
<template> |
|
<view class="swipe" @click.stop="onClick"> |
|
<view class="li" :style="movedistance" @touchstart="touchStart" @touchend="touchEnd"> |
|
<!-- 组件内部显示内容 --> |
|
<slot /> |
|
<div class="operations" :id="`operations${index}`" @click.stop.prevent> |
|
<view class="operations-list"> |
|
<block v-if="operating && operating.confirmNeed"> |
|
<view :class="[confirmClass(operating)]" @click="doConfirm(operating)">{{ operating.confirmText }}</view> |
|
</block> |
|
|
|
<block v-else> |
|
<view :class="[operationClass(operation)]" @click="doOperate(operation)" v-for="(operation, index) in operations" :key="index"> |
|
{{ operation.text }} |
|
</view> |
|
</block> |
|
</view> |
|
</div> |
|
</view> |
|
</view> |
|
</template> |
|
<script lang="ts" setup> |
|
import { computed, getCurrentInstance, nextTick, Ref, ref, watch } from 'vue' |
|
import { CommonUtil } from '../../index' |
|
/** |
|
* SwipeAction 滑动操作 |
|
* |
|
*/ |
|
interface Props { |
|
// 当前项的下标 |
|
index: number |
|
// 滑动项的下标 |
|
moveIndex: Ref<number> |
|
// 是否允许滑动 |
|
swipeAble?: boolean |
|
// 自定义操作数组 |
|
operations: Array<Operation> |
|
} |
|
|
|
const props = withDefaults(defineProps<Props>(), { |
|
// 是否允许滑动 |
|
swipeAble: true, |
|
// 自定义操作数组 |
|
operations: () => [] |
|
}) |
|
|
|
const innerSwipeAble = ref<boolean>(true) // 是否允许滑动 |
|
const clientNum = ref<Record<string, number>>({ x1: 0, x2: 0 }) // 记录开始滑动(x1),结束滑动(x2)的鼠标指针的位置 |
|
const canEmit = ref<boolean>(false) // 是否可以编辑 |
|
const operationWidth = ref<number>(0) // 操作项宽度 |
|
const operating = ref<Operation | null>(null) // 操作中的选项 |
|
|
|
// 侧滑距离 |
|
const movedistance = computed(() => { |
|
let movedistance = '' |
|
if (props.index === props.moveIndex.value) { |
|
const width: number = operationWidth.value || 0 |
|
movedistance = `transform:translateX(-${width}px);` |
|
} else { |
|
movedistance = 'transform:translateX(0px);' |
|
} |
|
return movedistance |
|
}) |
|
|
|
/** |
|
* 操作项类名 |
|
*/ |
|
const operationClass = computed(() => { |
|
return (operation) => { |
|
if (operation.type) { |
|
return `operations-list-item operations-list-item--${operation.type}` |
|
} else { |
|
return 'operations-list-item' |
|
} |
|
} |
|
}) |
|
|
|
/** |
|
* 操作项类确认操作类名 |
|
*/ |
|
const confirmClass = computed(() => { |
|
return (operation) => { |
|
if (operation.type) { |
|
return `operations-list-confirm operations-list-confirm--${operation.type}` |
|
} else { |
|
return 'operations-list-confirm' |
|
} |
|
} |
|
}) |
|
|
|
// 是否可以滑动 |
|
watch( |
|
() => props.swipeAble, |
|
(newVal) => { |
|
innerSwipeAble.value = newVal |
|
} |
|
) |
|
|
|
// 是否可以滑动 |
|
watch( |
|
() => props.moveIndex.value, |
|
(newVal) => { |
|
if (newVal === -1) { |
|
const timer = setTimeout(() => { |
|
operating.value = null |
|
clearTimeout(timer) |
|
}, 300) |
|
} |
|
} |
|
) |
|
|
|
const emit = defineEmits(['click', 'updateIndex', 'operate']) |
|
|
|
function onClick() { |
|
if (canEmit.value) { |
|
/** |
|
* 点击当前组件时触发 |
|
*/ |
|
emit('click') |
|
} |
|
} |
|
|
|
/** |
|
* 操作 |
|
*/ |
|
function doOperate(operation: Operation) { |
|
operating.value = operation |
|
nextTick(() => { |
|
doGetOperation() |
|
}) |
|
if (!operation.confirmNeed) { |
|
/** |
|
* 更新滑动项下标 |
|
* @arg index:Number 被操作项下标 |
|
*/ |
|
emit('updateIndex', -1) |
|
/** |
|
* 点击自定义操作时触发 |
|
* @arg value:String 自定义操作的值 |
|
*/ |
|
emit('operate', operation.value) |
|
} |
|
} |
|
|
|
/** |
|
* 确认 |
|
*/ |
|
function doConfirm(operation) { |
|
emit('updateIndex', -1) |
|
emit('operate', operation.value) |
|
} |
|
|
|
/** |
|
* 触摸开始事件 |
|
* @param event 触摸事件event |
|
*/ |
|
function touchStart(event: TouchEvent) { |
|
if (!innerSwipeAble.value) { |
|
return |
|
} |
|
if (!props.operations.length) { |
|
return |
|
} |
|
canEmit.value = props.index != props.moveIndex.value |
|
if (event.changedTouches.length == 0) { |
|
return |
|
} |
|
const touchs = event.changedTouches[0] |
|
// 记录开始滑动的鼠标位置 |
|
clientNum.value.x1 = touchs.pageX |
|
if (!canEmit.value) { |
|
return |
|
} |
|
emit('updateIndex', -1) |
|
doGetOperation() |
|
} |
|
|
|
/** |
|
* 触摸结束事件 |
|
* @param event 触摸事件event |
|
*/ |
|
function touchEnd(event: TouchEvent) { |
|
if (!innerSwipeAble.value) { |
|
return |
|
} |
|
if (!props.operations.length) { |
|
return |
|
} |
|
if (event.changedTouches.length == 0) { |
|
return |
|
} |
|
const touchs = event.changedTouches[0] |
|
// 记录结束滑动的鼠标位置 |
|
clientNum.value.x2 = touchs.pageX |
|
// 判断滑动距离大于50,判定为滑动成功,否则失败 |
|
if (clientNum.value.x2 < clientNum.value.x1 && Math.abs(clientNum.value.x1) - Math.abs(clientNum.value.x2) > 50) { |
|
event.preventDefault() |
|
emit('updateIndex', props.index) |
|
} else if (clientNum.value.x2 > clientNum.value.x1 && Math.abs(clientNum.value.x2) - Math.abs(clientNum.value.x1) > 10) { |
|
event.preventDefault() |
|
emit('updateIndex', -1) |
|
} |
|
} |
|
|
|
const { proxy } = getCurrentInstance() as any |
|
|
|
/** |
|
* 获取操作项元素 |
|
*/ |
|
function doGetOperation() { |
|
CommonUtil.getRect(`#operations${props.index}`, true, proxy) |
|
.then((result: any) => { |
|
operationWidth.value = result && result[0] && result[0].width ? result[0].width : 0 |
|
}) |
|
.catch((err: any) => {}) |
|
} |
|
</script> |
|
|
|
<style scoped lang="scss"> |
|
.swipe { |
|
border-radius: 8rpx; |
|
overflow-x: hidden; |
|
width: 100%; |
|
text-overflow: ellipsis; |
|
|
|
.li { |
|
position: relative; |
|
background: #fdfdfd; |
|
transform: translateX(0); |
|
transition: all 0.3s; /*滑动效果更生动*/ |
|
} |
|
|
|
.operations { |
|
position: absolute; |
|
top: 0; |
|
left: 100vw; |
|
z-index: 3; |
|
display: flex; |
|
flex-direction: row; |
|
justify-content: center; |
|
align-items: center; |
|
width: auto; |
|
height: 100%; |
|
color: #ffffff; |
|
text-align: center; |
|
.operations-list { |
|
background: #f5f6f7; |
|
padding-right: 24rpx; |
|
height: 100%; |
|
display: flex; |
|
&-item { |
|
font-size: 24rpx; |
|
width: 116rpx; |
|
border-radius: 16rpx; |
|
height: 100%; |
|
color: #fff; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
&-item:not(:last-child) { |
|
margin-right: 8rpx; |
|
} |
|
&-item--error { |
|
background: #f75856; |
|
} |
|
&-item--warn { |
|
background: #fd9b1c; |
|
} |
|
&-item--success { |
|
background: #26c997; |
|
} |
|
&-item--info { |
|
background: #2b93ff; |
|
} |
|
|
|
&-confirm { |
|
font-size: 24rpx; |
|
width: 200rpx; |
|
border-radius: 16rpx; |
|
height: 100%; |
|
color: #fff; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
&-confirm--error { |
|
background: #f75856; |
|
} |
|
&-confirm--warn { |
|
background: #fd9b1c; |
|
} |
|
&-confirm--success { |
|
background: #26c997; |
|
} |
|
&-confirm--info { |
|
background: #2b93ff; |
|
} |
|
} |
|
} |
|
} |
|
</style>
|
|
|