This commit is contained in:
weiwei committed 2025-03-17 19:02:12 +08:00
1 parent 47aa0e7ba8
commit f726bb11fb
1 file changed
+253 -63
+253 -63
View File
@@ -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>
<!-- 全屏按钮 -->
<el-tooltip :content="isFullscreen ? '退出全屏' : '全屏'" placement="bottom" effect="dark">
<button
@click="toggleFullScreen"
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"
>
<el-icon class="icon h-10 w-10 font-bold bg-contain bg-center bg-no-repeat"><FullScreen /></el-icon>
</button>
</el-tooltip>
<!-- 全屏按钮和滚动控制按钮 -->
<div class="absolute right-0 flex items-center gap-2">
<el-tooltip v-if="needScroll" :content="isScrolling ? '暂停滚动' : '开始滚动'" placement="bottom" effect="dark">
<button
@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="h-10 w-10 font-bold">
<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 class="flex-1 px-4" :style="containerStyle">
<div class="grid h-full w-full gap-3" :style="gridStyle">
<template v-for="hourType in foodData" :key="hourType">
<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(',')"
/>
<div class="scroll-wrapper" ref="scrollContainerRef">
<div class="scroll-content" :style="{ transform: `translateY(${-scrollOffset}px)` }">
<div class="grid w-full gap-3" :style="gridStyle">
<template v-for="hourType in foodData" :key="hourType">
<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>
<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>
</template>
</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>
@@ -104,13 +167,14 @@
</template>
<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 { ElTooltip } from "element-plus"
import { selectTimeFoodApi } from "/@/api/canteen/canteenDailyMenu"
import { getAllSimple as getCanteenCategory } from "/@/api/canteen/canteenCategory"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import { VideoPause, VideoPlay } from "@element-plus/icons-vue"
const { daily_menu } = useDict("daily_menu")
const containerRef = ref<HTMLElement | null>(null)
@@ -151,23 +215,22 @@ const foodData = reactive<{
const getDishByType = async () => {
const result = await selectTimeFoodApi({
// time: dayjs().format("YYYY-MM-DD"),
// time: "2025-03-14",
// time: "2025-03-16",
time: "2025-02-07",
time: "2025-03-14",
// time: "2025-02-07",
merchantId: "1887400193644199937",
hourType: 2,
// hourType: 2,
})
if (result.code === 0) {
result.data.forEach((r: any) => {
switch (r.hourType) {
case 1:
foodData.breakfast = r.dailyMenuVOS
// foodData.breakfast = r.dailyMenuVOS
break
case 2:
foodData.lunch = r.dailyMenuVOS
foodData.lunch = r.dailyMenuVOS.filter((v: any, index: number) => index < 13)
break
case 3:
foodData.dinner = r.dailyMenuVOS
// foodData.dinner = r.dailyMenuVOS
break
}
})
@@ -180,6 +243,91 @@ const updateTime = () => {
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 () => {
timer = setInterval(updateTime, 1000)
await getDishByType()
@@ -215,6 +363,38 @@ onMounted(async () => {
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange)
document.removeEventListener("mozfullscreenchange", 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
let columns = Math.ceil(Math.sqrt(totalItems * aspectRatio)) - 4
let columns = Math.ceil(Math.sqrt(totalItems * aspectRatio)) - 1
let rows = Math.ceil(totalItems / columns)
// 占满屏幕
@@ -284,6 +464,17 @@ const gridStyle = computed(() => {
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>
<style scoped lang="scss">
@@ -420,14 +611,13 @@ const gridStyle = computed(() => {
aspect-ratio: 16/9;
}
.flex-1 {
min-height: 0;
display: grid;
grid-template-rows: auto 1fr;
overflow-y: auto;
scroll-behavior: smooth;
scrollbar-width: none;
-webkit-scrollbar-width: none;
-ms-overflow-style: none;
.scroll-wrapper {
height: 100%;
overflow: hidden;
position: relative;
}
.scroll-content {
will-change: transform;
}
</style>