scroll list
This commit is contained in:
parent
314b03a9ab
commit
5d1f29d18d
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user