滚动
This commit is contained in:
1 parent
47aa0e7ba8
commit
f726bb11fb
1 file changed
+253
-63
@@ -38,57 +38,120 @@
|
|||||||
<div class="h-full w-full rounded-full bg-gradient-to-r from-transparent via-[#4a88ff]/30 to-transparent"></div>
|
<div class="h-full w-full rounded-full bg-gradient-to-r from-transparent via-[#4a88ff]/30 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 全屏按钮 -->
|
<!-- 全屏按钮和滚动控制按钮 -->
|
||||||
<el-tooltip :content="isFullscreen ? '退出全屏' : '全屏'" placement="bottom" effect="dark">
|
<div class="absolute right-0 flex items-center gap-2">
|
||||||
<button
|
<el-tooltip v-if="needScroll" :content="isScrolling ? '暂停滚动' : '开始滚动'" placement="bottom" effect="dark">
|
||||||
@click="toggleFullScreen"
|
<button
|
||||||
class="absolute right-0 flex h-8 w-8 items-center justify-center rounded-xl bg-[#4a88ff]/20 transition-all hover:scale-110 hover:bg-[#4a88ff]/40"
|
@click="toggleScroll"
|
||||||
>
|
class="flex h-8 w-8 items-center justify-center rounded-xl bg-[#4a88ff]/20 transition-all hover:scale-110 hover:bg-[#4a88ff]/40"
|
||||||
<el-icon class="icon h-10 w-10 font-bold bg-contain bg-center bg-no-repeat"><FullScreen /></el-icon>
|
>
|
||||||
</button>
|
<el-icon class="h-10 w-10 font-bold">
|
||||||
</el-tooltip>
|
<component :is="isScrolling ? 'VideoPause' : 'VideoPlay'" />
|
||||||
|
</el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip :content="isFullscreen ? '退出全屏' : '全屏'" placement="bottom" effect="dark">
|
||||||
|
<button
|
||||||
|
@click="toggleFullScreen"
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-xl bg-[#4a88ff]/20 transition-all hover:scale-110 hover:bg-[#4a88ff]/40"
|
||||||
|
>
|
||||||
|
<el-icon class="icon h-10 w-10 font-bold bg-contain bg-center bg-no-repeat"><FullScreen /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 菜品展示区域 -->
|
<!-- 菜品展示区域 -->
|
||||||
<div class="flex-1 px-4" :style="containerStyle">
|
<div class="flex-1 px-4" :style="containerStyle">
|
||||||
<div class="grid h-full w-full gap-3" :style="gridStyle">
|
<div class="scroll-wrapper" ref="scrollContainerRef">
|
||||||
<template v-for="hourType in foodData" :key="hourType">
|
<div class="scroll-content" :style="{ transform: `translateY(${-scrollOffset}px)` }">
|
||||||
<div
|
<div class="grid w-full gap-3" :style="gridStyle">
|
||||||
v-for="dish in hourType"
|
<template v-for="hourType in foodData" :key="hourType">
|
||||||
:key="dish.id"
|
<div
|
||||||
class="group relative flex flex-col overflow-hidden rounded-xl border border-[#4a88ff]/10 bg-[#1d2b45]/60 transition-all duration-500 hover:scale-[1.02] hover:-translate-y-1 hover:bg-[#1d2b45]/80 hover:shadow-[0_0_20px_rgba(74,136,255,0.1)]"
|
v-for="dish in hourType"
|
||||||
>
|
:key="dish.id"
|
||||||
<div class="relative h-32 overflow-hidden">
|
class="group relative flex flex-col overflow-hidden rounded-xl border border-[#4a88ff]/10 bg-[#1d2b45]/60 transition-all duration-500 hover:scale-[1.02] hover:-translate-y-1 hover:bg-[#1d2b45]/80 hover:shadow-[0_0_20px_rgba(74,136,255,0.1)]"
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[#1d2b45]/90"></div>
|
>
|
||||||
<img
|
<div class="relative h-32 overflow-hidden">
|
||||||
:src="imageBaseUrl + dish.picUrl"
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[#1d2b45]/90"></div>
|
||||||
:alt="dish.name"
|
<img
|
||||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
:src="imageBaseUrl + dish.picUrl"
|
||||||
/>
|
:alt="dish.name"
|
||||||
<div class="absolute left-2 top-2 rounded-lg">
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
<dict-tag
|
/>
|
||||||
:options="daily_menu"
|
<div class="absolute left-2 top-2 rounded-lg">
|
||||||
:value="canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.categoryType?.split(',')"
|
<!-- <dict-tag
|
||||||
/>
|
:options="daily_menu"
|
||||||
|
:value="canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.categoryType?.split(',')"
|
||||||
|
/> -->
|
||||||
|
<el-tag type="primary">午餐</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-1 top-2 rounded-lg px-2 py-0.5 text-sm">
|
||||||
|
<el-tag type="primary">
|
||||||
|
{{ canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.name }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-between gap-1 bg-[#1d2b45]/40 p-3 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-2xl font-bold text-[#8ab4ff] drop-shadow-lg transition-colors group-hover:text-white line-clamp-1">
|
||||||
|
{{ dish.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-3xl font-bold text-[#4a88ff] drop-shadow-lg whitespace-nowrap ml-4">
|
||||||
|
¥{{ displayPrice(dish.price) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="text-base text-[#8ab4ff]/80 line-clamp-1">{{ dish.name }}</span> -->
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-px rounded-xl border border-[#4a88ff]/10 pointer-events-none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute right-1 top-2 rounded-lg px-2 py-0.5 text-sm">
|
</template>
|
||||||
<el-tag type="primary">
|
|
||||||
{{ canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.name }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between gap-1 bg-[#1d2b45]/40 p-3 backdrop-blur-sm">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="text-2xl font-bold text-[#8ab4ff] drop-shadow-lg transition-colors group-hover:text-white line-clamp-1">
|
|
||||||
{{ dish.name }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-3xl font-bold text-[#4a88ff] drop-shadow-lg whitespace-nowrap ml-4">¥{{ displayPrice(dish.price) }}</p>
|
|
||||||
</div>
|
|
||||||
<!-- <span class="text-base text-[#8ab4ff]/80 line-clamp-1">{{ dish.name }}</span> -->
|
|
||||||
</div>
|
|
||||||
<div class="absolute inset-px rounded-xl border border-[#4a88ff]/10 pointer-events-none"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<!-- 只在需要滚动时显示克隆内容 -->
|
||||||
|
<div v-if="needScroll" class="grid w-full gap-3" :style="gridStyle">
|
||||||
|
<template v-for="hourType in foodData" :key="hourType + '_clone'">
|
||||||
|
<div
|
||||||
|
v-for="dish in hourType"
|
||||||
|
:key="dish.id"
|
||||||
|
class="group relative flex flex-col overflow-hidden rounded-xl border border-[#4a88ff]/10 bg-[#1d2b45]/60 transition-all duration-500 hover:scale-[1.02] hover:-translate-y-1 hover:bg-[#1d2b45]/80 hover:shadow-[0_0_20px_rgba(74,136,255,0.1)]"
|
||||||
|
>
|
||||||
|
<div class="relative h-32 overflow-hidden">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[#1d2b45]/90"></div>
|
||||||
|
<img
|
||||||
|
:src="imageBaseUrl + dish.picUrl"
|
||||||
|
:alt="dish.name"
|
||||||
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
|
/>
|
||||||
|
<div class="absolute left-2 top-2 rounded-lg">
|
||||||
|
<!-- <dict-tag
|
||||||
|
:options="daily_menu"
|
||||||
|
:value="canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.categoryType?.split(',')"
|
||||||
|
/> -->
|
||||||
|
<el-tag type="primary">午餐</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-1 top-2 rounded-lg px-2 py-0.5 text-sm">
|
||||||
|
<el-tag type="primary">
|
||||||
|
{{ canteenCategoryOption?.find((v: any) => v.id == dish.categoryId)?.name }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-between gap-1 bg-[#1d2b45]/40 p-3 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-2xl font-bold text-[#8ab4ff] drop-shadow-lg transition-colors group-hover:text-white line-clamp-1">
|
||||||
|
{{ dish.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-3xl font-bold text-[#4a88ff] drop-shadow-lg whitespace-nowrap ml-4">
|
||||||
|
¥{{ displayPrice(dish.price) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="text-base text-[#8ab4ff]/80 line-clamp-1">{{ dish.name }}</span> -->
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-px rounded-xl border border-[#4a88ff]/10 pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,13 +167,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from "vue"
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from "vue"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { ElTooltip } from "element-plus"
|
import { ElTooltip } from "element-plus"
|
||||||
import { selectTimeFoodApi } from "/@/api/canteen/canteenDailyMenu"
|
import { selectTimeFoodApi } from "/@/api/canteen/canteenDailyMenu"
|
||||||
import { getAllSimple as getCanteenCategory } from "/@/api/canteen/canteenCategory"
|
import { getAllSimple as getCanteenCategory } from "/@/api/canteen/canteenCategory"
|
||||||
import { useDict } from "/@/hooks/dict"
|
import { useDict } from "/@/hooks/dict"
|
||||||
import { displayPrice } from "/@/utils/formatMoney"
|
import { displayPrice } from "/@/utils/formatMoney"
|
||||||
|
import { VideoPause, VideoPlay } from "@element-plus/icons-vue"
|
||||||
|
|
||||||
const { daily_menu } = useDict("daily_menu")
|
const { daily_menu } = useDict("daily_menu")
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
@@ -151,23 +215,22 @@ const foodData = reactive<{
|
|||||||
const getDishByType = async () => {
|
const getDishByType = async () => {
|
||||||
const result = await selectTimeFoodApi({
|
const result = await selectTimeFoodApi({
|
||||||
// time: dayjs().format("YYYY-MM-DD"),
|
// time: dayjs().format("YYYY-MM-DD"),
|
||||||
// time: "2025-03-14",
|
time: "2025-03-14",
|
||||||
// time: "2025-03-16",
|
// time: "2025-02-07",
|
||||||
time: "2025-02-07",
|
|
||||||
merchantId: "1887400193644199937",
|
merchantId: "1887400193644199937",
|
||||||
hourType: 2,
|
// hourType: 2,
|
||||||
})
|
})
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
result.data.forEach((r: any) => {
|
result.data.forEach((r: any) => {
|
||||||
switch (r.hourType) {
|
switch (r.hourType) {
|
||||||
case 1:
|
case 1:
|
||||||
foodData.breakfast = r.dailyMenuVOS
|
// foodData.breakfast = r.dailyMenuVOS
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
foodData.lunch = r.dailyMenuVOS
|
foodData.lunch = r.dailyMenuVOS.filter((v: any, index: number) => index < 13)
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
foodData.dinner = r.dailyMenuVOS
|
// foodData.dinner = r.dailyMenuVOS
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -180,6 +243,91 @@ const updateTime = () => {
|
|||||||
currentTime.value = dayjs().format("HH:mm:ss")
|
currentTime.value = dayjs().format("HH:mm:ss")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
let scrollOffset = ref(0)
|
||||||
|
let isScrolling = ref(true)
|
||||||
|
let lastTimestamp = 0
|
||||||
|
const SCROLL_SPEED = 0.5
|
||||||
|
let animationFrame: number | null = null
|
||||||
|
|
||||||
|
// 添加一个计算属性来判断是否需要滚动
|
||||||
|
const needScroll = ref(false)
|
||||||
|
|
||||||
|
// 优化后的滚动处理函数
|
||||||
|
const handleScroll = (timestamp: number) => {
|
||||||
|
if (!isScrolling.value) return
|
||||||
|
|
||||||
|
if (!lastTimestamp) {
|
||||||
|
lastTimestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = timestamp - lastTimestamp
|
||||||
|
lastTimestamp = timestamp
|
||||||
|
|
||||||
|
const container = scrollContainerRef.value
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 计算新的滚动偏移量
|
||||||
|
scrollOffset.value += SCROLL_SPEED * (deltaTime / 16)
|
||||||
|
|
||||||
|
const contentHeight = container.scrollHeight / 2 // 单个内容的高度
|
||||||
|
|
||||||
|
// 当滚动超过一个内容高度时,减去一个内容高度,造成无缝衔接的效果
|
||||||
|
if (scrollOffset.value >= contentHeight) {
|
||||||
|
scrollOffset.value -= contentHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = requestAnimationFrame(handleScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始滚动
|
||||||
|
const startScroll = () => {
|
||||||
|
if (!isScrolling.value || !needScroll.value) return
|
||||||
|
lastTimestamp = 0
|
||||||
|
animationFrame = requestAnimationFrame(handleScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止滚动
|
||||||
|
const stopScroll = () => {
|
||||||
|
if (animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
animationFrame = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换滚动状态
|
||||||
|
const toggleScroll = () => {
|
||||||
|
isScrolling.value = !isScrolling.value
|
||||||
|
if (isScrolling.value) {
|
||||||
|
startScroll()
|
||||||
|
} else {
|
||||||
|
stopScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改检查是否需要滚动的函数
|
||||||
|
const checkNeedScroll = () => {
|
||||||
|
const container = scrollContainerRef.value
|
||||||
|
if (!container) return false
|
||||||
|
|
||||||
|
// 获取内容的实际高度(不包括克隆的内容)
|
||||||
|
const contentHeight = container.querySelector(".grid")?.getBoundingClientRect().height || 0
|
||||||
|
// 获取容器的可视高度
|
||||||
|
const containerHeight = container.clientHeight
|
||||||
|
|
||||||
|
// 更新是否需要滚动的状态
|
||||||
|
needScroll.value = contentHeight > containerHeight
|
||||||
|
|
||||||
|
// 如果不需要滚动,确保停止滚动并重置位置
|
||||||
|
if (!needScroll.value) {
|
||||||
|
stopScroll()
|
||||||
|
isScrolling.value = false
|
||||||
|
scrollOffset.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return needScroll.value
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
timer = setInterval(updateTime, 1000)
|
timer = setInterval(updateTime, 1000)
|
||||||
await getDishByType()
|
await getDishByType()
|
||||||
@@ -215,6 +363,38 @@ onMounted(async () => {
|
|||||||
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange)
|
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange)
|
||||||
document.removeEventListener("mozfullscreenchange", handleFullscreenChange)
|
document.removeEventListener("mozfullscreenchange", handleFullscreenChange)
|
||||||
document.removeEventListener("MSFullscreenChange", handleFullscreenChange)
|
document.removeEventListener("MSFullscreenChange", handleFullscreenChange)
|
||||||
|
stopScroll()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 延迟启动滚动,等待内容加载
|
||||||
|
setTimeout(() => {
|
||||||
|
// 检查是否需要滚动
|
||||||
|
if (checkNeedScroll()) {
|
||||||
|
isScrolling.value = true
|
||||||
|
startScroll()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 在内容变化时重新检查是否需要滚动
|
||||||
|
watch(
|
||||||
|
() => [foodData.breakfast, foodData.lunch, foodData.dinner],
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
checkNeedScroll()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在窗口大小改变时重新检查是否需要滚动
|
||||||
|
const handleResize = debounce(() => {
|
||||||
|
checkNeedScroll()
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("resize", handleResize)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -271,7 +451,7 @@ const gridStyle = computed(() => {
|
|||||||
|
|
||||||
// 计算行列数
|
// 计算行列数
|
||||||
const aspectRatio = window.innerWidth / window.innerHeight
|
const aspectRatio = window.innerWidth / window.innerHeight
|
||||||
let columns = Math.ceil(Math.sqrt(totalItems * aspectRatio)) - 4
|
let columns = Math.ceil(Math.sqrt(totalItems * aspectRatio)) - 1
|
||||||
let rows = Math.ceil(totalItems / columns)
|
let rows = Math.ceil(totalItems / columns)
|
||||||
|
|
||||||
// 占满屏幕
|
// 占满屏幕
|
||||||
@@ -284,6 +464,17 @@ const gridStyle = computed(() => {
|
|||||||
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加一个简单的防抖函数
|
||||||
|
function debounce(fn: Function, delay: number) {
|
||||||
|
let timer: number | null = null
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn.apply(this, args)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -420,14 +611,13 @@ const gridStyle = computed(() => {
|
|||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-1 {
|
.scroll-wrapper {
|
||||||
min-height: 0;
|
height: 100%;
|
||||||
display: grid;
|
overflow: hidden;
|
||||||
grid-template-rows: auto 1fr;
|
position: relative;
|
||||||
overflow-y: auto;
|
}
|
||||||
scroll-behavior: smooth;
|
|
||||||
scrollbar-width: none;
|
.scroll-content {
|
||||||
-webkit-scrollbar-width: none;
|
will-change: transform;
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user