scroll list

This commit is contained in:
feie9456 2025-07-08 12:03:10 +08:00
parent 314b03a9ab
commit 5d1f29d18d

View File

@ -1,18 +1,108 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import assets from '../assets'; import assets from '../assets';
import { regions } from '../data'; import { regions, type RegionData, type Store } from '../data';
const regionsPos = [ const regionsPos = [
{ w: 68.8, h: 68.8, l: -1, t: -4, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6 }, { w: 68.8, h: 68.8, l: -1, t: -4, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6, key: '大北区' },
{ w: 73.3, h: 73.3, l: 44, t: 20, src: assets.p1.大东区, tt: '大东区', ad: 3 }, { w: 73.3, h: 73.3, l: 44, t: 20, src: assets.p1.大东区, tt: '大东区', ad: 3, key: '大东区' },
{ w: 62.3, h: 63.7, l: -32, t: 72, src: assets.p1['大南区&免税'], tt: '大南区&免税', trt: 5, ad: 3 }, { w: 62.3, h: 63.7, l: -32, t: 72, src: assets.p1['大南区&免税'], tt: '大南区&免税', trt: 5, ad: 3, key: '大南区&免税' },
{ w: 59.6, h: 59.6, l: -37, t: 15, src: assets.p1.大西区, tt: '大西区', ad: 5 }, { w: 59.6, h: 59.6, l: -37, t: 15, src: assets.p1.大西区, tt: '大西区', ad: 5, key: '大西区' },
{ w: 52.1, h: 52, l: 3, t: 40, src: assets.p1.奥莱, tt: '奥莱', ad: 2 }, { w: 52.1, h: 52, l: 3, t: 40, src: assets.p1.奥莱, tt: '奥莱', ad: 2, key: '奥莱' },
{ w: 62.7, h: 62.7, l: 33, t: 74, src: assets.p1.博物馆, tt: '博物馆', tc: '#fff', ad: 0 }, { w: 62.7, h: 62.7, l: 33, t: 74, src: assets.p1.博物馆, tt: '博物馆', tc: '#fff', ad: 0, key: '博物馆' },
] ]
//
const showStoreSelector = ref(false);
const selectedRegion = ref<string>('');
const selectedStores = ref<Store[]>([]);
const selectedStoreIndex = ref(0);
//
const regionKeyMap: { [key: string]: keyof RegionData | 'combined' } = {
'大北区': '大北区',
'大东区': '大东区',
'大南区&免税': 'combined', //
'大西区': '大西区',
'奥莱': 'combined', //
'博物馆': '博物馆',
};
//
function getRegionStores(regionKey: string): Store[] {
const mappedKey = regionKeyMap[regionKey];
if (mappedKey === 'combined') {
if (regionKey === '大南区&免税') {
return [...regions.大南区, ...regions.免税];
} else if (regionKey === '奥莱') {
return [...regions.奥莱北区, ...regions.奥莱东区];
}
} else if (mappedKey) {
return regions[mappedKey];
}
return [];
}
// - 使 Intersection Observer
let observer: IntersectionObserver | null = null;
function initializeStoreSelector() {
// DOM
setTimeout(() => {
const storeItems = document.querySelectorAll('.store-item');
if (storeItems.length === 0) return;
// Intersection Observer
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
//
const index = Array.from(storeItems).indexOf(entry.target);
if (index !== -1 && index !== selectedStoreIndex.value) {
selectedStoreIndex.value = index;
}
}
});
}, {
root: document.querySelector('.store-list'),
rootMargin: '-45% 0px -45% 0px', // 10%
threshold: 0.5
});
//
storeItems.forEach(item => {
observer?.observe(item);
});
}, 100);
}
function selectStore(index: number) {
selectedStoreIndex.value = index;
// scroll-snap
const storeItems = document.querySelectorAll('.store-item');
if (storeItems[index]) {
storeItems[index].scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
// observer
function cleanupObserver() {
if (observer) {
observer.disconnect();
observer = null;
}
}
function startExploration() {
//
console.log('开始探索:', selectedStores.value[selectedStoreIndex.value]);
}
// //
let fadeInAnimations: Animation[] = []; let fadeInAnimations: Animation[] = [];
let swapInAnimations: Animation[] = []; let swapInAnimations: Animation[] = [];
@ -199,6 +289,11 @@ onMounted(() => {
} }
const pUp = () => { const pUp = () => {
//
selectedRegion.value = region.tt;
selectedStores.value = getRegionStores(region.key);
selectedStoreIndex.value = 0;
bubble.querySelector('span')?.animate([ bubble.querySelector('span')?.animate([
{ opacity: 1 }, { opacity: 1 },
{ opacity: 0 }, { opacity: 0 },
@ -208,7 +303,7 @@ onMounted(() => {
fill: 'forwards' fill: 'forwards'
}); });
bubble.animate([ const scaleAnimation = bubble.animate([
{ transform: 'translate3d(-50%, -50%, 0) scale(0.92)', filter: 'brightness(0.9)' }, { transform: 'translate3d(-50%, -50%, 0) scale(0.92)', filter: 'brightness(0.9)' },
{ transform: 'translate3d(-50%, -50%, 0) scale(4)', filter: 'brightness(1)', top: '53%', left: '50%' }, { transform: 'translate3d(-50%, -50%, 0) scale(4)', filter: 'brightness(1)', top: '53%', left: '50%' },
], { ], {
@ -233,6 +328,14 @@ onMounted(() => {
}); });
}); });
//
scaleAnimation.addEventListener('finish', () => {
showStoreSelector.value = true;
// Intersection Observer
initializeStoreSelector();
});
bubble.removeEventListener('pointerdown', pDown); bubble.removeEventListener('pointerdown', pDown);
} }
@ -242,6 +345,11 @@ onMounted(() => {
}); });
}); });
// observer
onUnmounted(() => {
cleanupObserver();
});
</script> </script>
<template> <template>
@ -303,7 +411,7 @@ onMounted(() => {
<span class="elan">BENEFITS</span> <span class="elan">BENEFITS</span>
</span> </span>
</div> </div>
<img :src="assets.p1.太阳" alt="" style="top: 30%;left: 5%;width: 26%;"> <!-- <img :src="assets.p1.太阳" alt="" style="top: 30%;left: 5%;width: 26%;"> -->
<div style="width: 86vw;height: 86vw;top: 47%; left: 50%;transform: translate3d(-50%, -50%, 0); z-index: -1;"> <div style="width: 86vw;height: 86vw;top: 47%; left: 50%;transform: translate3d(-50%, -50%, 0); z-index: -1;">
<div v-for="i in regionsPos" :style="{ <div v-for="i in regionsPos" :style="{
width: i.w + '%', width: i.w + '%',
@ -340,6 +448,48 @@ onMounted(() => {
<span>阵营</span> <span>阵营</span>
</span> </span>
</div> </div>
<!-- 店铺选择界面 -->
<div v-if="showStoreSelector" class="store-selector">
<div class="store-selector-content">
<h2 class="region-title">你来自{{ selectedRegion }}</h2>
<div class="store-list-container">
<div class="store-list relative">
<!-- 顶部填充确保第一个元素能滚动到中间 -->
<div class="scroll-padding relative"></div>
<div
v-for="(store, index) in selectedStores"
:key="store.店铺号"
class="store-item relative"
:class="{ active: index === selectedStoreIndex }"
@click="selectStore(index)"
>
<div class="store-name relative">{{ store.店铺 }}</div>
</div>
<!-- 底部填充确保最后一个元素能滚动到中间 -->
<div class="scroll-padding relative"></div>
</div>
</div>
<div class="name-input-section">
<h3>请输入你的名字</h3>
<div class="input-container">
<input type="text" class="name-input" placeholder="">
<div class="input-line"></div>
</div>
</div>
<div class="action-buttons">
<button class="start-button" @click="startExploration">
<span class="arrow"></span>
<span>开始探索</span>
</button>
</div>
</div>
</div>
</section> </section>
</template> </template>
@ -420,4 +570,192 @@ img {
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
} }
/* 店铺选择界面样式 */
.store-selector {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.store-selector-content {
width: 90%;
max-width: 400px;
text-align: center;
color: #333;
}
.region-title {
font-size: 6vw;
font-weight: bold;
margin-bottom: 8vw;
color: #333;
}
.store-list-container {
margin-bottom: 12vw;
height: 40vh;
overflow: hidden;
position: relative;
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 15%,
black 85%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
black 15%,
black 85%,
transparent 100%
);
}
.store-list {
height: 100%;
overflow-y: auto;
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
padding: 0;
}
.scroll-padding {
height: calc(20vh - 4vw); /* 容器高度的一半减去项目高度的一半 */
flex-shrink: 0;
}
.store-item {
width: 50vw;
height: 8vw;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8vw;
scroll-snap-align: center;
transition: all 0.3s ease;
cursor: pointer;
opacity: 0.6;
font-size: 4vw;
margin: 1vw auto;
flex-shrink: 0;
}
.store-item.active {
background: rgba(255, 255, 255, 0.9);
opacity: 1;
font-weight: bold;
}
.center-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 10vw;
border: 2px solid rgba(51, 51, 51, 0.2);
border-radius: 10vw;
pointer-events: none;
z-index: 2;
background: rgba(255, 255, 255, 0.05);
}
.name-input-section {
margin-bottom: 8vw;
}
.name-input-section h3 {
font-size: 5vw;
color: #333;
margin-bottom: 4vw;
font-weight: normal;
}
.input-container {
position: relative;
width: 80%;
margin: 0 auto;
}
.name-input {
width: 100%;
padding: 2vw 0;
font-size: 4.5vw;
background: transparent;
border: none;
outline: none;
color: #333;
text-align: center;
}
.input-line {
width: 100%;
height: 2px;
background: #333;
margin-top: 1vw;
position: relative;
}
.input-line::after {
content: '';
position: absolute;
right: 0;
top: -8px;
width: 0;
height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 16px solid #333;
}
.action-buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 4vw;
}
.start-button {
display: flex;
align-items: center;
gap: 3vw;
padding: 4vw 8vw;
background: #000;
color: #fff;
border: none;
border-radius: 50px;
font-size: 4.5vw;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.start-button:hover {
transform: scale(1.05);
}
.start-button .arrow {
font-size: 5vw;
font-weight: normal;
}
/* 隐藏滚动条 */
.store-list::-webkit-scrollbar {
display: none;
}
.store-list {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style> </style>