Files
YGChatCS/src/components/SwipeCards/index.vue
duanshuwen 15560574e0 refactor: replace legacy flex utility classes and remove flex.scss
Replace all instances of prefixed flex utility classes (like flex-items-center, flex-justify-between, flex-full, flex-shrink-0) with their standard un-prefixed counterparts (items-center, justify-between, flex-1, shrink-0) across the codebase. Remove the deprecated src/static/scss/flex.scss partial file as it is no longer needed after these updates.
2026-07-25 10:03:01 +08:00

224 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="card pb-[12px] relative mt-[12px]">
<view class="card-item absolute overflow-hidden" v-for="(card, index) in list" :key="card.__uid"
:style="[itemStyle(index, card), transformStyle(index, card)]" @touchstart.stop="touchStart($event, index)"
@touchmove.stop.prevent="touchMove($event, index)" @touchend.stop="touchEnd(index)"
@touchcancel.stop="touchCancel(index)" @transitionend="onTransitionEnd(index)">
<view class="inner-card bg-white">
<!-- 商品大图部分自适应剩余空间 -->
<view class="goods-image-wrapper relative">
<image class="w-full h-full" :src="card.commodityPhoto" mode="aspectFill" />
</view>
<!-- 底部价格 + 购买按钮 -->
<view class="card-footer p-[12px] flex justify-between items-center">
<view class="border-box">
<view class="text-[14px] font-[500] text-[#333333] truncate">
{{ card.commodityName }}
</view>
<view class="card-price-row text-[#333333]">
<text class="text-[11px]"></text>
<text class="text-[24px] font-bold">
{{ card.specificationPrice }}
</text>
<text class="text-[11px] ml-[2px]" v-if="card.stockUnitLabel">/{{ card.stockUnitLabel }}</text>
</view>
</view>
<view class="buy-btn" @click.stop="placeOrderHandle(card)">购买</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from "vue";
import { checkToken } from "@/hooks/useGoLogin";
const props = defineProps({
cardsData: { type: Array, default: () => [] },
});
const DURATION = 300;
const CLICK_THRESHOLD = 8; // 点击判定的最大偏移阈值(像素)
const swipering = ref(false);
const animatingOut = ref(false);
let reorderTimer = null;
const { windowWidth } = uni.getWindowInfo();
let uidCounter = 0;
// 始终生成全局唯一的 __uid避免因重复 key 导致后续卡片无法正确重渲染与绑定事件
const genUid = (item) =>
`swipe_${item?.commodityId ?? "unknown"}_${uidCounter++}_${Date.now()}`;
const normalize = (item) => ({
...item,
__uid: genUid(item),
x: 0,
y: 0,
opacity: 1,
});
// 循环队列全量堆栈仅前3张
const queue = ref((props.cardsData || []).map(normalize));
const list = ref(queue.value.slice(0, 3));
const updateStack = () => {
list.value = queue.value.slice(0, 3);
};
watch(
() => props.cardsData,
(val) => {
queue.value = (val || []).map(normalize);
updateStack();
},
{ deep: true }
);
// 触摸状态
const touchState = ref({
startX: 0,
startY: 0,
moving: false,
isClickCandidate: false,
});
const touchStart = (e, index) => {
if (index !== 0 || animatingOut.value) return;
const t = e.changedTouches?.[0];
if (!t) return;
touchState.value.startX = t.clientX;
touchState.value.startY = t.clientY;
touchState.value.moving = true;
// 初始认为可能是点击,移动过程中如果超过阈值则取消点击
touchState.value.isClickCandidate = true;
swipering.value = true;
};
const touchMove = (e, index) => {
if (index !== 0 || !touchState.value.moving || animatingOut.value) return;
const t = e.changedTouches?.[0];
if (!t) return;
const dx = t.clientX - touchState.value.startX;
const dy = t.clientY - touchState.value.startY;
// 超过点击阈值则标记为不是点击
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
touchState.value.isClickCandidate = false;
}
const top = list.value[0];
if (!top) return;
top.x = dx;
top.y = dy;
};
const finalizeReorder = () => {
const top = queue.value[0];
if (!top) return;
const moved = { ...top, x: 0, y: 0, opacity: 1 };
queue.value = [...queue.value.slice(1), moved];
updateStack();
animatingOut.value = false;
if (reorderTimer) {
clearTimeout(reorderTimer);
reorderTimer = null;
}
};
const touchEnd = (index) => {
if (index !== 0 || !touchState.value.moving) return;
touchState.value.moving = false;
swipering.value = false;
const top = list.value[0];
if (!top) return;
// 若在有效点击范围内,则触发跳转,不进行滑动逻辑
if (touchState.value.isClickCandidate) {
top.x = 0;
top.y = 0;
top.opacity = 1;
uni.navigateTo({
url: `/pages/goods/index?commodityId=${top.commodityId}`,
});
return;
}
const threshold = windowWidth / 4;
if (Math.abs(top.x) > threshold) {
const direction = top.x > 0 ? 1 : -1;
animatingOut.value = true;
top.x = direction * (windowWidth + 100);
top.opacity = 0;
if (reorderTimer) {
clearTimeout(reorderTimer);
reorderTimer = null;
}
reorderTimer = setTimeout(() => {
if (animatingOut.value) finalizeReorder();
}, DURATION + 40);
} else {
// 回弹复位
top.x = 0;
top.y = 0;
top.opacity = 1;
}
};
const touchCancel = (index) => {
if (index !== 0) return;
const top = list.value[0];
if (!top) return;
touchState.value.moving = false;
swipering.value = false;
top.x = 0;
top.y = 0;
top.opacity = 1;
};
const onTransitionEnd = (index) => {
if (index !== 0) return;
if (!animatingOut.value) return;
finalizeReorder();
};
// 栈样式:层级、基础位移缩放、过渡时长
const itemStyle = (index, card) => {
const zIndex = list.value.length - index;
const duration = swipering.value ? "0ms" : `${DURATION}ms`;
const opacity = card.opacity;
return {
zIndex,
opacity,
transition: `transform ${duration} ease, opacity ${duration} ease`,
};
};
// 变换样式:顶部卡动态位移/旋转,后续卡预览层级
const transformStyle = (index, card) => {
if (index === 0) {
const deg = card.x / 20;
return {
transform: `translate3d(${card.x}px, ${card.y}px, 0) rotate(${deg}deg)`,
};
}
// 预览层:轻微位移与缩放,确保连贯顶上
const previewScales = [1, 0.94, 0.86];
const previewOffsets = [0, 18, 39];
const scale = previewScales[index] ?? 0.94;
const y = previewOffsets[index] ?? 24;
return {
transform: `translate3d(0, ${y}px, 0) scale(${scale})`,
};
};
// 去下单
const placeOrderHandle = (item) => {
console.log("去下单", item);
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
};
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>