refact game.vue
This commit is contained in:
parent
3a524f02a0
commit
7e9847f287
@ -5,7 +5,7 @@ import Page1 from './pages/Page1.vue';
|
||||
import assets from './assets';
|
||||
import Loader from './pages/Loader.vue';
|
||||
|
||||
const stage = ref(-1);
|
||||
const stage = ref(1);
|
||||
|
||||
const userData = ref({
|
||||
region: '奥莱',
|
||||
|
||||
BIN
src/assets/ani/答题标题.zip
Normal file
BIN
src/assets/ani/答题标题.zip
Normal file
Binary file not shown.
@ -30,6 +30,7 @@ export default {
|
||||
结尾主标: new URL('./ani/结尾主标.zip', import.meta.url).href,
|
||||
海报p1: new URL('./ani/海报p1.zip', import.meta.url).href,
|
||||
海报p2: new URL('./ani/海报p2.zip', import.meta.url).href,
|
||||
答题标题: new URL('./ani/答题标题.zip', import.meta.url).href,
|
||||
},
|
||||
icons: [
|
||||
[new URL('./icons/icon1.webp', import.meta.url).href, new URL('./icons/bigicon1.webp', import.meta.url).href],
|
||||
|
||||
@ -6,6 +6,8 @@ import 弹簧蓄力下拉音效 from "./弹簧蓄力下拉音效.mp3"
|
||||
import 风声 from "./风声.mp3"
|
||||
import 标题出现 from "./首页标题、尾标出现音效.mp3"
|
||||
import 结算 from "./最终结算音效.mp3"
|
||||
import 正确 from "./正确.mp3"
|
||||
import 错误 from "./错误.mp3"
|
||||
|
||||
const ea = new EasyAudio([
|
||||
{name: "按钮音效", audioUrl: 按钮音效, volume: 0.5},
|
||||
@ -14,6 +16,8 @@ const ea = new EasyAudio([
|
||||
{name: "风声", audioUrl: 风声, volume: 0.5},
|
||||
{name: "标题出现", audioUrl: 标题出现, volume: 0.5},
|
||||
{name: "结算", audioUrl: 结算, volume: 0.5},
|
||||
{name: "正确", audioUrl: 正确, volume: 0.5},
|
||||
{name: "错误", audioUrl: 错误, volume: 0.5},
|
||||
]);
|
||||
|
||||
export default ea;
|
||||
BIN
src/assets/sounds/正确.mp3
Normal file
BIN
src/assets/sounds/正确.mp3
Normal file
Binary file not shown.
BIN
src/assets/sounds/错误.mp3
Normal file
BIN
src/assets/sounds/错误.mp3
Normal file
Binary file not shown.
@ -460,4 +460,173 @@ export const regions: RegionData = {
|
||||
"备注": "DFT0001-三亚海棠湾免税店"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 定义题目对象的接口,以确保数据类型的一致性
|
||||
export interface QuizQuestion {
|
||||
id: number; // 题目唯一ID
|
||||
type: 'single' | 'multiple'; // 题目类型:'single' 为单选, 'multiple' 为多选
|
||||
question: string; // 题目问题
|
||||
options: string[]; // 所有选项
|
||||
answers: string[]; // 正确答案(单选也使用数组,方便统一处理)
|
||||
}
|
||||
|
||||
// ARC Retail 弹性福利平台答题竞赛题库
|
||||
export const arcRetailQuiz: QuizQuestion[] = [
|
||||
// --- 一、单选题 ---
|
||||
{
|
||||
id: 1,
|
||||
type: 'single',
|
||||
question: '弹性福利平台的核心功能是?',
|
||||
options: [
|
||||
'发放基本工资',
|
||||
'实现福利积分兑换',
|
||||
'进行绩效评估',
|
||||
'规划职业发展',
|
||||
],
|
||||
answers: ['实现福利积分兑换'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'single',
|
||||
question: '弹性福利平台上线后,健康福利的变化是?',
|
||||
options: [
|
||||
'只能选体检',
|
||||
'可在体检和牙齿清洁中自主选择',
|
||||
'同时免费领体检 + 洁牙',
|
||||
'取消健康福利',
|
||||
],
|
||||
answers: ['可在体检和牙齿清洁中自主选择'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'single',
|
||||
question: '员工想知道自己有多少福利积分,最便捷的方式是?',
|
||||
options: [
|
||||
'每月找 HR 查',
|
||||
'登录弹性福利平台实时查看',
|
||||
'等年底邮件通知',
|
||||
'问同事打听',
|
||||
],
|
||||
answers: ['登录弹性福利平台实时查看'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'single',
|
||||
question: '以下哪项是 2025 年 新增 的福利兑换项目?',
|
||||
options: ['节日礼品', '员工洁牙', '法定社保', '商业保险'],
|
||||
answers: ['员工洁牙'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'single',
|
||||
question: '福利积分 主要通过什么方式获取?',
|
||||
options: ['日常加班', '参与景仰计划', '绩效满分', '达成销售业绩'],
|
||||
answers: ['参与景仰计划'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'single',
|
||||
question: '员工每年最多可以参加多少次公司组织的景仰计划?',
|
||||
options: ['1次', '2次', '3次', '无限制'],
|
||||
answers: ['2次'],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: 'single',
|
||||
question: '如果觉得近期压力大,心情低落,哪一项解决路径是公司平台提供的?',
|
||||
options: [
|
||||
'可以求助公司专业的EAP服务',
|
||||
'和朋友吐苦水',
|
||||
'大吃大喝',
|
||||
'睡一觉',
|
||||
],
|
||||
answers: ['可以求助公司专业的EAP服务'],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: 'single',
|
||||
question: '员工商保属于哪类福利?',
|
||||
options: ['法定福利', '企业补充福利', '带薪休假', '绩效奖金'],
|
||||
answers: ['企业补充福利'],
|
||||
},
|
||||
|
||||
// --- 二、多选题 ---
|
||||
{
|
||||
id: 9,
|
||||
type: 'multiple',
|
||||
question: '弹性福利平台可兑换的福利包含哪些?',
|
||||
options: ['员工体检', '员工洁牙', '节日礼包', '基本工资'],
|
||||
answers: ['员工体检', '员工洁牙', '节日礼包'],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
type: 'multiple',
|
||||
question: '关于弹性福利平台,这些 “新体验” 是真的!',
|
||||
options: ['福利能自己选', '积分随时看', '福利都搬到线上', '只有经理能使用'],
|
||||
answers: ['福利能自己选', '积分随时看', '福利都搬到线上'],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
type: 'multiple',
|
||||
question: '家属也能通过弹性福利平台享受哪些福利?',
|
||||
options: ['家属商保自选', '家属体检自选', '家属洁牙自选', '员工生日积分'],
|
||||
answers: ['家属商保自选', '家属体检自选', '家属洁牙自选'],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
type: 'multiple',
|
||||
question: '2025 年,福利积分新增了哪些类型?',
|
||||
options: ['生日积分', '周年积分', '长期服务积分', '景仰积分'],
|
||||
answers: ['生日积分', '周年积分', '长期服务积分', '景仰积分'],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
type: 'multiple',
|
||||
question: '弹性福利平台上线后,福利的 “获取和使用” 发生了哪些改变?',
|
||||
options: [
|
||||
'获取:从 “等公司发”→“参与景仰计划赚积分”',
|
||||
'使用:从 “被动接受固定福利”→“自主选想要的福利”',
|
||||
'管理:从 “找 HR 咨询”→“平台随时查、限时兑”',
|
||||
'福利变少了',
|
||||
],
|
||||
answers: [
|
||||
'获取:从 “等公司发”→“参与景仰计划赚积分”',
|
||||
'使用:从 “被动接受固定福利”→“自主选想要的福利”',
|
||||
'管理:从 “找 HR 咨询”→“平台随时查、限时兑”',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
type: 'multiple',
|
||||
question: '景仰计划包含哪些层级的活动?',
|
||||
options: [
|
||||
'T1最美鸟人,面向优秀员工',
|
||||
'T2探界鸟人,硬核挑战,自主报名',
|
||||
'T3领航鸟人,领导力发展,面向店铺管理',
|
||||
'T4起翼新鸟,新手入门,自主报名',
|
||||
],
|
||||
answers: [
|
||||
'T1最美鸟人,面向优秀员工',
|
||||
'T2探界鸟人,硬核挑战,自主报名',
|
||||
'T3领航鸟人,领导力发展,面向店铺管理',
|
||||
'T4起翼新鸟,新手入门,自主报名',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
type: 'multiple',
|
||||
question: '员工360关爱计划(EAP)的联系途径有哪些?',
|
||||
options: [
|
||||
'400电话(400 920 3300)',
|
||||
'微信公众号(职选)',
|
||||
'网站(https://global.helpwhereyouare.com)',
|
||||
'小红书',
|
||||
],
|
||||
answers: [
|
||||
'400电话(400 920 3300)',
|
||||
'微信公众号(职选)',
|
||||
'网站(https://global.helpwhereyouare.com)',
|
||||
],
|
||||
},
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
79
src/pages/Game/ActionArea.vue
Normal file
79
src/pages/Game/ActionArea.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useTemplateRef, type Ref } from 'vue';
|
||||
|
||||
const gPos: Ref<'left' | 'center' | 'right'> = ref('center');
|
||||
const gDeg = computed(() => ({
|
||||
left: 294 - 360,
|
||||
center: 326 - 360,
|
||||
right: 8
|
||||
})[gPos.value]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'gToggle', pos: 'left' | 'center' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const pMachine = useTemplateRef('pMachine');
|
||||
|
||||
function gToggle(pos: 'left' | 'center' | 'right') {
|
||||
gPos.value = pos;
|
||||
pMachine.value?.animate([
|
||||
{ scale: 1, },
|
||||
{ scale: 1.2, },
|
||||
{ scale: 1, },
|
||||
], {
|
||||
duration: 200,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
});
|
||||
|
||||
emit('gToggle', pos);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
hide: () => {
|
||||
(document.querySelector('.p-machine') as HTMLDivElement).style.animationDirection = 'reverse';
|
||||
(document.querySelectorAll('.action img') as NodeListOf<HTMLImageElement>).forEach(img => {
|
||||
img.style.animationDirection = 'reverse';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<img src="../../assets/game/鼓风机.webp" class="abs p-machine" ref="pMachine"
|
||||
style="bottom: -40%; width: 66vw; left: 17%; transform-origin: 50% 63%; transition: all 0.2s;"
|
||||
:style="{ transform: `rotate(${gDeg}deg)` }" />
|
||||
<div class="action">
|
||||
<img draggable="false" :style="{ animationDelay: '0.1s' }" src="../../assets/game/左.webp" alt=""
|
||||
@click="gToggle('left')">
|
||||
<img draggable="false" :style="{ animationDelay: '0.2s' }" src="../../assets/game/中.webp" alt=""
|
||||
@click="gToggle('center')">
|
||||
<img draggable="false" :style="{ animationDelay: '0.0s' }" src="../../assets/game/右.webp" alt=""
|
||||
@click="gToggle('right')">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.p-machine {
|
||||
animation: warp-in 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 19vw;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
gap: 2vw;
|
||||
|
||||
img {
|
||||
flex: 1;
|
||||
width: 28vw;
|
||||
scale: 0;
|
||||
animation: scale-in 0.4s ease-in-out forwards;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
260
src/pages/Game/GatheringCloud.vue
Normal file
260
src/pages/Game/GatheringCloud.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<style scoped>
|
||||
.wecare-title {
|
||||
scale: 0;
|
||||
}
|
||||
|
||||
.icon-cloud img {
|
||||
position: absolute;
|
||||
width: 16vw;
|
||||
transition: all 4s ease;
|
||||
animation: scale-in 0.4s ease-in-out forwards;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: left top;
|
||||
scale: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="icon-cloud" style="position: absolute; left: 50%; top: 20%;" ref="icon-cloud">
|
||||
<img v-for="icon in floatIcons" :src="icon.src" alt="" :style="{
|
||||
top: `${icon.top}vw`,
|
||||
left: `${icon.left}vw`,
|
||||
animationDelay: `${icon.delay}ms`,
|
||||
}">
|
||||
</div>
|
||||
|
||||
<img src="../../assets/game/wecare标题.webp" alt="" class="abs wecare-title" style="width: 72%;left: 14%;top: 12%;"
|
||||
ref="title-img">
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, useTemplateRef } from 'vue';
|
||||
|
||||
const iconCloud = useTemplateRef('icon-cloud');
|
||||
const titleImg = useTemplateRef('title-img');
|
||||
|
||||
import assets from '../../assets';
|
||||
|
||||
const giftList = assets.icons;
|
||||
|
||||
|
||||
defineExpose({
|
||||
init: () => {
|
||||
// 初始化时生成图标位置
|
||||
floatIcons.value = generateIconPositions();
|
||||
},
|
||||
gather: async () => {
|
||||
const finalPositions = simulateGatherIcons();
|
||||
await new Promise(resolve => setTimeout(resolve, 0)); // 等待动画完成
|
||||
floatIcons.value = finalPositions;
|
||||
|
||||
},
|
||||
titleIn: async () => {
|
||||
await iconCloud.value?.animate([
|
||||
{ scale: 1 },
|
||||
{ scale: 0 },
|
||||
], {
|
||||
duration: 400,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
}).finished;
|
||||
await titleImg.value?.animate([
|
||||
{ scale: 0 },
|
||||
{ scale: 1 },
|
||||
], {
|
||||
duration: 400,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
}).finished;
|
||||
},
|
||||
titleOut: async () => {
|
||||
await titleImg.value?.animate([
|
||||
{ scale: 1 },
|
||||
{ scale: 0 },
|
||||
], {
|
||||
duration: 400,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
}).finished;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 生成图标位置,确保之间距离不小于16
|
||||
const generateIconPositions = () => {
|
||||
const icons = [...giftList, ...assets.metal];
|
||||
const positions: Array<{ src: string; left: number; top: number; delay: number }> = [];
|
||||
const minDistance = 16; // 最小距离
|
||||
const maxAttempts = 100; // 防止无限循环的最大尝试次数
|
||||
|
||||
// 计算两点之间的距离
|
||||
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
|
||||
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
|
||||
};
|
||||
|
||||
// 检查位置是否与已有位置冲突
|
||||
const isPositionValid = (newPos: { left: number; top: number }) => {
|
||||
return positions.every(existingPos => getDistance(newPos, existingPos) >= minDistance);
|
||||
};
|
||||
|
||||
icons.forEach((icon, index) => {
|
||||
let attempts = 0;
|
||||
let validPosition = false;
|
||||
let newPosition: { left: number; top: number };
|
||||
|
||||
// 尝试生成不冲突的位置
|
||||
while (!validPosition && attempts < maxAttempts) {
|
||||
newPosition = {
|
||||
left: Math.random() * 100 - 50, // 扩大范围以增加成功概率
|
||||
top: Math.random() * 100 - 50
|
||||
};
|
||||
|
||||
if (positions.length === 0 || isPositionValid(newPosition)) {
|
||||
validPosition = true;
|
||||
positions.push({
|
||||
src: icon[0],
|
||||
left: newPosition.left,
|
||||
top: newPosition.top,
|
||||
delay: index * 50
|
||||
});
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
|
||||
// 如果无法找到不冲突的位置,使用稍微宽松的条件
|
||||
if (!validPosition) {
|
||||
console.warn(`Could not find non-conflicting position for icon ${index}, using fallback position`);
|
||||
positions.push({
|
||||
src: icon[0],
|
||||
left: Math.random() * 100 - 50,
|
||||
top: Math.random() * 100 - 50,
|
||||
delay: Math.random() * 5 + index * 0.5
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return positions;
|
||||
};
|
||||
|
||||
const floatIcons = ref(generateIconPositions());
|
||||
|
||||
|
||||
// 图标聚集模拟函数 - 返回最终位置而不实际移动
|
||||
const simulateGatherIcons = () => {
|
||||
const moveSpeed = 0.5; // 移动速度
|
||||
const minDistance = 16; // 最小距离
|
||||
const centerTarget = { left: 0, top: 0 }; // 目标中心点
|
||||
const threshold = 0.1; // 停止移动的阈值
|
||||
const maxIterations = 1000; // 防止无限循环的最大迭代次数
|
||||
|
||||
// 创建位置副本用于模拟
|
||||
const simulatedPositions = floatIcons.value.map(icon => ({
|
||||
src: icon.src,
|
||||
left: icon.left,
|
||||
top: icon.top,
|
||||
delay: icon.delay
|
||||
}));
|
||||
|
||||
// 计算两点之间的距离
|
||||
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
|
||||
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
|
||||
};
|
||||
|
||||
// 检查位置是否与其他图标冲突
|
||||
const isPositionValid = (newPos: { left: number; top: number }, currentIndex: number, positions: typeof simulatedPositions) => {
|
||||
return positions.every((icon, index) => {
|
||||
if (index === currentIndex) return true;
|
||||
return getDistance(newPos, icon) >= minDistance;
|
||||
});
|
||||
};
|
||||
|
||||
// 向目标点移动,但避免碰撞
|
||||
const moveTowardsTarget = (currentPos: { left: number; top: number }, targetPos: { left: number; top: number }, iconIndex: number, positions: typeof simulatedPositions) => {
|
||||
const dx = targetPos.left - currentPos.left;
|
||||
const dy = targetPos.top - currentPos.top;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < threshold) {
|
||||
return currentPos; // 已经足够接近目标
|
||||
}
|
||||
|
||||
// 计算移动方向的单位向量
|
||||
const unitX = dx / distance;
|
||||
const unitY = dy / distance;
|
||||
|
||||
// 计算新位置
|
||||
const newPos = {
|
||||
left: currentPos.left + unitX * moveSpeed,
|
||||
top: currentPos.top + unitY * moveSpeed
|
||||
};
|
||||
|
||||
// 检查新位置是否会产生碰撞
|
||||
if (isPositionValid(newPos, iconIndex, positions)) {
|
||||
return newPos;
|
||||
} else {
|
||||
// 如果会碰撞,尝试绕行
|
||||
const angles = [Math.PI / 4, -Math.PI / 4, Math.PI / 2, -Math.PI / 2, 3 * Math.PI / 4, -3 * Math.PI / 4];
|
||||
|
||||
for (const angle of angles) {
|
||||
const rotatedX = unitX * Math.cos(angle) - unitY * Math.sin(angle);
|
||||
const rotatedY = unitX * Math.sin(angle) + unitY * Math.cos(angle);
|
||||
|
||||
const alternativePos = {
|
||||
left: currentPos.left + rotatedX * moveSpeed * 0.5,
|
||||
top: currentPos.top + rotatedY * moveSpeed * 0.5
|
||||
};
|
||||
|
||||
if (isPositionValid(alternativePos, iconIndex, positions)) {
|
||||
return alternativePos;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有方向都被阻挡,保持原位置
|
||||
return currentPos;
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟聚集过程
|
||||
let iterations = 0;
|
||||
while (iterations < maxIterations) {
|
||||
let hasMovement = false;
|
||||
|
||||
// 创建新的位置数组
|
||||
const newPositions = simulatedPositions.map((icon, index) => {
|
||||
const oldPos = { left: icon.left, top: icon.top };
|
||||
const newPos = moveTowardsTarget(oldPos, centerTarget, index, simulatedPositions);
|
||||
|
||||
// 检查是否有移动
|
||||
if (Math.abs(newPos.left - oldPos.left) > threshold || Math.abs(newPos.top - oldPos.top) > threshold) {
|
||||
hasMovement = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...icon,
|
||||
left: newPos.left,
|
||||
top: newPos.top
|
||||
};
|
||||
});
|
||||
|
||||
// 更新模拟位置
|
||||
simulatedPositions.splice(0, simulatedPositions.length, ...newPositions);
|
||||
|
||||
// 如果没有移动,说明已经收敛
|
||||
if (!hasMovement) {
|
||||
console.log(`图标聚集模拟完成!迭代次数: ${iterations}`);
|
||||
break;
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
if (iterations >= maxIterations) {
|
||||
console.warn('图标聚集模拟达到最大迭代次数限制');
|
||||
}
|
||||
|
||||
// 返回最终位置
|
||||
return simulatedPositions;
|
||||
};
|
||||
|
||||
</script>
|
||||
157
src/pages/Game/LastPage.vue
Normal file
157
src/pages/Game/LastPage.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, useTemplateRef } from 'vue';
|
||||
import AniEle from '../../components/AniEle.vue';
|
||||
import assets from '../../assets';
|
||||
import AudioEffects from '../../assets/sounds'
|
||||
defineProps<{
|
||||
userdata: {
|
||||
region: string;
|
||||
store: string;
|
||||
username: string;
|
||||
},
|
||||
timeSpent: number;
|
||||
fmtTime: (time: number) => string;
|
||||
}>();
|
||||
import QRCode from 'qrcode';
|
||||
const qrcodeUrl = ref('');
|
||||
|
||||
QRCode.toDataURL(`https://arcteryx-game-demo.xn--876a.net`, {
|
||||
width: 256,
|
||||
}).then(url => {
|
||||
qrcodeUrl.value = url;
|
||||
}).catch(err => {
|
||||
console.error('生成二维码失败:', err);
|
||||
});
|
||||
|
||||
const posterP2Ele = useTemplateRef('gui-ani-end-post-p2');
|
||||
|
||||
const emit = defineEmits(['resetGame', 'showShareMask']);
|
||||
|
||||
const hasP2AnimationPlayed = ref(false);
|
||||
onMounted(() => {
|
||||
// 在最后一页显示后设置 Intersection Observer 监听 p2 元素
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !hasP2AnimationPlayed.value) {
|
||||
// p2 进入视口且动画未播放过
|
||||
hasP2AnimationPlayed.value = true;
|
||||
posterP2Ele.value?.jumpTo('p2');
|
||||
console.log('Poster P2 animation triggered');
|
||||
|
||||
// 停止观察,因为只需要触发一次
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.5, // 当 50% 的元素进入视口时触发
|
||||
rootMargin: '0px'
|
||||
});
|
||||
|
||||
// 观察 p2 元素
|
||||
const p2Element = posterP2Ele.value?.$el;
|
||||
if (p2Element) {
|
||||
observer.observe(p2Element);
|
||||
console.log('Started observing P2 element');
|
||||
} else {
|
||||
console.warn('P2 element not found');
|
||||
}
|
||||
|
||||
// 8秒后如果用户没有看过p2,自动滚动到p2
|
||||
setTimeout(() => {
|
||||
if (!hasP2AnimationPlayed.value) {
|
||||
console.log('Auto scrolling to P2 after 8 seconds');
|
||||
const posterContainer = document.querySelector('.poster-container');
|
||||
if (posterContainer) {
|
||||
// 滚动到p2位置(第二个元素,占总宽度的50%)
|
||||
posterContainer.scrollTo({
|
||||
left: posterContainer.scrollWidth * 0.5,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 8000); // 8秒 = 8000毫秒
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="last-page"
|
||||
style="z-index: 0;position: absolute; inset: 0;height: 100%; width: 100%; pointer-events: none;">
|
||||
<AniEle :url="assets.ani.结尾主标" :width="925" :height="340"
|
||||
:rules="[{ name: 'main', frame: 41, loop: 1, pauseAfter: true, duration: 33, reverse: false }]"
|
||||
style="position: absolute;top: 13%;width: 80%;left: 11%; pointer-events:none;" ref="main-logo" />
|
||||
<div class="poster-container"
|
||||
style="position: absolute;top: 29.3%;left: 37.2%;width: 57.3%; height: 38%; overflow-x: auto; overflow-y: hidden; pointer-events: all; scroll-snap-type: x mandatory;">
|
||||
<div class="poster-wrapper" style="display: flex; width: 200%; height: 100%; flex-direction: row;"
|
||||
dir="ltr">
|
||||
<AniEle :url="assets.ani.海报p1" ref="gui-ani-end-post-p1 "
|
||||
style="width: 50%; flex-shrink: 0; scroll-snap-align: start;" class="gui-ani-end-post" :height="940"
|
||||
:width="671" :rules="[
|
||||
{ name: 'p1', frame: 32, loop: 1, pauseAfter: true, duration: 33 },
|
||||
]" />
|
||||
<AniEle :url="assets.ani.海报p2" ref="gui-ani-end-post-p2"
|
||||
style="width: 50%; flex-shrink: 0; scroll-snap-align: start;" class="gui-ani-end-post" :height="940"
|
||||
:width="671" :rules="[
|
||||
{ name: 'wait', frame: 1, loop: 1, pauseAfter: true, duration: 33 },
|
||||
{ name: 'p2', frame: 31, loop: 1, pauseAfter: true, duration: 33 },
|
||||
]" />
|
||||
</div>
|
||||
</div>
|
||||
<AniEle :url="assets.ani.线" ref="gui-ani-end" class="gui-ani-end" :height="2462" :width="1179" :rules="[
|
||||
{ name: 'all', frame: 54, loop: 1, pauseAfter: false, duration: 16 },
|
||||
]" style="width: 100%; height: auto;position: absolute;inset: 0;pointer-events: none;" />
|
||||
<img src="../../assets/game/床.webp" alt="" class="abs last-bed" style="width: 66vw;bottom: -19%;left: 17%;">
|
||||
|
||||
<img src="../../assets/game/分享海报.webp" alt="" class="abs" @click="AudioEffects.play('按钮音效'); emit('showShareMask')"
|
||||
style="width: 34vw;bottom: -19%;left: 15%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; pointer-events: all;">
|
||||
<img src="../../assets/game/再玩一次.webp" alt="" class="abs" @click="AudioEffects.play('按钮音效'); emit('resetGame')"
|
||||
style="width: 34vw;bottom: -19%;left: 51%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; animation-delay: 200ms; pointer-events: all;">
|
||||
|
||||
<div class="time-spent abs" style="left: 10%; top:44%;width: 24%;height: 23%; display: flex;
|
||||
align-items: center; flex-direction: column;">
|
||||
<div class="line-1"
|
||||
style="display: flex; align-items: center; gap: 1vw; animation: line-in 0.5s ease-out 0.6s forwards;transform: translateX(-210%);">
|
||||
<div class="username" style="font-size: 2.9vw;">{{
|
||||
userdata.username }}</div>
|
||||
<div class="cost-info"
|
||||
style="font-size: 2.9vw; color: white; display: flex;align-items: center;justify-content: center;background-color: black;padding: 0 1.2vw; height: 4.5vw; border-radius: 4vw;">
|
||||
用时</div>
|
||||
</div>
|
||||
|
||||
<div class="time"
|
||||
style="font-size: 10vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
|
||||
{{ fmtTime(timeSpent / 1000) }}</div>
|
||||
|
||||
<img :src="qrcodeUrl" alt=""
|
||||
style="width: 100%;bottom:7%;position: absolute;animation: line-in 0.5s ease-out 1s forwards;transform: translateX(-210%);">
|
||||
</div>
|
||||
|
||||
<div class="region-area abs" style="left: 10%; top:30%;width: 24%;height: 10%;gap:2vw; display: flex;animation: line-in 0.5s ease-out 0.4s forwards;transform: translateX(-210%);
|
||||
align-items: center; flex-direction: column;justify-content: center;">
|
||||
<div class="region"
|
||||
style="width: 20vw; background-color: white;font-size: 3vw;text-align: center;height: 5.2vw;line-height: 5.2vw; border-radius: 4.5vw;">
|
||||
{{ userdata.region }}</div>
|
||||
<div class="store" style="font-size: 2.9vw; color: gray;">{{
|
||||
userdata.store }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.poster-container {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-wrapper {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.gui-ani-end-post {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
</style>
|
||||
11
src/pages/Game/Quiz.vue
Normal file
11
src/pages/Game/Quiz.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import assets from '../../assets';
|
||||
import AniEle from '../../components/AniEle.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AniEle :url="assets.ani.答题标题" :width="1000" :height="500" :rules="[
|
||||
{ name: 'main', frame: 60, duration: 33, loop: 1, pauseAfter: true, reverse: false }
|
||||
]" />
|
||||
</template>
|
||||
161
src/pages/Game/ScoreBoard.vue
Normal file
161
src/pages/Game/ScoreBoard.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
leaderBoard: Array<{
|
||||
name: string;
|
||||
time: number;
|
||||
region: string;
|
||||
store?: string;
|
||||
}>;
|
||||
fmtTime: (time: number) => string;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scoreboard">
|
||||
<div class="bar-container">
|
||||
<div class="bar" v-for="(item, index) in [leaderBoard[1], leaderBoard[0], leaderBoard[2]]" :key="index"
|
||||
:class="{
|
||||
'first': index === 1, 'second': index === 0, 'third': index === 2
|
||||
}">
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div class="time">{{ fmtTime(item.time) }}</div>
|
||||
<div class="region">{{ item.region }}</div>
|
||||
<div class="store">{{ item.store }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lines">
|
||||
<div class="line" v-for="(item, index) in leaderBoard.slice(3, 7)" :key="index"
|
||||
:style="{ animationDelay: index * 100 + 400 + 'ms' }">
|
||||
<div class="rank">{{ index + 4 }}</div>
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div class="time">{{ fmtTime(item.time) }}</div>
|
||||
<div class="region">{{ item.region }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.scoreboard {
|
||||
position: absolute;
|
||||
top: 44%;
|
||||
width: 100%;
|
||||
|
||||
.bar-container {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 2vw;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: relative;
|
||||
height: 60vw;
|
||||
width: 26vw;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
mask-image: linear-gradient(to bottom, black, black, transparent);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5vw;
|
||||
animation: bar-in 0.7s ease-out forwards;
|
||||
transform: translateY(150%);
|
||||
|
||||
.name {
|
||||
font-size: 4.5vw;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 3vw;
|
||||
}
|
||||
|
||||
.region {
|
||||
font-size: 3vw;
|
||||
color: white;
|
||||
background-color: #7EA0CA;
|
||||
padding: 0 2vw;
|
||||
border-radius: 2vw;
|
||||
}
|
||||
|
||||
.store {
|
||||
max-width: 13vw;
|
||||
text-align: center;
|
||||
font-size: 2.4vw;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
&.first {
|
||||
top: 3vw;
|
||||
background-image: url('../../assets/game/first.webp');
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
&.second {
|
||||
background-image: url('../../assets/game/second.webp');
|
||||
animation-delay: 250ms;
|
||||
top: 9vw;
|
||||
}
|
||||
|
||||
&.third {
|
||||
background-image: url('../../assets/game/third.webp');
|
||||
animation-delay: 500ms;
|
||||
top: 9vw;
|
||||
}
|
||||
}
|
||||
|
||||
.lines {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: -5vw;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
font-size: 3.2vw;
|
||||
background-color: #FFFFFF;
|
||||
padding: 0 2vw;
|
||||
height: 8vw;
|
||||
border-radius: 8vw;
|
||||
animation: line-in 0.5s ease-out forwards;
|
||||
transform: translateX(-150%);
|
||||
|
||||
.rank {
|
||||
// italic
|
||||
font-style: italic;
|
||||
flex: 1;
|
||||
font-size: 4vw;
|
||||
text-align: center;
|
||||
color: #7EA0CA;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 2;
|
||||
text-align: center;
|
||||
border-right: .35vw solid black;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex: 2;
|
||||
text-align: center;
|
||||
border-right: .35vw solid black;
|
||||
}
|
||||
|
||||
.region {
|
||||
flex: 2;
|
||||
color: gray;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user