Files
nianxx-h5/src/components/SwipeCards/index.vue
duanshuwen 1a5a2ae6a9 refactor: clean up codebase and add new features
Replace SCSS variable usages with explicit pixel/hex values for consistent styling across all components
Fix broken template syntax including missing class spaces and incorrect closing tags
Migrate constant and API imports to centralized @/constants and @/api modules
Add new utility classes: IdUtils, CallbackUtils, and TimerUtils
Add new chat conversation API endpoints for recent conversations and message lists
Add new Discovery page components (FindTabs, QuickQuestions, CardSwiper) and their styles
Update app store config to use environment variables for base API and WebSocket URLs
Add new selected tab icon assets
2026-05-26 23:50:37 +08:00

225 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>
<div class="card border-box pb-12 relative mt-12">
<div 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)">
<div class="inner-card bg-white">
<!-- 商品大图部分自适应剩余空间 -->
<div class="goods-image-wrapper relative">
<img class="w-full h-full" :src="card.commodityPhoto" mode="aspectFill" />
</div>
<!-- 底部价格 + 购买按钮 -->
<div class="card-footer border-box p-12 flex flex-justify-between flex-items-center">
<div class="border-box">
<div class="font-size-14 font-500 color-333 ellipsis-1">
{{ card.commodityName }}
</div>
<div class="card-price-row color-333">
<span class="font-size-11"></span>
<span class="font-size-24 font-bold">
{{ card.specificationPrice }}
</span>
<span class="font-size-11 ml-2" v-if="card.stockUnitLabel">/{{ card.stockUnitLabel }}</span>
</div>
</div>
<div class="buy-btn" @click.stop="placeOrderHandle(card)">购买</div>
</div>
</div>
</div>
</div>
</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 predivScales = [1, 0.94, 0.86];
const predivOffsets = [0, 18, 39];
const scale = predivScales[index] ?? 0.94;
const y = predivOffsets[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>