272 行
6.8 KiB
Plaintext
272 行
6.8 KiB
Plaintext
<template>
|
|
<view class="card" ref="card" @touchstart="touchstart($event as TouchEvent)"
|
|
@touchmove="touchmove($event as TouchEvent)" @touchend="touchend" @touchcancel="touchend">
|
|
<image class="card-img" ref="cardImg" :src="img"></image>
|
|
<view class="state">
|
|
<image class="state-icon like" ref="stateIconLike" src="/static/template/drop-card/like.png" mode="widthFix">
|
|
</image>
|
|
<image class="state-icon dislike" ref="stateIconDislike" src="/static/dislike.png"
|
|
mode="widthFix"></image>
|
|
<!-- cardIndex:{{cardIndex}} -->
|
|
</view>
|
|
</view>
|
|
</template>
|
|
<script setup lang="uts">
|
|
/**
|
|
* @description: 作为卡片堆叠中的单张卡片,支持拖拽跟手、状态提示和划走动画。
|
|
* @logic: 记录手势起点后实时更新卡片位移与旋转角度,松手时根据水平位移判断回弹或飞出,并通知父级更新堆叠顺序。
|
|
* @optimization: 通过缓存模板元素引用并直接更新 transform 和 opacity,降低拖拽过程中的样式计算与节点查询开销。
|
|
*/
|
|
// Template refs
|
|
const card = ref<UniElement | null>(null)
|
|
const cardImg = ref<UniElement | null>(null)
|
|
const stateIconLike = ref<UniElement | null>(null)
|
|
const stateIconDislike = ref<UniElement | null>(null)
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
img: {
|
|
type: String,
|
|
default: "https://web-ext-storage.dcloud.net.cn/hello-uni-app-x/drop-card-1.jpg"
|
|
},
|
|
cardIndex: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
})
|
|
|
|
// Module-level variables
|
|
let sX : number = 0
|
|
let sY : number = 0
|
|
let screenWidth : number = 1
|
|
let screenHeight : number = 1
|
|
let floating : boolean = false
|
|
let touchstartAfter : boolean = false
|
|
|
|
// Data
|
|
const $elementMap = new Map<string, UniElement>()
|
|
const x = ref(0 as number)
|
|
const y = ref(0 as number)
|
|
let floatCount = 0
|
|
|
|
// Computed
|
|
const movePercent = computed(() : number => {
|
|
return Math.abs(x.value) / (screenWidth / 2 * 3)
|
|
})
|
|
|
|
const likeOpacity = computed(() : number => {
|
|
return x.value < 0 ? 0 : movePercent.value * 100
|
|
})
|
|
|
|
const dislikeOpacity = computed(() : number => {
|
|
return x.value > 0 ? 0 : movePercent.value * 100
|
|
})
|
|
|
|
// Methods
|
|
function setElementStyle(refName : string, propertyName : string, propertyStyle : any) : void {
|
|
let element : UniElement | null = $elementMap.get(refName)
|
|
if (element == null) {
|
|
// Map ref name to corresponding ref variable
|
|
if (refName == 'card') {
|
|
element = card.value
|
|
} else if (refName == 'card-img') {
|
|
element = cardImg.value
|
|
} else if (refName == 'state-icon-like') {
|
|
element = stateIconLike.value
|
|
} else if (refName == 'state-icon-dislike') {
|
|
element = stateIconDislike.value
|
|
}
|
|
|
|
if (element != null) {
|
|
$elementMap.set(refName, element)
|
|
}
|
|
}
|
|
|
|
if (element != null) {
|
|
element.style.setProperty(propertyName, propertyStyle.toString())
|
|
}
|
|
}
|
|
|
|
let _index = 0
|
|
function initCardStyle() {
|
|
_index = (props.cardIndex + floatCount) % 3
|
|
// TODO 临时处理 setProperty设置z-index无效的问题
|
|
setElementStyle('card', 'z-index', _index.toString())
|
|
setElementStyle('card', 'margin-top', screenHeight * 0.15 - 30 * _index + 'px')
|
|
setElementStyle('card', 'transform', 'scale(' + (0.9 + 0.05 * _index) + ')')
|
|
}
|
|
|
|
function moveCard() {
|
|
if (_index != 2){
|
|
// console.log('不是顶层卡片不允许移动')
|
|
return
|
|
}
|
|
setElementStyle("card",
|
|
'transform',
|
|
`translate(${x.value}px,${y.value}px) rotate(${x.value / -30}deg) scale(1)`
|
|
)
|
|
setElementStyle("state-icon-like", 'opacity', x.value < 0 ? 0 : movePercent.value * 10)
|
|
setElementStyle("state-icon-dislike", 'opacity', x.value > 0 ? 0 : movePercent.value * 10)
|
|
}
|
|
|
|
function touchstart(e : TouchEvent) {
|
|
e.preventDefault()
|
|
if (floating) {
|
|
return
|
|
}
|
|
sX = e.touches[0].screenX
|
|
sY = e.touches[0].screenY
|
|
x.value = 0
|
|
y.value = 0
|
|
touchstartAfter = true
|
|
}
|
|
|
|
function touchmove(e : TouchEvent) {
|
|
e.preventDefault()
|
|
if (!touchstartAfter || floating) {
|
|
return
|
|
}
|
|
x.value += e.touches[0].screenX - sX
|
|
y.value += e.touches[0].screenY - sY
|
|
sX = e.touches[0].screenX
|
|
sY = e.touches[0].screenY
|
|
moveCard()
|
|
}
|
|
|
|
function touchend() {
|
|
touchstartAfter = false
|
|
if (floating) {
|
|
return
|
|
}
|
|
floating = true
|
|
|
|
let k : number = 0
|
|
if (x.value > screenWidth / 10) {
|
|
k = 1
|
|
} else if (x.value < screenWidth * -1 / 10) {
|
|
k = -1
|
|
}
|
|
|
|
function cardTo(targetX : number, targetY : number, callback : () => void, speed : number = 10) {
|
|
let interval : number = 0
|
|
let acceleration : number = 1
|
|
interval = setInterval(() => {
|
|
acceleration += 0.2
|
|
|
|
const dx = targetX - x.value
|
|
if (Math.abs(dx) < 1) {
|
|
x.value = targetX
|
|
} else {
|
|
x.value += dx / speed * acceleration
|
|
}
|
|
|
|
const dy = targetY - y.value
|
|
if (Math.abs(dy) < 1) {
|
|
y.value = targetY
|
|
} else {
|
|
y.value += dy / speed * acceleration
|
|
}
|
|
|
|
moveCard()
|
|
if (x.value == targetX && y.value == targetY) {
|
|
clearInterval(interval)
|
|
callback()
|
|
}
|
|
}, 16)
|
|
}
|
|
|
|
if (Math.floor(k) != 0) {
|
|
cardTo(k * screenWidth * 1.3, y.value * 3, () => {
|
|
setElementStyle("state-icon-like", 'opacity', 0)
|
|
setElementStyle("state-icon-dislike", 'opacity', 0)
|
|
setElementStyle("card", 'opacity', 0)
|
|
setTimeout(() => {
|
|
setElementStyle("card", 'opacity', 1)
|
|
}, 300)
|
|
uni.$emit('uni-drop-card-float', null)
|
|
floating = false
|
|
}, 8)
|
|
} else {
|
|
const _x : number = x.value
|
|
const _y : number = y.value
|
|
cardTo(Math.floor(_x * -0.05), Math.floor(_y * -0.05), () => {
|
|
cardTo(0, 0, () => {
|
|
console.log('bounce')
|
|
floating = false
|
|
}, 30)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
screenWidth = uni.getWindowInfo().screenWidth
|
|
screenHeight = uni.getWindowInfo().screenHeight
|
|
|
|
setTimeout(() => {
|
|
setElementStyle('card', 'height', screenHeight * 0.7 + 'px')
|
|
setElementStyle('card-img', 'height', screenHeight * 0.7 + 'px')
|
|
initCardStyle()
|
|
}, 200)
|
|
|
|
uni.$on('uni-drop-card-float', () => {
|
|
floatCount++
|
|
initCardStyle()
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
uni.$off('uni-drop-card-float')
|
|
})
|
|
|
|
defineOptions({
|
|
options: {
|
|
virtualHost: true
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.card {
|
|
width:95%;
|
|
height: 0px;
|
|
position: absolute;
|
|
top: 0px;
|
|
/* left: 0px; */
|
|
margin: 0 12px;
|
|
margin-top: 50px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
|
background-color: #FFF;
|
|
transition: margin-top 300ms;
|
|
transition-timing-function: ease-in;
|
|
}
|
|
|
|
.card-img {
|
|
width: 100%;
|
|
height: 0px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.state {
|
|
top: 10px;
|
|
left: 10px;
|
|
width: 86%;
|
|
padding: 4px;
|
|
position: absolute;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.state-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border: 1px solid #FFF;
|
|
background-color: #FFF;
|
|
padding: 3px;
|
|
border-radius: 100px;
|
|
box-shadow: 0 0 1px #EBEBEB;
|
|
opacity: 0;
|
|
}
|
|
</style>
|