chore: flatten web project structure

This commit is contained in:
duanshuwen
2026-05-26 12:14:31 +08:00
parent 30b9100b4a
commit aff380dad9
569 changed files with 1081 additions and 74985 deletions

View File

@@ -1,231 +0,0 @@
<template>
<view class="card border-box pb-12 relative mt-12">
<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 border-box p-12 flex flex-justify-between flex-items-center">
<view class="border-box">
<view class="font-size-14 font-500 color-333 ellipsis-1">
{{ card.commodityName }}
</view>
<view class="card-price-row color-333">
<text class="font-size-11"></text>
<text class="font-size-24 font-bold">
{{ card.specificationPrice }}
</text>
<text class="font-size-11 ml-2" 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>