style: judge移动端

This commit is contained in:
2026-03-18 13:57:24 +08:00
parent 1b5d2bf762
commit 8efe7efa71
5 changed files with 255 additions and 204 deletions

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -32,7 +32,7 @@ let flexible = (designWidth, maxWidth, minWidth) => {
} else { } else {
designWidth = 750 designWidth = 750
} }
console.log(width, designWidth) // console.log(width, designWidth)
// var rem = width * 10 / designWidth; // var rem = width * 10 / designWidth;
var rem = Math.round((width * 10) / designWidth) var rem = Math.round((width * 10) / designWidth)

View File

@@ -1,34 +1,54 @@
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
export const useIsMobile = () => { export const useIsMobile = () => {
const isMobile = ref(false) const isMobile = ref(false)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
const checkDevice = () => { const checkDevice = () => {
// 1. 现代 Client Hints APIChrome/Edge 最准) // 使用防抖避免频繁触发
if (navigator.userAgentData?.mobile !== undefined) { if (resizeTimer) {
isMobile.value = navigator.userAgentData.mobile clearTimeout(resizeTimer)
return
} }
resizeTimer = setTimeout(() => {
// 1. 现代 Client Hints APIChrome/Edge 最准)
// if (navigator.userAgentData?.mobile !== undefined) {
// isMobile.value = navigator.userAgentData.mobile
// console.log('使用 userAgentData:', isMobile.value)
// }
// 2. 综合判断(兼容所有浏览器)
const ua = navigator.userAgent.toLowerCase()
const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i
// 2. 综合判断(兼容所有浏览器) const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 1
const ua = navigator.userAgent.toLowerCase() const smallScreen = window.innerWidth <= 1200 // 你可以改成 1024 等
const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 1 const uaCheck = mobileRegex.test(ua)
const smallScreen = window.innerWidth <= 768 // 你可以改成 1024 等 isMobile.value = uaCheck || smallScreen
}, 100)
}
isMobile.value = mobileRegex.test(ua) || (hasTouch && smallScreen) const handleResize = () => {
checkDevice()
} }
onMounted(() => { onMounted(() => {
checkDevice() checkDevice()
window.addEventListener('resize', checkDevice) window.addEventListener('resize', handleResize)
window.addEventListener('orientationchange', checkDevice) // 手机旋转必备 window.addEventListener('orientationchange', checkDevice) // 手机旋转必备
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', checkDevice) window.removeEventListener('resize', handleResize)
window.removeEventListener('orientationchange', checkDevice) window.removeEventListener('orientationchange', checkDevice)
if (resizeTimer) {
clearTimeout(resizeTimer)
}
})
// 处理 keep-alive 缓存的组件重新激活场景
onActivated(() => {
checkDevice()
}) })
return { isMobile } return { isMobile }

View File

@@ -1,24 +1,24 @@
<template> <template>
<div class="judges-container flex flex-col align-center"> <div class="judges-container flex flex-col align-center" :class="{ mobile: isMobile }">
<div class="title" ref="judgesTitleRef">{{ $t('AwardsPage.panelOfJudges') }}</div> <div class="title" ref="judgesTitleRef">{{ $t('AwardsPage.panelOfJudges') }}</div>
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> --> <!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
<div class="sub-title" ref="judgesSubTitleRef">{{ $t('AwardsPage.expertise') }}</div> <div class="sub-title" ref="judgesSubTitleRef">{{ $t('AwardsPage.expertise') }}</div>
<div class="judgement-list" ref="judgementListRef"> <div class="judgement-list" ref="judgementListRef">
<div <div
class="judgement-item flex flex-col align-center" class="judgement-item flex flex-col align-center"
v-for="item in judgements" v-for="item in judgements"
:key="item.name" :key="item.name"
> >
<img :src="item.picture" class="picture" /> <img :src="item.picture" class="picture" />
<div class="name">{{ $t(item.name) }}</div> <div class="name">{{ $t(item.name) }}</div>
<div class="desc">{{ $t(item.desc) }}</div> <div class="desc">{{ $t(item.desc) }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, nextTick, ref } from 'vue' import { onBeforeUnmount, onMounted, nextTick, ref, inject } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { gsap } from 'gsap' import { gsap } from 'gsap'
import jae from '@/assets/images/award/jae.png' import jae from '@/assets/images/award/jae.png'
@@ -29,38 +29,38 @@ import tim from '@/assets/images/award/tim.png'
import desmond from '@/assets/images/award/desmond.png' import desmond from '@/assets/images/award/desmond.png'
const { t } = useI18n() const { t } = useI18n()
const isMobile = inject<boolean>('isMobile')
const judgements = [ const judgements = [
{ {
picture: jae, picture: jae,
name: 'Jae Hyuk Lim', name: 'Jae Hyuk Lim',
desc: 'AwardsPage.judgesHat.jae' desc: 'AwardsPage.judgesHat.jae'
}, },
{ {
picture: diego, picture: diego,
name: 'Diego Dultzin Lacoste', name: 'Diego Dultzin Lacoste',
desc: 'AwardsPage.judgesHat.diego' desc: 'AwardsPage.judgesHat.diego'
}, },
{ {
picture: gregory, picture: gregory,
name: 'Gregory de la Hogue Moran', name: 'Gregory de la Hogue Moran',
desc: 'AwardsPage.judgesHat.gregory' desc: 'AwardsPage.judgesHat.gregory'
}, },
{ {
picture: vincenzo, picture: vincenzo,
name: 'Vincenzo La Torre', name: 'Vincenzo La Torre',
desc: 'AwardsPage.judgesHat.vincenzo' desc: 'AwardsPage.judgesHat.vincenzo'
}, },
{ {
picture: tim, picture: tim,
name: 'Tim Lim', name: 'Tim Lim',
desc: 'AwardsPage.judgesHat.tim' desc: 'AwardsPage.judgesHat.tim'
}, },
{ {
picture: desmond, picture: desmond,
name: 'Desmond Lim', name: 'Desmond Lim',
desc: 'AwardsPage.judgesHat.desmond' desc: 'AwardsPage.judgesHat.desmond'
} }
] ]
const judgesTitleRef = ref<HTMLElement | null>(null) const judgesTitleRef = ref<HTMLElement | null>(null)
@@ -70,165 +70,196 @@ const hasPlayedJudgementAnim = ref(false)
let judgementObserver: IntersectionObserver | null = null let judgementObserver: IntersectionObserver | null = null
const setupJudgementInitialState = () => { const setupJudgementInitialState = () => {
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter( const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(Boolean) as HTMLElement[]
Boolean if (titleEls.length) {
) as HTMLElement[] gsap.set(titleEls, {
if (titleEls.length) { opacity: 0,
gsap.set(titleEls, { scale: 0,
opacity: 0, transformOrigin: '50% 50%'
scale: 0, })
transformOrigin: '50% 50%' }
}) const items = judgementListRef.value?.querySelectorAll<HTMLElement>('.judgement-item')
} if (items?.length) {
const items = gsap.set(items, {
judgementListRef.value?.querySelectorAll<HTMLElement>('.judgement-item') opacity: 0,
if (items?.length) { clipPath: 'inset(0 0 100% 0)'
gsap.set(items, { })
opacity: 0, }
clipPath: 'inset(0 0 100% 0)'
})
}
} }
const playJudgementAnimation = () => { const playJudgementAnimation = () => {
if (hasPlayedJudgementAnim.value) return if (hasPlayedJudgementAnim.value) return
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter( const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(Boolean) as HTMLElement[]
Boolean const listEl = judgementListRef.value
) as HTMLElement[] if (!titleEls.length || !listEl) return
const listEl = judgementListRef.value
if (!titleEls.length || !listEl) return
const items = Array.from( const items = Array.from(listEl.querySelectorAll<HTMLElement>('.judgement-item'))
listEl.querySelectorAll<HTMLElement>('.judgement-item') const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
)
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
tl.to(titleEls, { tl.to(titleEls, {
opacity: 1, opacity: 1,
scale: 1, scale: 1,
duration: 0.4, duration: 0.4,
ease: 'back.out(1.6)', ease: 'back.out(1.6)',
stagger: 0.1 stagger: 0.1
}) })
if (items.length) { if (items.length) {
const firstRow = items.slice(0, 3) const firstRow = items.slice(0, 3)
const secondRow = items.slice(3) const secondRow = items.slice(3)
if (firstRow.length) { if (firstRow.length) {
tl.to( tl.to(
firstRow, firstRow,
{ {
opacity: 1, opacity: 1,
clipPath: 'inset(0% 0% 0% 0%)', clipPath: 'inset(0% 0% 0% 0%)',
duration: 0.45, duration: 0.45,
stagger: 0.05 stagger: 0.05
}, },
'-=0.2' '-=0.2'
) )
} }
if (secondRow.length) { if (secondRow.length) {
tl.to( tl.to(
secondRow, secondRow,
{ {
opacity: 1, opacity: 1,
clipPath: 'inset(0% 0% 0% 0%)', clipPath: 'inset(0% 0% 0% 0%)',
duration: 0.45, duration: 0.45,
stagger: 0.05 stagger: 0.05
}, },
'+=0.1' '+=0.1'
) )
} }
} }
hasPlayedJudgementAnim.value = true hasPlayedJudgementAnim.value = true
judgementObserver?.disconnect() judgementObserver?.disconnect()
} }
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
setupJudgementInitialState() setupJudgementInitialState()
if ('IntersectionObserver' in window) { if ('IntersectionObserver' in window) {
judgementObserver = new IntersectionObserver( judgementObserver = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
playJudgementAnimation() playJudgementAnimation()
} }
}) })
}, },
{ threshold: 0.3 } { threshold: 0.3 }
) )
if (judgementListRef.value) { if (judgementListRef.value) {
judgementObserver.observe(judgementListRef.value) judgementObserver.observe(judgementListRef.value)
} }
} else { } else {
// Fallback: play immediately if IntersectionObserver unsupported // Fallback: play immediately if IntersectionObserver unsupported
playJudgementAnimation() playJudgementAnimation()
} }
}) })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
judgementObserver?.disconnect() judgementObserver?.disconnect()
}) })
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.judges-container { .judges-container {
height: 147.4rem; height: 147.4rem;
background: url('@/assets/images/award/judges_bg.png') no-repeat; background: url('@/assets/images/award/judges_bg.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
padding-top: 12.8rem; padding-top: 12.8rem;
.title { .title {
color: #232323; color: #232323;
font-family: 'PoppinsBold'; font-family: 'PoppinsBold';
font-weight: 600; font-weight: 600;
font-size: 4rem; font-size: 4rem;
margin-bottom: 2.4rem; margin-bottom: 2.4rem;
} }
.logo { .logo {
margin: 2.4rem 0 2.2rem; margin: 2.4rem 0 2.2rem;
} }
.sub-title { .sub-title {
color: #b10000; color: #b10000;
font-family: 'Arial'; font-family: 'Arial';
font-weight: 400; font-weight: 400;
font-size: 3rem; font-size: 3rem;
margin-bottom: 12rem; margin-bottom: 12rem;
} }
.judgement-list { .judgement-list {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
column-gap: 23.22rem; column-gap: 23.22rem;
row-gap: 8rem; row-gap: 8rem;
padding: 0 25rem 0 26.6rem; padding: 0 25rem 0 26.6rem;
div{ div {
text-align: center; text-align: center;
} }
.judgement-item { .judgement-item {
overflow: hidden; overflow: hidden;
.picture { .picture {
width: 20.2rem; width: 20.2rem;
height: 26rem; height: 26rem;
border-radius: 0.8rem; border-radius: 0.8rem;
} }
.name { .name {
margin: 3rem 0 2.4rem; margin: 3rem 0 2.4rem;
color: #232323; color: #232323;
font-family: 'PoppinsBold'; font-family: 'PoppinsBold';
font-weight: 600; font-weight: 600;
font-size: 2.4rem; font-size: 2.4rem;
} }
.desc { .desc {
color: #585858; color: #585858;
font-family: 'Arial'; font-family: 'Arial';
font-weight: 400; font-weight: 400;
font-size: 2rem; font-size: 2rem;
white-space: pre-line; white-space: pre-line;
text-align: center; text-align: center;
} }
} }
} }
&.mobile {
height: 139.6rem;
background: url('@/assets/images/mobile_version_background/judge_bg.png') no-repeat;
background-size: 100% 100%;
padding-top: 6rem;
.title {
font-size: 3.2rem;
margin-bottom: 0.8rem;
}
.sub-title {
font-family: 'PoppinsMedium';
font-weight: 500;
font-size: 2.4rem;
margin-bottom: 5.8rem;
}
.judgement-list {
padding: 0 8.6rem;
grid-template-columns: repeat(2, 1fr);
column-gap: 6.8rem;
row-gap: 6rem;
// row-gap: 4rem;
// padding: 0 1.6rem;
.judgement-item {
.picture {
width: 15.6rem;
height: 20rem;
}
.name {
font-size: 2.4rem;
margin: 1.2rem 0;
}
.desc {
font-family: 'Instrument';
font-size: 2rem;
}
}
}
}
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="award-page" :class="{ 'is-zh': isZh }"> <div class="award-page" :class="{ 'is-zh': isZh }">
<div class="banner"> <div class="banner" :class="{ 'mobile': isMobile }">
<video <video
:src="bannerUrl" :src="bannerUrl"
autoplay autoplay