This commit is contained in:
2025-07-10 10:54:10 +08:00
parent 5d1f29d18d
commit 9d5870b37b
60 changed files with 1794 additions and 159 deletions
+20 -4
View File
@@ -1,23 +1,39 @@
<script setup lang="ts">
import { ref } from 'vue';
import Game from './pages/Game.vue';
import Page1 from './pages/Page1.vue';
const stage = ref(1);
const userData = ref({
region: '奥莱',
store: '北京斯普瑞斯',
username: '测试用户'
});
function startExploration(payload: { region: string; store: string; username: string }) {
userData.value = payload;
stage.value = 1;
}
</script>
<template>
<main>
<Page1 class="page"/>
<Page1 class="page" v-if="stage == 0" @start-exploration="startExploration"/>
<Game class="page" v-if="stage == 1" :userdata="userData"/>
</main>
</template>
<style scoped>
main{
main {
height: 100vh;
height: 100dvh;
width: 100vw;
overflow: hidden;
position: relative;
}
.page{
.page {
position: absolute;
top: 50%;
left: 0;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+26 -1
View File
@@ -13,5 +13,30 @@ export default {
"太阳": new URL('./p1/太阳.png', import.meta.url).href,
"Flexible benefits": new URL('./p1/Flexible benefits.png', import.meta.url).href,
"Tour": new URL('./p1/Tour.png', import.meta.url).href,
}
},
game:{
},
ani:{
下拉蓄力提示: new URL('./ani/下拉蓄力提示.zip', import.meta.url).href,
小太阳总: new URL('./ani/小太阳总.zip', import.meta.url).href,
后端效果: new URL('./ani/后段效果.zip', import.meta.url).href,
主标出现: new URL('./ani/主标出现.zip', import.meta.url).href,
结尾: new URL('./ani/结尾.zip', import.meta.url).href,
},
icons: [
[new URL('./icons/icon1.png', import.meta.url).href, new URL('./icons/bigicon1.png', import.meta.url).href],
[new URL('./icons/icon2.png', import.meta.url).href, new URL('./icons/bigicon2.png', import.meta.url).href],
[new URL('./icons/icon3.png', import.meta.url).href, new URL('./icons/bigicon3.png', import.meta.url).href],
[new URL('./icons/icon4.png', import.meta.url).href, new URL('./icons/bigicon4.png', import.meta.url).href],
[new URL('./icons/icon5.png', import.meta.url).href, new URL('./icons/bigicon5.png', import.meta.url).href],
[new URL('./icons/icon6.png', import.meta.url).href, new URL('./icons/bigicon6.png', import.meta.url).href],
[new URL('./icons/icon7.png', import.meta.url).href, new URL('./icons/bigicon7.png', import.meta.url).href],
[new URL('./icons/icon8.png', import.meta.url).href, new URL('./icons/bigicon8.png', import.meta.url).href],
],
metal:[
[new URL('./game/metal_0.png', import.meta.url).href,],
[new URL('./game/metal_1.png', import.meta.url).href,],
[new URL('./game/metal_2.png', import.meta.url).href,]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+449
View File
@@ -0,0 +1,449 @@
<template>
<div class="ani-ele">
<canvas v-if="!loading && !error" ref="canvasRef" :width="canvasWidth" :height="canvasHeight"></canvas>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import JSZip from 'jszip'
// Props (接口定义不变)
interface AnimationRule {
name: string
frame: number
duration?: number
loop: number
startFrame?: number
endFrame?: number
reverse?: boolean
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
pauseAfter?: boolean
}
interface Props {
url: string
rules: AnimationRule[]
width?: number
height?: number
autoPlay?: boolean
log?: boolean
}
const props = withDefaults(defineProps<Props>(), {
width: 800,
height: 600,
autoPlay: true
})
// 响应式状态
const loading = ref(false)
const error = ref('')
const progress = ref(0)
const images = ref<string[]>([]) // 仍然用于临时存储Blob URL
// 【优化】: 存储 ImageData 而不是 HTMLImageElement
const loadedImages = ref<ImageData[]>([])
const canvasRef = ref<HTMLCanvasElement>()
const canvasWidth = ref(props.width)
const canvasHeight = ref(props.height)
const isPlaying = ref(false)
const isPaused = ref(false)
const currentRuleIndex = ref(0)
const currentFrame = ref(0)
const currentLoopCount = ref(0)
const animationId = ref<number>()
const lastFrameTime = ref(0)
const pendingJumpTo = ref<string>()
// loadImageSequence 函数 (不变)
const loadImageSequence = async (zipUrl: string) => {
try {
loading.value = true
error.value = ''
progress.value = 0
images.value = []
loadedImages.value = []
const response = await fetch(zipUrl)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const zipBlob = await response.blob()
progress.value = 30
const zip = new JSZip()
const zipData = await zip.loadAsync(zipBlob)
progress.value = 50
const webpFiles = Object.keys(zipData.files)
.filter(name => !zipData.files[name].dir && (name.endsWith('.webp') || name.endsWith('.png') || name.endsWith('.jpg')))
.sort((a, b) => {
const numA = parseInt(a.match(/(\d+)\.\w+$/)?.[1] || '0')
const numB = parseInt(b.match(/(\d+)\.\w+$/)?.[1] || '0')
return numA - numB
})
if (webpFiles.length === 0) {
throw new Error('压缩包中没有找到支持的图片文件 (webp, png, jpg)')
}
const imageUrls: string[] = []
for (let i = 0; i < webpFiles.length; i++) {
const fileName = webpFiles[i]
const file = zipData.files[fileName]
const imageBlob = await file.async('blob')
const blobUrl = URL.createObjectURL(imageBlob)
imageUrls.push(blobUrl)
progress.value = 50 + (i + 1) / webpFiles.length * 30
}
images.value = imageUrls
await loadAllImages()
loading.value = false
if (props.log) console.log(`成功加载并转换 ${loadedImages.value.length} 帧 ImageData`)
if (props.autoPlay) {
startAnimation()
}
} catch (err) {
console.error('加载图片序列失败:', err)
error.value = err instanceof Error ? err.message : '未知错误'
loading.value = false
}
}
// =================================================================
// 核心优化区域 1: loadAllImages 函数
// 将图片转换为 ImageData
// =================================================================
const loadAllImages = async () => {
const imageDataArray: ImageData[] = [];
// 创建一个离屏Canvas用于转换
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true });
if (!offscreenCtx) {
throw new Error("无法创建离屏Canvas上下文用于图片转换");
}
for (let i = 0; i < images.value.length; i++) {
const img = new Image();
img.src = images.value[i];
await new Promise<void>((resolve, reject) => {
img.onload = () => {
// 第一张图片决定了Canvas的尺寸
if (i === 0) {
canvasWidth.value = props.width || img.width;
canvasHeight.value = props.height || img.height;
// 设置离屏Canvas的尺寸
offscreenCanvas.width = canvasWidth.value;
offscreenCanvas.height = canvasHeight.value;
}
// 将图片绘制到离屏Canvas上
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
offscreenCtx.drawImage(img, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
// 从离屏Canvas中提取ImageData
const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);
imageDataArray.push(imageData);
// 【内存优化】: 转换完成后,立即释放Blob URL,因为它不再需要
URL.revokeObjectURL(img.src);
resolve();
};
img.onerror = reject;
});
progress.value = 80 + (i + 1) / images.value.length * 20;
}
loadedImages.value = imageDataArray;
// 【内存优化】: 清空临时的URL数组
images.value = [];
};
// =================================================================
// 核心优化区域 2: drawFrame 函数
// 使用 putImageData 进行绘制
// =================================================================
const drawFrame = (frameIndex: number) => {
if (!canvasRef.value || !loadedImages.value.length) return;
const safeIndex = Math.max(0, Math.min(frameIndex, loadedImages.value.length - 1));
// if (props.log) {
// console.log(`绘制帧: ${safeIndex}`);
// }
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 清空画布
// ctx.clearRect(0, 0, canvas.width, canvas.height); // putImageData会完全覆盖,所以clearRect可以省略
const imageData = loadedImages.value[safeIndex];
if (imageData) {
// 使用putImageData直接绘制像素数据,性能更高
ctx.putImageData(imageData, 0, 0);
}
};
// jumpToRule 函数 (不变)
const jumpToRule = (ruleName: string) => {
const ruleIndex = findRuleIndex(ruleName)
if (ruleIndex === -1) {
console.warn(`未找到名为 "${ruleName}" 的规则`)
return false
}
if (props.log) console.log(`跳转到规则 "${ruleName}" (索引: ${ruleIndex})`);
if (isPlaying.value) {
stopAnimation()
}
currentRuleIndex.value = ruleIndex
currentLoopCount.value = 0
const targetRule = props.rules[ruleIndex]
let startFrame = targetRule.startFrame ?? 0
if (targetRule.startFrame === undefined) {
for (let i = 0; i < ruleIndex; i++) {
startFrame += props.rules[i].frame
}
}
const endFrame = targetRule.endFrame ?? (startFrame + targetRule.frame - 1);
currentFrame.value = targetRule.reverse ? endFrame : startFrame;
if (props.log) {
console.log(`跳转到规则 "${ruleName}", 实际开始帧: ${currentFrame.value}, 规则帧范围: ${startFrame} - ${endFrame}`);
}
drawFrame(currentFrame.value)
isPaused.value = false
pendingJumpTo.value = undefined
isPlaying.value = true
lastFrameTime.value = performance.now()
animate()
return true
}
// startAnimation 函数 (不变)
const startAnimation = () => {
if (!props.rules.length || !loadedImages.value.length || isPlaying.value) return
isPlaying.value = true
isPaused.value = false
if (currentRuleIndex.value >= props.rules.length) {
resetAnimation()
}
const currentRule = props.rules[currentRuleIndex.value]
let startFrame = currentRule.startFrame ?? 0
if (currentRule.startFrame === undefined) {
for (let i = 0; i < currentRuleIndex.value; i++) {
startFrame += props.rules[i].frame
}
}
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
if (props.log) console.log(`开始播放规则 "${currentRule.name}", 实际开始帧: ${currentFrame.value}`);
lastFrameTime.value = performance.now()
animate()
}
// animate 函数 (不变)
const animate = () => {
if (!isPlaying.value || currentRuleIndex.value >= props.rules.length) {
animationId.value = undefined;
return;
}
animationId.value = requestAnimationFrame(animate);
const now = performance.now();
const currentRule = props.rules[currentRuleIndex.value];
const frameDuration = currentRule.duration ?? 33;
if (now - lastFrameTime.value >= frameDuration) {
lastFrameTime.value = now - (now - lastFrameTime.value) % frameDuration;
let startFrame = currentRule.startFrame ?? 0;
if (currentRule.startFrame === undefined) {
for (let i = 0; i < currentRuleIndex.value; i++) {
startFrame += props.rules[i].frame;
}
}
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
let nextFrame = currentFrame.value + (currentRule.reverse ? -1 : 1);
const isEndOfRuleSegment = currentRule.reverse ? (nextFrame < startFrame) : (nextFrame > endFrame);
if (isEndOfRuleSegment) {
currentLoopCount.value++;
const isRuleFinished = currentRule.loop > 0 && currentLoopCount.value >= currentRule.loop;
if (isRuleFinished) {
if (pendingJumpTo.value) {
const targetRule = pendingJumpTo.value;
pendingJumpTo.value = undefined;
jumpToRule(targetRule);
return;
}
if (currentRule.pauseAfter) {
isPaused.value = true;
isPlaying.value = false;
if(props.log) console.log(`规则 "${currentRule.name}" 完成,暂停等待跳转指令`);
drawFrame(currentRule.reverse ? startFrame : endFrame);
return;
}
currentRuleIndex.value++;
currentLoopCount.value = 0;
if (currentRuleIndex.value >= props.rules.length) {
isPlaying.value = false;
if(props.log) console.log('所有规则播放完毕');
drawFrame(currentRule.reverse ? startFrame : endFrame);
return;
}
const nextRule = props.rules[currentRuleIndex.value];
let nextRuleStartFrame = nextRule.startFrame ?? 0;
if (nextRule.startFrame === undefined) {
nextRuleStartFrame = endFrame + 1;
}
const nextRuleEndFrame = nextRule.endFrame ?? (nextRuleStartFrame + nextRule.frame - 1);
currentFrame.value = nextRule.reverse ? nextRuleEndFrame : nextRuleStartFrame;
if(props.log) console.log(`进入下一规则 "${nextRule.name}", 实际开始帧: ${currentFrame.value}`);
} else {
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
}
} else {
currentFrame.value = nextFrame;
}
drawFrame(currentFrame.value);
}
};
// stopAnimation, resetAnimation, etc. (不变)
const stopAnimation = () => {
isPlaying.value = false
isPaused.value = false
if (animationId.value) {
cancelAnimationFrame(animationId.value)
animationId.value = undefined
}
}
const resetAnimation = () => {
stopAnimation()
currentRuleIndex.value = 0
currentFrame.value = 0
currentLoopCount.value = 0
pendingJumpTo.value = undefined
if (loadedImages.value.length > 0) {
drawFrame(0)
}
}
const togglePlayback = () => {
if (isPlaying.value) {
stopAnimation()
} else {
startAnimation()
}
}
const setJumpTarget = (ruleName: string) => {
if (isPaused.value) {
jumpToRule(ruleName)
} else {
pendingJumpTo.value = ruleName
}
}
const jumpToRuleImmediately = (ruleName: string) => {
return jumpToRule(ruleName)
}
const resumeFromPause = () => {
if (isPaused.value) {
isPaused.value = false
isPlaying.value = true
lastFrameTime.value = performance.now()
animate()
}
}
// watch 和 onMounted (清理逻辑现在更加健壮)
watch(() => props.url, (newUrl) => {
if (newUrl) {
stopAnimation()
// 由于Blob URL在加载时已释放,这里主要是清空状态
images.value.forEach(url => URL.revokeObjectURL(url)); // 以防加载中途切换URL
images.value = [];
loadedImages.value = [];
loadImageSequence(newUrl)
}
}, { immediate: true })
watch(() => props.rules, () => {
if (loadedImages.value.length > 0) {
resetAnimation()
if (props.autoPlay) {
startAnimation()
}
}
}, { deep: true })
onBeforeUnmount(() => {
stopAnimation();
// 由于images数组在加载成功后会被清空,此处的forEach可能为空操作,但作为安全保障是好的。
images.value.forEach(url => URL.revokeObjectURL(url));
});
// findRuleIndex 和 defineExpose (不变)
const findRuleIndex = (ruleName: string): number => {
return props.rules.findIndex(rule => rule.name === ruleName)
}
defineExpose({
images,
loadedImages,
loading,
error,
isPlaying,
isPaused,
currentFrame,
currentRuleIndex,
currentLoopCount,
reload: () => loadImageSequence(props.url),
play: startAnimation,
pause: stopAnimation,
reset: resetAnimation,
toggle: togglePlayback,
resume: resumeFromPause,
jumpTo: jumpToRuleImmediately,
setJumpTarget,
drawFrame: (frameIndex: number) => drawFrame(frameIndex),
findRuleIndex,
getRules: () => props.rules,
getCurrentRule: () => props.rules[currentRuleIndex.value],
getRuleByName: (name: string) => props.rules.find(rule => rule.name === name)
})
</script>
<style scoped>
canvas {
width: 100%;
height: auto;
object-fit: contain;
}
</style>
+1104
View File
File diff suppressed because it is too large Load Diff
+94 -150
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import assets from '../assets';
import { regions, type RegionData, type Store } from '../data';
import AniEle from '../components/AniEle.vue';
const regionsPos = [
{ w: 68.8, h: 68.8, l: -1, t: -4, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6, key: '大北区' },
@@ -22,7 +23,7 @@ const selectedStoreIndex = ref(0);
// 区域映射
const regionKeyMap: { [key: string]: keyof RegionData | 'combined' } = {
'大北区': '大北区',
'大东区': '大东区',
'大东区': '大东区',
'大南区&免税': 'combined', // 需要合并大南区和免税
'大西区': '大西区',
'奥莱': 'combined', // 需要合并奥莱北区和奥莱东区
@@ -51,9 +52,9 @@ function initializeStoreSelector() {
// 等待DOM渲染完成
setTimeout(() => {
const storeItems = document.querySelectorAll('.store-item');
if (storeItems.length === 0) return;
// 创建 Intersection Observer
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -97,10 +98,25 @@ function cleanupObserver() {
observer = null;
}
}
const emit = defineEmits<{
(e: 'startExploration', payload: { region: string; store: string, username: string }): void;
}>();
function startExploration() {
// 这里可以添加开始探索的逻辑
console.log('开始探索:', selectedStores.value[selectedStoreIndex.value]);
if (!selectedRegion.value || selectedStores.value.length === 0) {
alert('请选择一个区域和店铺!');
return;
}
if ((document.querySelector('.name-input') as HTMLInputElement).value.trim() === '') {
alert('请输入你的名字!');
return;
}
emit('startExploration', {
region: selectedRegion.value,
store: selectedStores.value[selectedStoreIndex.value].店铺,
username: (document.querySelector('.name-input') as HTMLInputElement).value
});
}
// 动画管理
@@ -179,12 +195,12 @@ function createScaleInTextAnimation(element: Element, delay: number = 0) {
// 气泡下降动画
function createBubbleDropAnimation(element: Element, startTop: string, startLeft: string, endTop: string, endLeft: string, delay: number = 0, duration: number = 600) {
const animation = element.animate([
{
{
top: startTop,
left: startLeft,
transform: 'translate3d(-50%, -50%, 0) scale(0.1)'
},
{
{
top: endTop,
left: endLeft,
transform: 'translate3d(-50%, -50%, 0) scale(1)'
@@ -200,8 +216,15 @@ function createBubbleDropAnimation(element: Element, startTop: string, startLeft
return animation;
}
const mainLogo = useTemplateRef('main-logo');
const mainLogoRules = ref([
{ name: 'main', frame: 41, loop: 1, pauseAfter: true, duration: 33, reverse: false }
]);
// 反向播放所有动画来淡出
function reverseAllAnimations() {
mainLogoRules.value[0].reverse = true;
allAnimations.forEach(animation => {
// 确保动画已经完成
if (animation.playState === 'finished') {
@@ -269,11 +292,11 @@ onMounted(() => {
const startLeft = `${region.l / 2 + 50}%`;
const endTop = `${region.t + 50}%`;
const endLeft = `${region.l + 50}%`;
// 设置初始位置
(bubble as HTMLElement).style.top = startTop;
(bubble as HTMLElement).style.left = startLeft;
createBubbleDropAnimation(bubble, startTop, startLeft, endTop, endLeft, 0, 600 + region.ad * 100);
// 点击事件
@@ -287,13 +310,13 @@ onMounted(() => {
fill: 'forwards'
});
}
const pUp = () => {
// 设置选中的区域和店铺数据
selectedRegion.value = region.tt;
selectedStores.value = getRegionStores(region.key);
selectedStoreIndex.value = 0;
bubble.querySelector('span')?.animate([
{ opacity: 1 },
{ opacity: 0 },
@@ -302,7 +325,7 @@ onMounted(() => {
easing: 'ease-in-out',
fill: 'forwards'
});
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%' },
@@ -331,14 +354,14 @@ onMounted(() => {
// 动画完成后显示选择界面
scaleAnimation.addEventListener('finish', () => {
showStoreSelector.value = true;
// 初始化 Intersection Observer
initializeStoreSelector();
});
bubble.removeEventListener('pointerdown', pDown);
}
bubble.addEventListener('pointerdown', pDown);
bubble.addEventListener('pointerup', pUp, { once: true });
});
@@ -356,65 +379,14 @@ onUnmounted(() => {
<section class="p1">
<img :src="assets.标准logo" alt="" class="logo" style="top: 6%;left: 8%;width: 12%;opacity: 0;">
<img :src="assets.p1.蓝天" alt="" class="bg">
<div style="height: 12vw;top: 11.4%;left: 6%">
<span class="green scale-in-ctn" style="border-radius: 6vw;font-size: 9vw; padding: 0 3vw;height: 100%;transform: scale(0);--base-delay: 0s;">
<span class="scale-in-txt" style="transform: scale(0);">太阳脸</span>
</span>
<span class="green fake scale-in-txt" style="border-radius: 6vw;font-size: 9vw; padding: 0 3vw;height: 100%;transform: scale(0);--base-delay: 0s;">
<span class="scale-in-txt" style="transform: scale(0);">太阳脸</span>
</span>
<span class="green scale-in-ctn"
style="width: 12vw;border-radius: 5vw; padding: 2vw 2vw 2vw 3vw; left: 34vw;height: 100%;transform: scale(0);--base-delay: 0s;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 91.18 90.18" preserveAspectRatio="xMidYMid meet">
<path fill-rule="evenodd" stroke="rgb(255, 255, 255)" stroke-width="12.12px" stroke-linecap="round"
stroke-linejoin="round" fill="none" d="M13.003,13.014 L13.003,71.540 L72.429,71.540 " />
<path fill-rule="evenodd" stroke="rgb(255, 255, 255)" stroke-width="12.12px" stroke-linecap="round"
stroke-linejoin="round" fill="none" d="M60.428,21.892 L14.854,70.801 " />
</svg>
</span>
<span class="green fake scale-in-txt"
style="width: 12vw;border-radius: 5vw; padding: 2vw 2vw 2vw 3vw; left: 34vw;height: 100%;transform: scale(0);--base-delay: 0s;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 91.18 90.18" preserveAspectRatio="xMidYMid meet">
<path fill-rule="evenodd" stroke="rgb(255, 255, 255)" stroke-width="12.12px" stroke-linecap="round"
stroke-linejoin="round" fill="none" d="M13.003,13.014 L13.003,71.540 L72.429,71.540 " />
<path fill-rule="evenodd" stroke="rgb(255, 255, 255)" stroke-width="12.12px" stroke-linecap="round"
stroke-linejoin="round" fill="none" d="M60.428,21.892 L14.854,70.801 " />
</svg>
</span>
<AniEle :url="assets.ani.主标出现" :width="1015" :height="336" :rules="mainLogoRules"
style="top: 10%;width: 84%;left: 8%;" ref="main-logo" />
</div>
<div style="left: 12%;top: 17.2%;width: 73vw;height: 7%;">
<span class="black scale-in-ctn" style="border-radius: 7vw;height: 100%;width: 100%;transform: scale(0);--base-delay:0.1s">
<span>弹性福利之旅</span>
</span>
<span class="black fake scale-in-txt" style="border-radius: 7vw;height: 100%;width: 100%;transform: scale(0);--base-delay:0.1s">
<span>弹性福利之旅</span>
</span>
<span class="green scale-in-ctn"
style="padding: 0 1.5vw;top: -17%;font-size: 3.5vw;right: -6%;height: 30%;border-radius: 3vw;transform: scale(0);--base-delay:0.2s">
<span class="elan block relative">TOUR</span>
</span>
<span class="green fake scale-in-txt"
style="padding: 0 1.5vw;top: -17%;font-size: 3.5vw;right: -6%;height: 30%;border-radius: 3vw;transform: scale(0);--base-delay:0.2s">
<span class="elan block relative">TOUR</span>
</span>
</div>
<div style="top: 11.7%;left: 53%; display: flex; flex-direction: column;">
<span class="white relative swap-in" style="left: 30%;height: 5.5vw;clip-path: inset(0 100% 0 0);--base-delay:0.6s">
<span class="elan">FLEXIBEL</span>
</span>
<span class="white relative swap-in" style="height: 5.5vw;clip-path: inset(0 100% 0 0);--base-delay:0.9s">
<span class="elan">BENEFITS</span>
</span>
</div>
<!-- <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 + '%',
width: i.w + '%',
height: i.h + '%',
transform: 'translate3d(-50%, -50%, 0) scale(0)',
}" class="region-bubble">
@@ -429,64 +401,62 @@ onUnmounted(() => {
</div>
<div style="color: white; bottom: 16%;left: 50%;transform: translateX(-50%); font-size: 4.5vw;">
<span class="block tag scale-in-ctn" style="background-color: #000;top: -1vw;left: -12vw;color: transparent;transform: scale(0);--base-delay:1s;">
<span class="block tag scale-in-ctn"
style="background-color: #000;top: -1vw;left: -12vw;color: transparent;transform: scale(0);--base-delay:1s;">
<span>点击</span>
</span>
<span class="block tag scale-in-ctn" style="background-color: #304A7B;top: 4vw;left: -6vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
<span class="block tag scale-in-ctn"
style="background-color: #304A7B;top: 4vw;left: -6vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
<span>选择</span>
</span>
<span class="block tag scale-in-ctn" style="background-color: #8FBE56;top: .8vw;left: 4.5vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
<span class="block tag scale-in-ctn"
style="background-color: #8FBE56;top: .8vw;left: 4.5vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
<span>阵营</span>
</span>
<span class="block tag scale-in-txt" style="background-color: transparent;top: -1vw;left: -12vw;transform: scale(0);--base-delay:1.2s;">
<span class="block tag scale-in-txt"
style="background-color: transparent;top: -1vw;left: -12vw;transform: scale(0);--base-delay:1.2s;">
<span>点击</span>
</span>
<span class="block tag scale-in-txt" style="background-color: transparent;top: 4vw;left: -6vw;transform: scale(0);--base-delay:1.2s;">
<span class="block tag scale-in-txt"
style="background-color: transparent;top: 4vw;left: -6vw;transform: scale(0);--base-delay:1.2s;">
<span>选择</span>
</span>
<span class="block tag scale-in-txt" style="background-color: transparent;top: .8vw;left: 4.5vw;transform: scale(0);--base-delay:1.3s;">
<span class="block tag scale-in-txt"
style="background-color: transparent;top: .8vw;left: 4.5vw;transform: scale(0);--base-delay:1.3s;">
<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 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">
<div class="name-input-section relative">
<div class="title relative">请输入你的名字</div>
<div class="input-container relative">
<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 class="action-buttons relative">
<div class="start-button relative" @click="startExploration">
</div>
</div>
</div>
</div>
@@ -579,22 +549,20 @@ img {
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.store-selector-content {
top: 15%;
left: 5%;
width: 90%;
max-width: 400px;
text-align: center;
color: #333;
}
.region-title {
font-size: 6vw;
font-weight: bold;
margin-bottom: 8vw;
font-weight: 400;
margin-bottom: 6vw;
color: #333;
}
@@ -603,20 +571,16 @@ img {
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%
);
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 {
@@ -628,7 +592,8 @@ img {
}
.scroll-padding {
height: calc(20vh - 4vw); /* 容器高度的一半减去项目高度的一半 */
height: calc(20vh - 4vw);
/* 容器高度的一半减去项目高度的一半 */
flex-shrink: 0;
}
@@ -662,8 +627,6 @@ img {
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);
@@ -674,8 +637,9 @@ img {
margin-bottom: 8vw;
}
.name-input-section h3 {
.name-input-section .title {
font-size: 5vw;
font-weight: 600;
color: #333;
margin-bottom: 4vw;
font-weight: normal;
@@ -696,26 +660,10 @@ img {
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;
background: url('../assets/p1/输入框.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.action-buttons {
@@ -726,17 +674,13 @@ img {
}
.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;
width: 40vw;
height: 15vw;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
background-image: url('../assets/p1/开始探索.png');
transition: all 0.3s ease;
}
+2 -2
View File
@@ -1,9 +1,9 @@
@import "tailwindcss";
@import "./assets/fonts/stylesheet.css";
body {
margin: 0;
font-family: 'Noto Serif SC Variable' sans-serif;
font-family: 'Noto Sans SC Variable', sans-serif;
user-select: none;
touch-action: none;
}
+5
View File
@@ -1 +1,6 @@
/// <reference types="vite/client" />
// JSZip类型定义
declare module 'jszip' {
export default JSZip
}