scroll list
This commit is contained in:
parent
314b03a9ab
commit
5d1f29d18d
@ -1,18 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import assets from '../assets';
|
||||
|
||||
import { regions } from '../data';
|
||||
import { regions, type RegionData, type Store } from '../data';
|
||||
|
||||
const regionsPos = [
|
||||
{ w: 68.8, h: 68.8, l: -1, t: -4, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6 },
|
||||
{ w: 73.3, h: 73.3, l: 44, t: 20, src: assets.p1.大东区, tt: '大东区', ad: 3 },
|
||||
{ w: 62.3, h: 63.7, l: -32, t: 72, src: assets.p1['大南区&免税'], tt: '大南区&免税', trt: 5, ad: 3 },
|
||||
{ w: 59.6, h: 59.6, l: -37, t: 15, src: assets.p1.大西区, tt: '大西区', ad: 5 },
|
||||
{ w: 52.1, h: 52, l: 3, t: 40, src: assets.p1.奥莱, tt: '奥莱', ad: 2 },
|
||||
{ w: 62.7, h: 62.7, l: 33, t: 74, src: assets.p1.博物馆, tt: '博物馆', tc: '#fff', ad: 0 },
|
||||
{ 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, key: '大东区' },
|
||||
{ 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, key: '大西区' },
|
||||
{ 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, 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 swapInAnimations: Animation[] = [];
|
||||
@ -199,6 +289,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
const pUp = () => {
|
||||
// 设置选中的区域和店铺数据
|
||||
selectedRegion.value = region.tt;
|
||||
selectedStores.value = getRegionStores(region.key);
|
||||
selectedStoreIndex.value = 0;
|
||||
|
||||
bubble.querySelector('span')?.animate([
|
||||
{ opacity: 1 },
|
||||
{ opacity: 0 },
|
||||
@ -208,7 +303,7 @@ onMounted(() => {
|
||||
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(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);
|
||||
}
|
||||
|
||||
@ -242,6 +345,11 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载时清理 observer
|
||||
onUnmounted(() => {
|
||||
cleanupObserver();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -303,7 +411,7 @@ onMounted(() => {
|
||||
<span class="elan">BENEFITS</span>
|
||||
</span>
|
||||
</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 v-for="i in regionsPos" :style="{
|
||||
width: i.w + '%',
|
||||
@ -340,6 +448,48 @@ onMounted(() => {
|
||||
<span>阵营</span>
|
||||
</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -420,4 +570,192 @@ img {
|
||||
width: 100%;
|
||||
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>
|
||||
Loading…
x
Reference in New Issue
Block a user