fix:修复bug

This commit is contained in:
2026-03-17 12:03:54 +08:00
parent 5d7b973ddd
commit 60df817de5
32 changed files with 654 additions and 528 deletions
@@ -1,34 +1,32 @@
<template>
<view class="class-bullet-container">
<!-- 第一行可拖 + 自动滚动 -->
<!-- 第一行自动缓慢滚 + 可手动滑动 -->
<view class="scroll-row first-row">
<scroll-view
class="ab-scroll mb-22rpx"
scroll-x
show-scrollbar="false"
id="sv-1"
style="--ab-scroll-timeout: 50s"
:scroll-left="scrollLeft1"
@touchstart="onTouchStart1"
@touchend="onTouchEnd1"
@touchcancel="onTouchEnd1"
@scroll="onScroll1"
>
<view id="sv-inner-1" :class="['sv-inner', { 'paused': isDragging1 }]">
<view class="sv-inner" id="sv1-inner">
<!-- 一份内容 -->
<view
v-for="(item, idx) in categories"
:key="item.id + '-a-' + idx"
:key="item.id + '-row1-a-' + idx"
class="category-item"
@click="handleItemClick(item)"
>
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
<text class="category-text">{{ item.categoryName || item.name }}</text>
</view>
<!-- 第二份重复内容用于无缝循环 -->
<!-- 第二份重复内容用于无缝滚动 -->
<view
v-for="(item, idx) in categories"
:key="item.id + '-b-' + idx"
:key="item.id + '-row1-b-' + idx"
class="category-item"
@click="handleItemClick(item)"
>
@@ -39,24 +37,22 @@
</scroll-view>
</view>
<!-- 第二行可拖动 + 自动滚动速度不同 -->
<!-- 第二行反向数据自动滚动速度不同 -->
<view class="scroll-row">
<scroll-view
class="ab-scroll"
scroll-x
show-scrollbar="false"
id="sv-2"
style="--ab-scroll-timeout: 70s"
:scroll-left="scrollLeft2"
@touchstart="onTouchStart2"
@touchend="onTouchEnd2"
@touchcancel="onTouchEnd2"
@scroll="onScroll2"
>
<view id="sv-inner-2" :class="['sv-inner', { 'paused': isDragging2 }]">
<view class="sv-inner" id="sv2-inner">
<view
v-for="(item, idx) in categoriesReversed"
:key="item.id + '-c-' + idx"
:key="item.id + '-row2-a-' + idx"
class="category-item"
@click="handleItemClick(item)"
>
@@ -65,7 +61,7 @@
</view>
<view
v-for="(item, idx) in categoriesReversed"
:key="item.id + '-d-' + idx"
:key="item.id + '-row2-b-' + idx"
class="category-item"
@click="handleItemClick(item)"
>
@@ -80,7 +76,7 @@
</template>
<script setup lang="ts">
import { computed, ref, onMounted, nextTick, getCurrentInstance } from 'vue'
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
// 定义分类项接口(与模板字段一致)
interface CategoryItem {
@@ -88,7 +84,7 @@ interface CategoryItem {
categoryImage?: string
logoUrl?: string
name?: string
categoryName: string
categoryName?: string
}
// 定义组件属性
@@ -128,116 +124,121 @@ const categoriesReversed = computed(() => {
return list.slice().reverse()
})
// 自动滚动(CSS 动画)+ 手动拖动时暂停
const isDragging1 = ref(false)
const isDragging2 = ref(false)
const RESUME_DELAY_MS = 1200
let resumeTimer1: any
let resumeTimer2: any
// ===== 自动滚动相关(电商标签推荐风格) =====
// 视口宽度,用于计算最大可滚动距离(scrollWidth - viewportWidth
const systemInfo = uni.getSystemInfoSync()
const viewportWidth = systemInfo.windowWidth || 0
// 中心定位与边界重置,避免滚动到底卡住
const scrollLeft1 = ref(0)
const scrollLeft2 = ref(0)
const copyWidth1Px = ref(0)
const copyWidth2Px = ref(0)
const viewportWidth1Px = ref(0)
const viewportWidth2Px = ref(0)
function measureAndInit() {
const ins = getCurrentInstance()
const q = uni.createSelectorQuery().in(ins?.proxy as any)
q.select('#sv-1').boundingClientRect()
.select('#sv-inner-1').boundingClientRect()
.select('#sv-2').boundingClientRect()
.select('#sv-inner-2').boundingClientRect()
.exec((res) => {
const sv1 = res?.[0]
const inner1 = res?.[1]
const sv2 = res?.[2]
const inner2 = res?.[3]
if (sv1?.width) viewportWidth1Px.value = sv1.width
if (inner1?.width) copyWidth1Px.value = inner1.width / 2
if (sv2?.width) viewportWidth2Px.value = sv2.width
if (inner2?.width) copyWidth2Px.value = inner2.width / 2
// 初始化到中间位置
if (copyWidth1Px.value) scrollLeft1.value = copyWidth1Px.value
if (copyWidth2Px.value) scrollLeft2.value = copyWidth2Px.value
})
// 最大可滚动距离(由 scroll 事件提供,避免自己算)
const maxScroll1 = ref(0)
const maxScroll2 = ref(0)
const isTouching1 = ref(false)
const isTouching2 = ref(false)
let autoTimer1: any = null
let autoTimer2: any = null
let resumeTimer1: any = null
let resumeTimer2: any = null
// 速度和间隔可以根据效果微调
const AUTO_INTERVAL = 30
const AUTO_STEP_1 = 0.6
const AUTO_STEP_2 = 0.4
const RESUME_DELAY = 800
const RESET_THRESHOLD = 2 // 在距离末尾一定范围内就重置
function startAuto1() {
if (autoTimer1) return
autoTimer1 = setInterval(() => {
if (isTouching1.value) return
let next = scrollLeft1.value + AUTO_STEP_1
const max = maxScroll1.value
if (max > 0 && next >= max - RESET_THRESHOLD) {
// 距离最右侧很近时,从头开始,实现循环
next = 0
}
scrollLeft1.value = next
}, AUTO_INTERVAL)
}
function recenter1(currentLeft?: number) {
const cw = copyWidth1Px.value
const vw = viewportWidth1Px.value
if (!cw || !vw) return
const left = currentLeft ?? scrollLeft1.value
const mod = left % cw
scrollLeft1.value = cw + mod
function startAuto2() {
if (autoTimer2) return
autoTimer2 = setInterval(() => {
if (isTouching2.value) return
let next = scrollLeft2.value + AUTO_STEP_2
const max = maxScroll2.value
if (max > 0 && next >= max - RESET_THRESHOLD) {
next = 0
}
scrollLeft2.value = next
}, AUTO_INTERVAL)
}
function recenter2(currentLeft?: number) {
const cw = copyWidth2Px.value
const vw = viewportWidth2Px.value
if (!cw || !vw) return
const left = currentLeft ?? scrollLeft2.value
const mod = left % cw
scrollLeft2.value = cw + mod
function stopAuto1() {
if (autoTimer1) {
clearInterval(autoTimer1)
autoTimer1 = null
}
}
function stopAuto2() {
if (autoTimer2) {
clearInterval(autoTimer2)
autoTimer2 = null
}
}
function onTouchStart1() {
isDragging1.value = true
isTouching1.value = true
if (resumeTimer1) clearTimeout(resumeTimer1)
stopAuto1()
}
function onTouchEnd1() {
// 手指松开后,若在边界附近,立即重置到中间位置,保持无缝
recenter1(scrollLeft1.value)
resumeTimer1 = setTimeout(() => {
isDragging1.value = false
}, RESUME_DELAY_MS)
isTouching1.value = false
startAuto1()
}, RESUME_DELAY)
}
function onScroll1(e: any) {
isDragging1.value = true
if (resumeTimer1) clearTimeout(resumeTimer1)
const left = e?.detail?.scrollLeft ?? 0
const cw = copyWidth1Px.value
const vw = viewportWidth1Px.value
if (cw && vw) {
const total = cw * 2
// 临界阈值,靠近起点或终点时重置
const threshold = 10
if (left <= threshold || left >= total - vw - threshold) {
recenter1(left)
const detail = e?.detail
if (detail && typeof detail.scrollLeft === 'number') {
scrollLeft1.value = detail.scrollLeft
// scrollWidth - viewportWidth 为最大可滚动距离
if (typeof detail.scrollWidth === 'number' && viewportWidth > 0) {
const max = detail.scrollWidth - viewportWidth
maxScroll1.value = max > 0 ? max : 0
}
}
resumeTimer1 = setTimeout(() => {
isDragging1.value = false
}, RESUME_DELAY_MS)
}
function onTouchStart2() {
isDragging2.value = true
isTouching2.value = true
if (resumeTimer2) clearTimeout(resumeTimer2)
stopAuto2()
}
function onTouchEnd2() {
recenter2(scrollLeft2.value)
resumeTimer2 = setTimeout(() => {
isDragging2.value = false
}, RESUME_DELAY_MS)
isTouching2.value = false
startAuto2()
}, RESUME_DELAY)
}
function onScroll2(e: any) {
isDragging2.value = true
if (resumeTimer2) clearTimeout(resumeTimer2)
const left = e?.detail?.scrollLeft ?? 0
const cw = copyWidth2Px.value
const vw = viewportWidth2Px.value
if (cw && vw) {
const total = cw * 2
const threshold = 10
if (left <= threshold || left >= total - vw - threshold) {
recenter2(left)
const detail = e?.detail
if (detail && typeof detail.scrollLeft === 'number') {
scrollLeft2.value = detail.scrollLeft
if (typeof detail.scrollWidth === 'number' && viewportWidth > 0) {
const max = detail.scrollWidth - viewportWidth
maxScroll2.value = max > 0 ? max : 0
}
}
resumeTimer2 = setTimeout(() => {
isDragging2.value = false
}, RESUME_DELAY_MS)
}
// 点击处理
@@ -252,9 +253,20 @@ function navigateTo(url: string) {
onMounted(() => {
nextTick(() => {
measureAndInit()
// 启动自动滚动,maxScroll 值会在第一次 scroll 事件时更新
setTimeout(() => {
startAuto1()
startAuto2()
}, 300)
})
})
onUnmounted(() => {
stopAuto1()
stopAuto2()
if (resumeTimer1) clearTimeout(resumeTimer1)
if (resumeTimer2) clearTimeout(resumeTimer2)
})
</script>
<style lang="scss" scoped>
@@ -280,8 +292,6 @@ onMounted(() => {
display: flex;
flex-direction: row;
gap: 20rpx;
animation: ab-move-marquee var(--ab-scroll-timeout) linear infinite;
will-change: transform;
}
.category-item {
@@ -316,17 +326,5 @@ onMounted(() => {
}
}
// GPU 动画,无缝循环(两份内容)
@keyframes ab-move-marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.paused {
animation-play-state: paused;
}
// 取消 CSS 动画,由 JS 控制的 scroll-left 实现无缝滚动,避免与手势滚动冲突导致的抖动
</style>
@@ -24,7 +24,7 @@ function handleClickFood(item: any) {
<view class="text-24rpx lh-24rpx flex items-center mt-12rpx">
<text class="text-#333 font-500">{{ item.rating }}</text>
<image src="@img/chef/124.png" class="w-24rpx h-24rpx mx-4rpx mt-2rpx"></image>
<text class="text-#7D7D7D">({{ item.commentCount }}) {{ item.deliveryTime }} {{ t('common.minutes') }}</text>
<text class="text-#7D7D7D">({{ item.commentCount }}) {{ item.deliveryTime }} {{ Number(item.deliveryTime) === 1 ? t('common.day') : t('common.days') }}</text>
</view>
</view>
</template>
@@ -7,12 +7,24 @@
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<script setup lang="ts">
// @ts-ignore - Vue SFC default export is provided by tooling
import Collection from "@/components/collection/index.vue";
import {appCollectCollectPost} from "@/service";
import {CollectionType} from "@/constant/enums";
const { t } = useI18n();
type FoodBoxItem = {
id: number | string;
merchantId?: string;
isCollect?: boolean;
dishImage?: string;
logo?: string;
dishName?: string;
merchantName?: string;
originalPrice?: string | number;
[key: string]: any;
};
const props = defineProps<{
item: object;
item: FoodBoxItem;
}>();
function handleClickFood() {
@@ -34,10 +46,11 @@ if(props.item.merchantId){
}
function handleCollectionChange(value: boolean) {
const isDish = !!props.item?.merchantId;
appCollectCollectPost({
body: {
targetId: props.item.id,
targetType: CollectionType.STORE
targetId: String(props.item.id),
targetType: isDish ? CollectionType.DISH : CollectionType.STORE
}
}).then(res=> {
props.item.isCollect = value;
@@ -69,7 +82,7 @@ function handleCollectionChange(value: boolean) {
<!-- <text class="text-#7D7D7D">({{ item.commentCount }}) {{ item.deliveryTime }}{{ t('common.minutes') }}</text> -->
</view>
</view>
<collection :is-collected="item.isCollect" @collectionChange="handleCollectionChange" />
<collection :is-collected="item.isCollect" @collection-change="handleCollectionChange" />
</view>
</view>
</template>
@@ -25,7 +25,7 @@ function handleClickFood(item: any) {
<view class="w-156rpx text-center line-clamp-1 text-#333 text-28rpx lh-28rpx mt-12rpx font-500">
{{ item.merchantName }}
</view>
<view v-if="+item.deliveryService === 1" class="mt-12rpx text-center text-24rpx lh-24rpx text-#7D7D7D">{{ item.deliveryTime }}{{ t('common.minutes') }}</view>
<view v-if="+item.deliveryService === 1" class="mt-12rpx text-center text-24rpx lh-24rpx text-#7D7D7D">{{ item.deliveryTime }}{{ Number(item.deliveryTime) === 1 ? t('common.day') : t('common.days') }}</view>
</view>
</template>
<view class="shrink-0 w-30rpx op-0">1</view>
@@ -392,7 +392,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
<view class="flex-center-sb mt-12rpx">
<view class="text-28rpx text-#999">
<view class="line-through">US${{ item?.originalPrice }}</view>
<!-- <view>{{ t('pages-store.store.sales') }}:{{ item?.salesCount }}</view> -->
<view>{{ t('pages-store.store.sales') }}:{{ item?.salesCount }}</view>
</view>
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">