feat:修复滑动卡片 闪烁的问题
This commit is contained in:
@@ -61,6 +61,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update:modelValue", "change", "didSelectItem"]);
|
||||
|
||||
const DURATION = 280;
|
||||
const RECYCLE_FRAME_DELAY = 32;
|
||||
const CLICK_THRESHOLD = 8;
|
||||
const SWIPE_THRESHOLD = 60;
|
||||
|
||||
@@ -75,12 +76,14 @@ const activeCursor = ref(0);
|
||||
const deltaX = ref(0);
|
||||
const isDragging = ref(false);
|
||||
const isAnimating = ref(false);
|
||||
const isRecycling = ref(false);
|
||||
const isTapCandidate = ref(false);
|
||||
const swipeStep = ref(0);
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let settleTimer = null;
|
||||
let recycleTimer = null;
|
||||
const instance = getCurrentInstance();
|
||||
const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
||||
instance?.vnode.props || {},
|
||||
@@ -89,10 +92,11 @@ const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
||||
|
||||
const actualCount = computed(() => props.list.length);
|
||||
const virtualCount = computed(() => {
|
||||
if (actualCount.value === 1) return 3;
|
||||
if (actualCount.value <= 1) return actualCount.value;
|
||||
if (actualCount.value === 2) return 4;
|
||||
return actualCount.value;
|
||||
});
|
||||
const canSwipe = computed(() => virtualCount.value > 1);
|
||||
const canSwipe = computed(() => actualCount.value > 1);
|
||||
const progress = computed(() => {
|
||||
if (!canSwipe.value) return 0;
|
||||
return clamp(deltaX.value / sideOffset, -1, 1);
|
||||
@@ -113,10 +117,6 @@ const getItemByVirtualIndex = (virtualIndex) => {
|
||||
return props.list[getActualIndex(virtualIndex)] || null;
|
||||
};
|
||||
|
||||
const syncActiveCursor = (incomingIndex = 0) => {
|
||||
activeCursor.value = normalizeIndex(incomingIndex, virtualCount.value);
|
||||
};
|
||||
|
||||
function clearSettleTimer() {
|
||||
if (settleTimer) {
|
||||
clearTimeout(settleTimer);
|
||||
@@ -124,17 +124,61 @@ function clearSettleTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearRecycleTimer() {
|
||||
if (recycleTimer) {
|
||||
clearTimeout(recycleTimer);
|
||||
recycleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const getCircularDistance = (from, to, total) => {
|
||||
const forward = normalizeIndex(to - from, total);
|
||||
const backward = normalizeIndex(from - to, total);
|
||||
return Math.min(forward, backward);
|
||||
};
|
||||
|
||||
const getNearestVirtualIndexByActualIndex = (actualIndex, anchor = activeCursor.value) => {
|
||||
if (!actualCount.value || !virtualCount.value) return 0;
|
||||
|
||||
const targetActualIndex = normalizeIndex(actualIndex, actualCount.value);
|
||||
let matchedIndex = targetActualIndex;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let index = 0; index < virtualCount.value; index += 1) {
|
||||
if (getActualIndex(index) !== targetActualIndex) continue;
|
||||
|
||||
const distance = getCircularDistance(anchor, index, virtualCount.value);
|
||||
if (distance < minDistance) {
|
||||
matchedIndex = index;
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return matchedIndex;
|
||||
};
|
||||
|
||||
const syncActiveCursorByVirtualIndex = (incomingIndex = 0) => {
|
||||
activeCursor.value = normalizeIndex(incomingIndex, virtualCount.value);
|
||||
};
|
||||
|
||||
const syncActiveCursorByActualIndex = (incomingIndex = 0) => {
|
||||
activeCursor.value = getNearestVirtualIndexByActualIndex(incomingIndex);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
clearSettleTimer();
|
||||
clearRecycleTimer();
|
||||
deltaX.value = 0;
|
||||
isDragging.value = false;
|
||||
isAnimating.value = false;
|
||||
const nextCursor = hasExternalModel
|
||||
? props.modelValue
|
||||
: activeCursor.value;
|
||||
syncActiveCursor(nextCursor);
|
||||
isRecycling.value = false;
|
||||
if (hasExternalModel) {
|
||||
syncActiveCursorByActualIndex(props.modelValue);
|
||||
} else {
|
||||
syncActiveCursorByVirtualIndex(activeCursor.value);
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
@@ -144,11 +188,11 @@ watch(
|
||||
(value) => {
|
||||
if (!hasExternalModel) return;
|
||||
if (isAnimating.value || isDragging.value) return;
|
||||
syncActiveCursor(value);
|
||||
syncActiveCursorByActualIndex(value);
|
||||
}
|
||||
);
|
||||
|
||||
const getItemKey = (virtualIndex, role) => {
|
||||
const getItemKey = (virtualIndex) => {
|
||||
const item = getItemByVirtualIndex(virtualIndex) || {};
|
||||
const baseKey =
|
||||
item.id ??
|
||||
@@ -157,7 +201,7 @@ const getItemKey = (virtualIndex, role) => {
|
||||
item.title ??
|
||||
virtualIndex;
|
||||
|
||||
return `${baseKey}-${virtualIndex}-${role}`;
|
||||
return `${baseKey}-${virtualIndex}`;
|
||||
};
|
||||
|
||||
const states = {
|
||||
@@ -256,7 +300,7 @@ const buildCardStyle = (role) => {
|
||||
transform: `translate3d(-50%, 0, 0) translateX(${state.x}px) scale(${state.scale})`,
|
||||
opacity: state.opacity,
|
||||
zIndex: getCardZIndex(role),
|
||||
transition: isDragging.value
|
||||
transition: isDragging.value || isRecycling.value
|
||||
? "none"
|
||||
: `transform ${DURATION}ms ease, opacity ${DURATION}ms ease`,
|
||||
};
|
||||
@@ -287,32 +331,44 @@ const getMaskOpacity = (role) => {
|
||||
|
||||
const getMaskStyle = (role) => ({
|
||||
opacity: getMaskOpacity(role),
|
||||
transition: isDragging.value ? "none" : "opacity 120ms ease-out",
|
||||
transition: isDragging.value || isRecycling.value ? "none" : "opacity 120ms ease-out",
|
||||
});
|
||||
|
||||
const renderSlots = computed(() => {
|
||||
if (!actualCount.value) return [];
|
||||
|
||||
if (!canSwipe.value) {
|
||||
return [
|
||||
{
|
||||
key: getItemKey(activeCursor.value),
|
||||
role: "current",
|
||||
index: getActualIndex(activeCursor.value),
|
||||
item: getItemByVirtualIndex(activeCursor.value),
|
||||
style: buildCardStyle("current"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const prevIndex = normalizeIndex(activeCursor.value - 1, virtualCount.value);
|
||||
const nextIndex = normalizeIndex(activeCursor.value + 1, virtualCount.value);
|
||||
|
||||
return [
|
||||
{
|
||||
key: getItemKey(prevIndex, "prev"),
|
||||
key: getItemKey(prevIndex),
|
||||
role: "prev",
|
||||
index: getActualIndex(prevIndex),
|
||||
item: getItemByVirtualIndex(prevIndex),
|
||||
style: buildCardStyle("prev"),
|
||||
},
|
||||
{
|
||||
key: getItemKey(activeCursor.value, "current"),
|
||||
key: getItemKey(activeCursor.value),
|
||||
role: "current",
|
||||
index: getActualIndex(activeCursor.value),
|
||||
item: getItemByVirtualIndex(activeCursor.value),
|
||||
style: buildCardStyle("current"),
|
||||
},
|
||||
{
|
||||
key: getItemKey(nextIndex, "next"),
|
||||
key: getItemKey(nextIndex),
|
||||
role: "next",
|
||||
index: getActualIndex(nextIndex),
|
||||
item: getItemByVirtualIndex(nextIndex),
|
||||
@@ -336,16 +392,22 @@ const finishSwipe = (step) => {
|
||||
clearSettleTimer();
|
||||
settleTimer = setTimeout(() => {
|
||||
const nextCursor = normalizeIndex(activeCursor.value + step, virtualCount.value);
|
||||
isRecycling.value = true;
|
||||
activeCursor.value = nextCursor;
|
||||
const actualIndex = getActualIndex(nextCursor);
|
||||
emit("update:modelValue", actualIndex);
|
||||
emit("change", actualIndex);
|
||||
resetGesture();
|
||||
clearRecycleTimer();
|
||||
recycleTimer = setTimeout(() => {
|
||||
isRecycling.value = false;
|
||||
recycleTimer = null;
|
||||
}, RECYCLE_FRAME_DELAY);
|
||||
}, DURATION);
|
||||
};
|
||||
|
||||
const handleTouchStart = (event) => {
|
||||
if (!canSwipe.value || isAnimating.value) return;
|
||||
if (!canSwipe.value || isAnimating.value || isRecycling.value) return;
|
||||
|
||||
const touch = event.touches?.[0] || event.changedTouches?.[0];
|
||||
if (!touch) return;
|
||||
@@ -399,20 +461,23 @@ const handleTouchEnd = () => {
|
||||
const handleTouchCancel = () => {
|
||||
if (!canSwipe.value) return;
|
||||
clearSettleTimer();
|
||||
clearRecycleTimer();
|
||||
deltaX.value = 0;
|
||||
isDragging.value = false;
|
||||
isAnimating.value = false;
|
||||
isRecycling.value = false;
|
||||
isTapCandidate.value = false;
|
||||
swipeStep.value = 0;
|
||||
};
|
||||
|
||||
const handleCardTap = (slot) => {
|
||||
if (slot.role !== "current" || isDragging.value || isAnimating.value) return;
|
||||
if (slot.role !== "current" || isDragging.value || isAnimating.value || isRecycling.value) return;
|
||||
emit("didSelectItem", slot.item, slot.index);
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearSettleTimer();
|
||||
clearRecycleTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user