finish quiz
@ -106,15 +106,15 @@ app.post('/api/score', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/score - 获取排行榜(前6名)
|
// GET /api/score - 获取排行榜(前8名)
|
||||||
app.get('/api/score', async (req, res) => {
|
app.get('/api/score', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scores = await readScores();
|
const scores = await readScores();
|
||||||
|
|
||||||
// 按时间排序(时间短的在前)并取前6名
|
// 按时间排序(时间短的在前)并取前8名
|
||||||
const leaderBoard = scores
|
const leaderBoard = scores
|
||||||
.sort((a, b) => a.time - b.time)
|
.sort((a, b) => a.time - b.time)
|
||||||
.slice(0, 6)
|
.slice(0, 8)
|
||||||
.map(score => ({
|
.map(score => ({
|
||||||
name: score.name,
|
name: score.name,
|
||||||
time: score.time,
|
time: score.time,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Page1 from './pages/Page1.vue';
|
|||||||
import assets from './assets';
|
import assets from './assets';
|
||||||
import Loader from './pages/Loader.vue';
|
import Loader from './pages/Loader.vue';
|
||||||
|
|
||||||
const stage = ref(1);
|
const stage = ref(-1);
|
||||||
|
|
||||||
const userData = ref({
|
const userData = ref({
|
||||||
region: '奥莱',
|
region: '奥莱',
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
import os
|
|
||||||
from PIL import Image
|
|
||||||
import re
|
|
||||||
|
|
||||||
# --- 配置 ---
|
|
||||||
# 目标尺寸
|
|
||||||
TARGET_WIDTH = 670
|
|
||||||
TARGET_HEIGHT = 939
|
|
||||||
|
|
||||||
# 裁剪区域 (left, upper, right, lower)
|
|
||||||
# 我们从左上角 (0, 0) 开始,裁剪出一个 670x939 的区域
|
|
||||||
CROP_BOX = (0, 0, TARGET_WIDTH, TARGET_HEIGHT)
|
|
||||||
|
|
||||||
# 输出文件夹名称
|
|
||||||
OUTPUT_DIR = "cropped"
|
|
||||||
# --- 结束配置 ---
|
|
||||||
|
|
||||||
def batch_crop_images():
|
|
||||||
"""
|
|
||||||
批量裁剪当前目录下的 WebP 图片
|
|
||||||
"""
|
|
||||||
# 获取当前脚本所在的目录
|
|
||||||
current_dir = os.getcwd()
|
|
||||||
output_path = os.path.join(current_dir, OUTPUT_DIR)
|
|
||||||
|
|
||||||
# 如果输出目录不存在,则创建它
|
|
||||||
if not os.path.exists(output_path):
|
|
||||||
os.makedirs(output_path)
|
|
||||||
print(f"已创建输出目录: {output_path}")
|
|
||||||
|
|
||||||
# 获取目录下所有文件
|
|
||||||
files = os.listdir(current_dir)
|
|
||||||
|
|
||||||
# 定义一个正则表达式来匹配 '数字.webp' 格式的文件名
|
|
||||||
file_pattern = re.compile(r"^\d+\.webp$")
|
|
||||||
|
|
||||||
image_files_to_process = [f for f in files if file_pattern.match(f)]
|
|
||||||
|
|
||||||
if not image_files_to_process:
|
|
||||||
print("未在当前目录找到符合 '数字.webp' 格式的图片文件。")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"找到了 {len(image_files_to_process)} 个待处理的图片文件。")
|
|
||||||
|
|
||||||
# 遍历所有符合条件的图片文件
|
|
||||||
for filename in image_files_to_process:
|
|
||||||
try:
|
|
||||||
# 构建完整的文件路径
|
|
||||||
file_path = os.path.join(current_dir, filename)
|
|
||||||
|
|
||||||
# 打开图片
|
|
||||||
with Image.open(file_path) as img:
|
|
||||||
print(f"正在处理: {filename} (原始尺寸: {img.size[0]}x{img.size[1]})")
|
|
||||||
|
|
||||||
# 检查原始尺寸是否符合预期
|
|
||||||
if img.size != (671, 940):
|
|
||||||
print(f" -> 警告: {filename} 的尺寸 ({img.size[0]}x{img.size[1]}) 与预期的 671x940 不符,已跳过。")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 进行裁剪
|
|
||||||
cropped_img = img.crop(CROP_BOX)
|
|
||||||
|
|
||||||
# 构建输出文件路径
|
|
||||||
save_path = os.path.join(output_path, filename)
|
|
||||||
|
|
||||||
# 保存裁剪后的图片
|
|
||||||
# quality=100 可以尽量保持高质量,你可以根据需要调整
|
|
||||||
cropped_img.save(save_path, "WEBP", quality=100)
|
|
||||||
|
|
||||||
print(f" -> 已裁剪并保存至: {save_path} (新尺寸: {cropped_img.size[0]}x{cropped_img.size[1]})")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"处理 {filename} 时发生错误: {e}")
|
|
||||||
|
|
||||||
print("\n所有图片处理完成!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
batch_crop_images()
|
|
||||||
|
Before Width: | Height: | Size: 57 KiB |
BIN
src/assets/game/share_mask.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/game/对.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/game/对.webp
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
src/assets/game/确认按钮.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/game/答题卡片背景.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src/assets/game/罚时.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/game/罚时.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/game/请选择你的答案.webp
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/game/错.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/game/错.webp
Normal file
|
After Width: | Height: | Size: 790 B |
BIN
src/assets/game/闹钟.webp
Normal file
|
After Width: | Height: | Size: 978 B |
@ -20,7 +20,7 @@ export default {
|
|||||||
ani:{
|
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,
|
后段效果: 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,
|
||||||
P1太阳总: new URL('./ani/P1太阳总.zip', import.meta.url).href,
|
P1太阳总: new URL('./ani/P1太阳总.zip', import.meta.url).href,
|
||||||
|
|||||||
@ -53,7 +53,6 @@ const currentLoopCount = ref(0)
|
|||||||
const animationId = ref<number>()
|
const animationId = ref<number>()
|
||||||
const lastFrameTime = ref(0)
|
const lastFrameTime = ref(0)
|
||||||
const pendingJumpTo = ref<string>()
|
const pendingJumpTo = ref<string>()
|
||||||
// 【新增】: 用于存储 soft jump 的 Promise resolve 函数
|
|
||||||
const pendingJumpResolver = ref<((success: boolean) => void) | null>(null)
|
const pendingJumpResolver = ref<((success: boolean) => void) | null>(null)
|
||||||
|
|
||||||
|
|
||||||
@ -118,30 +117,45 @@ const loadImageSequence = async (zipUrl: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAllImages 函数 (不变)
|
// loadAllImages 函数 (已修改)
|
||||||
const loadAllImages = async () => {
|
const loadAllImages = async () => {
|
||||||
const imageElements: HTMLImageElement[] = []
|
const imageElements: HTMLImageElement[] = []
|
||||||
|
let dimensionsSet = false; // 用于确保仅从第一个成功加载的图像设置尺寸
|
||||||
|
|
||||||
for (let i = 0; i < images.value.length; i++) {
|
for (let i = 0; i < images.value.length; i++) {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.src = images.value[i]
|
img.src = images.value[i]
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve) => {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
if (i === 0) {
|
// 如果尺寸尚未设置,则使用第一个成功加载的图像的尺寸
|
||||||
|
if (!dimensionsSet) {
|
||||||
canvasWidth.value = props.width || img.width
|
canvasWidth.value = props.width || img.width
|
||||||
canvasHeight.value = props.height || img.height
|
canvasHeight.value = props.height || img.height
|
||||||
|
dimensionsSet = true
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
img.onerror = reject
|
img.onerror = () => {
|
||||||
|
// 当图像加载失败时,发出警告而不是中断整个过程
|
||||||
|
console.warn(`[Animation Component] Image at index ${i} failed to load. It will be rendered as a transparent frame.`);
|
||||||
|
// 即使出错也 resolve,以允许加载序列继续
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
imageElements.push(img)
|
imageElements.push(img)
|
||||||
progress.value = 80 + (i + 1) / images.value.length * 20
|
progress.value = 80 + (i + 1) / images.value.length * 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dimensionsSet) {
|
||||||
|
console.warn("[Animation Component] All images failed to load. Using specified or default canvas dimensions.");
|
||||||
|
}
|
||||||
|
|
||||||
loadedImages.value = imageElements
|
loadedImages.value = imageElements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// drawFrame 函数 (不变)
|
// drawFrame 函数 (不变)
|
||||||
const drawFrame = (frameIndex: number) => {
|
const drawFrame = (frameIndex: number) => {
|
||||||
if (!canvasRef.value || !loadedImages.value.length) return
|
if (!canvasRef.value || !loadedImages.value.length) return
|
||||||
@ -153,21 +167,19 @@ const drawFrame = (frameIndex: number) => {
|
|||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
const img = loadedImages.value[safeIndex]
|
const img = loadedImages.value[safeIndex]
|
||||||
|
// 如果img加载失败,它仍然是一个Image对象,但drawImage不会绘制任何内容,
|
||||||
|
// 从而使该帧保持透明,这正是我们想要的效果。
|
||||||
if (img) {
|
if (img) {
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 核心修改区域 1: jumpToRule 函数
|
|
||||||
// =================================================================
|
|
||||||
const jumpToRule = (ruleName: string): boolean => {
|
const jumpToRule = (ruleName: string): boolean => {
|
||||||
const ruleIndex = findRuleIndex(ruleName)
|
const ruleIndex = findRuleIndex(ruleName)
|
||||||
if (ruleIndex === -1) {
|
if (ruleIndex === -1) {
|
||||||
console.warn(`未找到名为 "${ruleName}" 的规则`)
|
console.warn(`未找到名为 "${ruleName}" 的规则`)
|
||||||
// 如果有待处理的Promise,则拒绝它
|
|
||||||
if (pendingJumpResolver.value) {
|
if (pendingJumpResolver.value) {
|
||||||
pendingJumpResolver.value(false); // resolve(false) 表示失败
|
pendingJumpResolver.value(false);
|
||||||
pendingJumpResolver.value = null;
|
pendingJumpResolver.value = null;
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -204,7 +216,6 @@ const jumpToRule = (ruleName: string): boolean => {
|
|||||||
lastFrameTime.value = performance.now()
|
lastFrameTime.value = performance.now()
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
// 【修改】: 跳转成功,兑现Promise
|
|
||||||
if (pendingJumpResolver.value) {
|
if (pendingJumpResolver.value) {
|
||||||
pendingJumpResolver.value(true);
|
pendingJumpResolver.value(true);
|
||||||
pendingJumpResolver.value = null;
|
pendingJumpResolver.value = null;
|
||||||
@ -319,7 +330,6 @@ const animate = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// stopAnimation 函数 (增加清理逻辑)
|
|
||||||
const stopAnimation = () => {
|
const stopAnimation = () => {
|
||||||
isPlaying.value = false
|
isPlaying.value = false
|
||||||
isPaused.value = false
|
isPaused.value = false
|
||||||
@ -327,21 +337,18 @@ const stopAnimation = () => {
|
|||||||
cancelAnimationFrame(animationId.value)
|
cancelAnimationFrame(animationId.value)
|
||||||
animationId.value = undefined
|
animationId.value = undefined
|
||||||
}
|
}
|
||||||
// 【修改】: 清理待处理的跳转
|
|
||||||
if (pendingJumpResolver.value) {
|
if (pendingJumpResolver.value) {
|
||||||
pendingJumpResolver.value(false); // 动画停止,视为跳转失败
|
pendingJumpResolver.value(false);
|
||||||
pendingJumpResolver.value = null;
|
pendingJumpResolver.value = null;
|
||||||
}
|
}
|
||||||
pendingJumpTo.value = undefined;
|
pendingJumpTo.value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetAnimation 函数 (增加清理逻辑)
|
|
||||||
const resetAnimation = () => {
|
const resetAnimation = () => {
|
||||||
stopAnimation()
|
stopAnimation()
|
||||||
currentRuleIndex.value = 0
|
currentRuleIndex.value = 0
|
||||||
currentFrame.value = 0
|
currentFrame.value = 0
|
||||||
currentLoopCount.value = 0
|
currentLoopCount.value = 0
|
||||||
// pendingJumpTo 和 pendingJumpResolver 已在 stopAnimation 中清理
|
|
||||||
if (loadedImages.value.length > 0) {
|
if (loadedImages.value.length > 0) {
|
||||||
drawFrame(0)
|
drawFrame(0)
|
||||||
}
|
}
|
||||||
@ -365,9 +372,6 @@ const resumeFromPause = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 核心修改区域 2: 跳转方法
|
|
||||||
// =================================================================
|
|
||||||
/**
|
/**
|
||||||
* 立即跳转到指定规则,会打断当前动画。
|
* 立即跳转到指定规则,会打断当前动画。
|
||||||
* @param {string} ruleName 要跳转到的规则名。
|
* @param {string} ruleName 要跳转到的规则名。
|
||||||
@ -391,16 +395,13 @@ const jumpToSoftly = (ruleName: string): Promise<boolean> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果之前有待处理的跳转,先将其标记为失败
|
|
||||||
if (pendingJumpResolver.value) {
|
if (pendingJumpResolver.value) {
|
||||||
pendingJumpResolver.value(false);
|
pendingJumpResolver.value(false);
|
||||||
}
|
}
|
||||||
// 设置新的 Promise resolver
|
|
||||||
pendingJumpResolver.value = resolve;
|
pendingJumpResolver.value = resolve;
|
||||||
|
|
||||||
if (isPaused.value || !isPlaying.value) {
|
if (isPaused.value || !isPlaying.value) {
|
||||||
if(props.log) console.log(`动画已暂停或停止。立即执行跳转到 "${ruleName}"。`);
|
if(props.log) console.log(`动画已暂停或停止。立即执行跳转到 "${ruleName}"。`);
|
||||||
// jumpToRule 将会调用并清理 pendingJumpResolver
|
|
||||||
jumpToRule(ruleName);
|
jumpToRule(ruleName);
|
||||||
} else if (isPlaying.value) {
|
} else if (isPlaying.value) {
|
||||||
if(props.log) console.log(`已计划软跳转到 "${ruleName}"。将在当前循环结束后触发。`);
|
if(props.log) console.log(`已计划软跳转到 "${ruleName}"。将在当前循环结束后触发。`);
|
||||||
@ -413,7 +414,7 @@ const jumpToSoftly = (ruleName: string): Promise<boolean> => {
|
|||||||
// watch 和生命周期钩子 (不变)
|
// watch 和生命周期钩子 (不变)
|
||||||
watch(() => props.url, (newUrl) => {
|
watch(() => props.url, (newUrl) => {
|
||||||
if (newUrl) {
|
if (newUrl) {
|
||||||
resetAnimation() // 使用 reset 来确保清理
|
resetAnimation()
|
||||||
images.value.forEach(url => URL.revokeObjectURL(url));
|
images.value.forEach(url => URL.revokeObjectURL(url));
|
||||||
images.value = [];
|
images.value = [];
|
||||||
loadedImages.value = [];
|
loadedImages.value = [];
|
||||||
@ -431,7 +432,7 @@ watch(() => props.rules, () => {
|
|||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
resetAnimation(); // 使用 reset 来确保清理
|
resetAnimation();
|
||||||
images.value.forEach(url => URL.revokeObjectURL(url));
|
images.value.forEach(url => URL.revokeObjectURL(url));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -440,7 +441,7 @@ const findRuleIndex = (ruleName: string): number => {
|
|||||||
return props.rules.findIndex(rule => rule.name === ruleName)
|
return props.rules.findIndex(rule => rule.name === ruleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// defineExpose (修改)
|
// defineExpose (不变)
|
||||||
defineExpose({
|
defineExpose({
|
||||||
images,
|
images,
|
||||||
loadedImages,
|
loadedImages,
|
||||||
|
|||||||
28
src/components/Button.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'click'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
import eas from '../assets/sounds';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="btn" @click="eas.play('按钮音效'); $emit('click')">
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6vw 6vw 6vw 0;
|
||||||
|
height: 12vw;
|
||||||
|
width: 34vw;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 4vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -630,3 +630,8 @@ export const arcRetailQuiz: QuizQuestion[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function getRandomQuizQuestions(count: number): QuizQuestion[] {
|
||||||
|
const shuffled = [...arcRetailQuiz].sort(() => 0.5 - Math.random());
|
||||||
|
return shuffled.slice(0, count);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, useTemplateRef, watch, type Ref } from 'vue';
|
import { computed, onMounted, reactive, ref, TransitionGroup, useTemplateRef, watch, type Ref } from 'vue';
|
||||||
import assets from '../assets';
|
import assets from '../assets';
|
||||||
import AniEle from '../components/AniEle.vue';
|
import AniEle from '../components/AniEle.vue';
|
||||||
import LastPage from './Game/LastPage.vue';
|
import LastPage from './Game/LastPage.vue';
|
||||||
@ -7,6 +7,7 @@ import AudioEffects from '../assets/sounds'
|
|||||||
import GatheringCloud from './Game/GatheringCloud.vue';
|
import GatheringCloud from './Game/GatheringCloud.vue';
|
||||||
import ScoreBoard from './Game/ScoreBoard.vue';
|
import ScoreBoard from './Game/ScoreBoard.vue';
|
||||||
import ActionArea from './Game/ActionArea.vue';
|
import ActionArea from './Game/ActionArea.vue';
|
||||||
|
import Quiz from './Game/Quiz.vue';
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
init: () => {
|
init: () => {
|
||||||
@ -103,6 +104,18 @@ const leaderBoard = ref([
|
|||||||
"time": 122.2,
|
"time": 122.2,
|
||||||
"region": "大南区",
|
"region": "大南区",
|
||||||
"store": "深圳湾万象城"
|
"store": "深圳湾万象城"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elena2",
|
||||||
|
"time": 122.4,
|
||||||
|
"region": "大南区",
|
||||||
|
"store": "深圳湾万象城"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elena3",
|
||||||
|
"time": 122.7,
|
||||||
|
"region": "大南区",
|
||||||
|
"store": "深圳湾万象城"
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
const showTitleIcon = ref(true)
|
const showTitleIcon = ref(true)
|
||||||
@ -382,20 +395,6 @@ function gToggle(pos: 'left' | 'center' | 'right') {
|
|||||||
|
|
||||||
|
|
||||||
async function gameEnd() {
|
async function gameEnd() {
|
||||||
fetch('/api/score', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
region: props.userdata.region,
|
|
||||||
store: props.userdata.store,
|
|
||||||
name: props.userdata.username,
|
|
||||||
time: timeSpent.value / 1000,
|
|
||||||
})
|
|
||||||
}).then(_ => {
|
|
||||||
fetch('/api/score', {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(res => res.json()).then(data => leaderBoard.value = data);
|
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelector('.bed')?.animate([
|
document.querySelector('.bed')?.animate([
|
||||||
{ transform: 'translateY(170%)' },
|
{ transform: 'translateY(170%)' },
|
||||||
@ -430,23 +429,36 @@ async function gameEnd() {
|
|||||||
await wait(1800)
|
await wait(1800)
|
||||||
|
|
||||||
showEndAni.value = true;
|
showEndAni.value = true;
|
||||||
sunEndEle.value?.jumpTo('all');
|
sunEndEle.value?.jumpTo('进入');
|
||||||
|
|
||||||
await wait(3400);
|
}
|
||||||
gameState.value = GAME_STATE.leaderboard;
|
|
||||||
|
|
||||||
document.querySelector('.bed')?.animate([
|
async function startQuiz() {
|
||||||
{ transform: 'translateY(0)' },
|
document.querySelector('.start-quiz-btn')?.animate([
|
||||||
{ transform: 'translateY(170%)' },
|
{ scale: 1 },
|
||||||
|
{ scale: 0 },
|
||||||
], {
|
], {
|
||||||
duration: 600,
|
duration: 500,
|
||||||
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
|
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
|
||||||
fill: 'forwards',
|
fill: 'forwards',
|
||||||
});
|
});
|
||||||
|
gatheringCloud.value?.titleOut();
|
||||||
|
await sunEndEle.value?.jumpToSoftly('落下出屏幕');
|
||||||
|
document.querySelector('.bed')?.animate([
|
||||||
|
{ transform: 'translateY(0%)' },
|
||||||
|
{ transform: 'translateY(170%)' },
|
||||||
|
], {
|
||||||
|
duration: 400,
|
||||||
|
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
|
||||||
|
fill: 'forwards',
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(900);
|
||||||
|
gameState.value = GAME_STATE.quiz;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finishCollect() {
|
async function finishCollect() {
|
||||||
document.querySelector('.finish-collect')?.animate([
|
document.querySelector('.finish-collect-btn')?.animate([
|
||||||
{ transform: 'scale(1)' },
|
{ transform: 'scale(1)' },
|
||||||
{ transform: 'scale(0)' },
|
{ transform: 'scale(0)' },
|
||||||
], {
|
], {
|
||||||
@ -455,7 +467,6 @@ async function finishCollect() {
|
|||||||
fill: 'forwards',
|
fill: 'forwards',
|
||||||
})
|
})
|
||||||
|
|
||||||
gatheringCloud.value?.titleOut();
|
|
||||||
showTitleIcon.value = true;
|
showTitleIcon.value = true;
|
||||||
|
|
||||||
await document.querySelector('.lines')?.animate([
|
await document.querySelector('.lines')?.animate([
|
||||||
@ -464,7 +475,8 @@ async function finishCollect() {
|
|||||||
easing: 'ease-in-out',
|
easing: 'ease-in-out',
|
||||||
fill: 'forwards',
|
fill: 'forwards',
|
||||||
}).finished
|
}).finished
|
||||||
await sunEndEle.value?.jumpToSoftly('落下');
|
|
||||||
|
await sunEndEle.value?.jumpToSoftly('落下进蹦床');
|
||||||
await document.querySelector('.bar-container')?.animate([
|
await document.querySelector('.bar-container')?.animate([
|
||||||
{ transform: 'translateY(300%)' }], {
|
{ transform: 'translateY(300%)' }], {
|
||||||
duration: 500,
|
duration: 500,
|
||||||
@ -487,6 +499,42 @@ const fmtTime = (time: number) => {
|
|||||||
const seconds = time % 60;
|
const seconds = time % 60;
|
||||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleFinishQuiz(time: number, choices: ('none' | 'selected' | 'correct' | 'wrong' | 'miss')[][]) {
|
||||||
|
// 处理完成答题的逻辑
|
||||||
|
timeSpent.value = time;
|
||||||
|
|
||||||
|
fetch('/api/score', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
region: props.userdata.region,
|
||||||
|
store: props.userdata.store,
|
||||||
|
name: props.userdata.username,
|
||||||
|
time: timeSpent.value,
|
||||||
|
})
|
||||||
|
}).then(_ => {
|
||||||
|
fetch('/api/score', {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(res => res.json()).then(data => leaderBoard.value = data);
|
||||||
|
})
|
||||||
|
|
||||||
|
await wait(1500);
|
||||||
|
sunEndEle.value?.jumpTo('向上进入')
|
||||||
|
timeSpent.value = time;
|
||||||
|
console.log('Quiz finished:', time, choices);
|
||||||
|
|
||||||
|
await wait(300)
|
||||||
|
gameState.value = GAME_STATE.leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
import Button from '../components/Button.vue';
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
giftProgress.value = 7;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -514,12 +562,12 @@ const fmtTime = (time: number) => {
|
|||||||
{ name: '起飞前', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
|
{ name: '起飞前', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
|
||||||
{ name: '蓄力飞', frame: 181, loop: 1, pauseAfter: false, duration: 33 },
|
{ name: '蓄力飞', frame: 181, loop: 1, pauseAfter: false, duration: 33 },
|
||||||
{ name: '天上飞', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
|
{ name: '天上飞', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
|
||||||
{ name: '落下', frame: 382, loop: 0, pauseAfter: false, duration: 33 },
|
{ name: '落下', frame: 382, loop: 1, pauseAfter: false, duration: 33 },
|
||||||
]" style="width: 100%; height: auto;" />
|
]" style="width: 100%; height: auto;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="gameState >= GAME_STATE.ingame && gameState < GAME_STATE.poster">
|
<div class="in-game" v-if="gameState >= GAME_STATE.ingame && gameState < GAME_STATE.poster">
|
||||||
<ActionArea @gToggle="gToggle" ref="action-area" />
|
<ActionArea @gToggle="gToggle" ref="action-area" />
|
||||||
|
|
||||||
<img class="abs gift-item" v-for="(gift, index) in giftList" :src="gift[0]" style="width: 20vw;"
|
<img class="abs gift-item" v-for="(gift, index) in giftList" :src="gift[0]" style="width: 20vw;"
|
||||||
@ -539,32 +587,46 @@ const fmtTime = (time: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="game-end abs" v-if="gameState >= GAME_STATE.gameEnd"
|
|
||||||
style="z-index: 2; inset: 0; height: 100%; width: 100%;pointer-events: none;">
|
|
||||||
|
|
||||||
<GatheringCloud ref="gatheringCloud" />
|
|
||||||
|
|
||||||
<ScoreBoard :leaderBoard="leaderBoard" :fmtTime="fmtTime" v-if="gameState >= GAME_STATE.leaderboard" />
|
|
||||||
|
|
||||||
<div class="sun-ani-end-wrapper abs" style="pointer-events: none; inset: 0; width: 100%;z-index: 2;"
|
<div class="sun-ani-end-wrapper abs" style="pointer-events: none; inset: 0; width: 100%;z-index: 2;"
|
||||||
:style="{ display: showEndAni ? 'block' : 'none' }">
|
:style="{ display: showEndAni ? 'block' : 'none' }">
|
||||||
|
|
||||||
<AniEle :url="assets.ani.后端效果" ref="sun-ani-end" class="sun-ani-end" :height="2462" :width="1179"
|
<AniEle :url="assets.ani.后段效果" ref="sun-ani-end" class="sun-ani-end" :height="2462" :width="1179" :rules="[
|
||||||
:rules="[
|
// 0 - 97
|
||||||
{ name: 'all', frame: 164, loop: 1, pauseAfter: false, duration: 33 },
|
{ name: '进入', frame: 98, loop: 1, pauseAfter: false, duration: 33 },
|
||||||
{ name: '左右跳', frame: 66, loop: 0, pauseAfter: true, duration: 33 },
|
// 98 - 129
|
||||||
{ name: '落下', frame: 32, loop: 1, pauseAfter: false, duration: 33 },
|
{ name: '落下前循环', frame: 32, loop: 0, pauseAfter: true, duration: 33 },
|
||||||
{ name: 'loop', frame: 33, loop: 0, pauseAfter: true, duration: 33 },
|
// 130 - 161
|
||||||
|
{ name: '落下出屏幕', frame: 32, loop: 1, pauseAfter: true, duration: 33 },
|
||||||
|
// 162 - 227
|
||||||
|
{ name: '向上进入', frame: 66, loop: 1, pauseAfter: false, duration: 33 },
|
||||||
|
// 228 - 293
|
||||||
|
{ name: '左右循环', frame: 66, loop: 0, pauseAfter: true, duration: 33 },
|
||||||
|
// 294 - 326
|
||||||
|
{ name: '落下进蹦床', frame: 33, loop: 1, pauseAfter: false, duration: 33 },
|
||||||
|
// 327 - 358
|
||||||
|
{ name: '蹦床弹跳循环', frame: 32, loop: 0, pauseAfter: false, duration: 33 },
|
||||||
]" style="width: 100%; height: auto;" />
|
]" style="width: 100%; height: auto;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div class="game-end" v-if="gameState == GAME_STATE.gameEnd">
|
||||||
|
<GatheringCloud ref="gatheringCloud" />
|
||||||
|
<Button @click="startQuiz" class="start-quiz-btn">开始答题</Button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<Quiz :fmt-time="fmtTime" v-if="gameState == GAME_STATE.quiz" @finish-quiz="handleFinishQuiz" />
|
||||||
|
|
||||||
|
|
||||||
|
<div class="scoreboard" v-if="gameState >= GAME_STATE.leaderboard">
|
||||||
|
<ScoreBoard :leaderBoard="leaderBoard" :fmtTime="fmtTime" v-if="gameState >= GAME_STATE.leaderboard" />
|
||||||
|
|
||||||
<img src="../assets/game/完成收集.webp" alt="" v-if="gameState >= GAME_STATE.leaderboard"
|
<img src="../assets/game/完成收集.webp" alt="" v-if="gameState >= GAME_STATE.leaderboard"
|
||||||
class="finish-collect abs" @click.once="AudioEffects.play('按钮音效'); finishCollect()"
|
@click.once="AudioEffects.play('按钮音效'); finishCollect()" class="finish-collect-btn abs">
|
||||||
style="bottom: 8%;left: 33%;width: 34vw;animation: scale-in 0.3s ease-out;pointer-events: all;">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LastPage :userdata="userdata" :time-spent="timeSpent" v-if="gameState == GAME_STATE.poster" :fmt-time="fmtTime"
|
|
||||||
|
<LastPage :userdata="userdata" :time-spent="fmtTime(timeSpent)" v-if="gameState == GAME_STATE.poster"
|
||||||
@reset-game="emit('restart')" @show-share-mask="showShareMask = true" />
|
@reset-game="emit('restart')" @show-share-mask="showShareMask = true" />
|
||||||
|
|
||||||
<Transition name="gift-popup">
|
<Transition name="gift-popup">
|
||||||
@ -582,7 +644,7 @@ const fmtTime = (time: number) => {
|
|||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="showShareMask" class="share-mask" @click="showShareMask = false">
|
<div v-if="showShareMask" class="share-mask" @click="showShareMask = false">
|
||||||
<img src="../assets/game/share_mask.png" alt="">
|
<img src="../assets/game/share_mask.webp" alt="">
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
@ -590,6 +652,26 @@ const fmtTime = (time: number) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.finish-collect-btn {
|
||||||
|
scale: 0;
|
||||||
|
animation: scale-in 0.5s ease-out forwards;
|
||||||
|
animation-delay: 1s;
|
||||||
|
bottom: 8%;
|
||||||
|
left: 33%;
|
||||||
|
width: 34vw;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-quiz-btn {
|
||||||
|
animation: scale-in 0.5s ease-out forwards;
|
||||||
|
scale: 0;
|
||||||
|
animation-delay: 4.5s;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 33%;
|
||||||
|
width: 34%;
|
||||||
|
}
|
||||||
|
|
||||||
.share-mask {
|
.share-mask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -656,21 +738,24 @@ const fmtTime = (time: number) => {
|
|||||||
@keyframes bar-in {
|
@keyframes bar-in {
|
||||||
from {
|
from {
|
||||||
transform: translateY(300%);
|
transform: translateY(300%);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes line-in {
|
@keyframes line-in {
|
||||||
from {
|
from {
|
||||||
transform: translateX(-210%);
|
transform: translateX(-210%);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translateX(-0%);
|
transform: translateX(-0%);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,7 @@ defineProps<{
|
|||||||
store: string;
|
store: string;
|
||||||
username: string;
|
username: string;
|
||||||
},
|
},
|
||||||
timeSpent: number;
|
timeSpent: string;
|
||||||
fmtTime: (time: number) => string;
|
|
||||||
}>();
|
}>();
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
const qrcodeUrl = ref('');
|
const qrcodeUrl = ref('');
|
||||||
@ -119,7 +118,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="time"
|
<div class="time"
|
||||||
style="font-size: 10vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
|
style="font-size: 10vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
|
||||||
{{ fmtTime(timeSpent / 1000) }}</div>
|
{{ timeSpent}}</div>
|
||||||
|
|
||||||
<img :src="qrcodeUrl" alt=""
|
<img :src="qrcodeUrl" alt=""
|
||||||
style="width: 100%;bottom:7%;position: absolute;animation: line-in 0.5s ease-out 1s forwards;transform: translateX(-210%);">
|
style="width: 100%;bottom:7%;position: absolute;animation: line-in 0.5s ease-out 1s forwards;transform: translateX(-210%);">
|
||||||
|
|||||||
@ -1,11 +1,416 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, type Reactive } from 'vue';
|
||||||
import assets from '../../assets';
|
import assets from '../../assets';
|
||||||
import AniEle from '../../components/AniEle.vue';
|
import AniEle from '../../components/AniEle.vue';
|
||||||
|
import { getRandomQuizQuestions } from '../../data';
|
||||||
|
|
||||||
|
const total = 5; // 总题数
|
||||||
|
const qIndex = ref(0); // 当前题目索引
|
||||||
|
const qs = getRandomQuizQuestions(5)
|
||||||
|
|
||||||
|
const time = ref(0);
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
fmtTime: (time: number) => string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showSelectNotice = ref(false);
|
||||||
|
|
||||||
|
import eas from '../../assets/sounds';
|
||||||
|
import Button from '../../components/Button.vue';
|
||||||
|
|
||||||
|
function playConfirmSound(correct: boolean) {
|
||||||
|
if (correct) {
|
||||||
|
eas.play('正确')
|
||||||
|
} else {
|
||||||
|
eas.play('错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const choiceState: Reactive<('none' | 'selected' | 'correct' | 'wrong' | 'miss')[][]> = reactive(
|
||||||
|
qs.map((q) => q.options.map(() => 'none'))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (confirmState.value == 'confirmed') return
|
||||||
|
time.value += 1;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'finishQuiz', time: number, choices: ('none' | 'selected' | 'correct' | 'wrong' | 'miss')[][]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function finishQuiz() {
|
||||||
|
|
||||||
|
document.querySelector('.quiz-card')?.animate([
|
||||||
|
{ scale: 1, opacity: 1 },
|
||||||
|
{ scale: 0.5, opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 600,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
fill: 'forwards',
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('.confirm-btn')?.animate([
|
||||||
|
{ scale: 1 },
|
||||||
|
{ scale: 0 }
|
||||||
|
], {
|
||||||
|
duration: 400,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
fill: 'forwards',
|
||||||
|
});
|
||||||
|
|
||||||
|
titleAniRules.value[0].reverse = true;
|
||||||
|
|
||||||
|
emit('finishQuiz', time.value, choiceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmChoices() {
|
||||||
|
if (confirmState.value === 'confirmed') {
|
||||||
|
showSelectNotice.value = false;
|
||||||
|
if (qIndex.value >= total - 1) {
|
||||||
|
finishQuiz();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
confirmState.value = 'none';
|
||||||
|
qIndex.value++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentChoices = choiceState[qIndex.value];
|
||||||
|
|
||||||
|
if (currentChoices.every(choice => choice === 'none')) {
|
||||||
|
showSelectNotice.value = true;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
showSelectNotice.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentQuestion = qs[qIndex.value];
|
||||||
|
|
||||||
|
let correctCount = 0;
|
||||||
|
let wrongCount = 0;
|
||||||
|
|
||||||
|
const correctAnswersIndexs = currentQuestion.answers.map(answer => currentQuestion.options.indexOf(answer));
|
||||||
|
currentChoices.forEach((choice, index) => {
|
||||||
|
if (choice === 'selected') {
|
||||||
|
if (correctAnswersIndexs.includes(index)) {
|
||||||
|
choiceState[qIndex.value][index] = 'correct';
|
||||||
|
correctCount++;
|
||||||
|
} else {
|
||||||
|
choiceState[qIndex.value][index] = 'wrong';
|
||||||
|
wrongCount++;
|
||||||
|
}
|
||||||
|
} else if (correctAnswersIndexs.includes(index)) {
|
||||||
|
choiceState[qIndex.value][index] = 'miss';
|
||||||
|
wrongCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wrongCount > 0) {
|
||||||
|
playConfirmSound(false);
|
||||||
|
time.value += wrongCount * 10; // 每个错误选项罚时10秒
|
||||||
|
} else {
|
||||||
|
playConfirmSound(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmState.value = 'confirmed';
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleAniRules = ref([
|
||||||
|
{ name: 'main', frame: 60, duration: 33, loop: 1, pauseAfter: true, reverse: false }
|
||||||
|
])
|
||||||
|
|
||||||
|
const confirmState = ref<'none' | 'confirmed'>('none');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AniEle :url="assets.ani.答题标题" :width="1000" :height="500" :rules="[
|
<div class="quiz">
|
||||||
{ name: 'main', frame: 60, duration: 33, loop: 1, pauseAfter: true, reverse: false }
|
<AniEle :url="assets.ani.答题标题" :width="1000" :height="500" :rules="titleAniRules" class="quiz-page-title" />
|
||||||
]" />
|
|
||||||
|
<div class="quiz-card">
|
||||||
|
<img src="../../assets/game/答题卡片背景.webp" alt="" class="quiz-card-bg">
|
||||||
|
<div class="quiz-card-main">
|
||||||
|
<div class="quiz-type">{{ qs[qIndex].type == 'single' ? '单选题' : '多选题' }}</div>
|
||||||
|
<div class="time">
|
||||||
|
<img src="../../assets/game/闹钟.webp" alt="" class="time-icon">
|
||||||
|
<span class="time-text">{{ fmtTime(time) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quiz-content-wrap" :style="{ transform: `translateX(-${qIndex * 100 / total}%)` }">
|
||||||
|
<div class="quiz-content" v-for="(qu, index) in qs" :key="index">
|
||||||
|
<div class="quiz-title">{{ qu.question }}</div>
|
||||||
|
<div class="quiz-choice-container">
|
||||||
|
<div class="quiz-choice" v-for="(val, i) in qu.options" :key="val"
|
||||||
|
:style="{ animationDelay: `${(i + 8) * 0.1}s` }" :class="choiceState[index][i]" @click="() => {
|
||||||
|
if (choiceState[index][i] === 'none') {
|
||||||
|
choiceState[index][i] = 'selected';
|
||||||
|
} else if (choiceState[index][i] === 'selected') {
|
||||||
|
choiceState[index][i] = 'none';
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<span class="choice-letter">{{ String.fromCharCode(65 + i) }}</span>
|
||||||
|
<span class="choice-text">{{ val }}</span>
|
||||||
|
|
||||||
|
<img src="../../assets/game/罚时.webp" alt="" class="wrong-add-time-icon"
|
||||||
|
v-if="choiceState[index][i] === 'wrong' || choiceState[index][i] === 'miss'"></img>
|
||||||
|
|
||||||
|
<img src="../../assets/game/对.webp" alt="" class="right-wrong-icon"
|
||||||
|
v-if="choiceState[index][i] === 'correct' || choiceState[index][i] === 'miss'">
|
||||||
|
<img src="../../assets/game/错.webp" alt="" class="right-wrong-icon"
|
||||||
|
v-if="choiceState[index][i] === 'wrong'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="quiz-progress dot">
|
||||||
|
<div class="dot-item" v-for="(val, i) in Array.from({ length: total })" :key="i" @click="qIndex = i"
|
||||||
|
:class="{ active: i <= qIndex }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<img src="../../assets/game/请选择你的答案.webp" alt="" v-if="showSelectNotice" class="select-notice"
|
||||||
|
style="position: absolute; bottom: 18%; left: 50%; transform: translateX(-50%); width: 34%; z-index: 1000;">
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<Button @click="confirmChoices" class="confirm-btn">{{ confirmState === 'confirmed' ?
|
||||||
|
qIndex === total - 1 ? '完成答题' : '下一题'
|
||||||
|
: '确认' }}</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.confirm-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 34%;
|
||||||
|
animation: scale-in 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-page-title {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 84%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-card {
|
||||||
|
position: absolute;
|
||||||
|
left: 5%;
|
||||||
|
width: 90%;
|
||||||
|
height: fit-content;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 23%;
|
||||||
|
z-index: 99;
|
||||||
|
animation: scale-in 0.5s ease-out forwards;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-card-bg {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-card-main {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
mask-image: linear-gradient(to right, transparent 5%, black 10%, black 95%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-type {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
background: #7194C1;
|
||||||
|
color: white;
|
||||||
|
font-size: 5vw;
|
||||||
|
height: 8vw;
|
||||||
|
line-height: 8vw;
|
||||||
|
border-radius: 5vw;
|
||||||
|
padding: 0 4.5vw;
|
||||||
|
animation: fade-in 0.5s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
animation-delay: 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
position: absolute;
|
||||||
|
top: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2vw;
|
||||||
|
animation: fade-in 0.5s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
animation-delay: 600ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-icon {
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 5vw;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-content-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 500%;
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
margin-top: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-content {
|
||||||
|
width: 20%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 15vw 10vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-title {
|
||||||
|
font-size: 4.2vw;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6%;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
animation: fade-in 0.5s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
animation-delay: 700ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-choice-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.quiz-choice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4vw;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
height: 12vw;
|
||||||
|
border-radius: 6vw;
|
||||||
|
padding: 0 5vw;
|
||||||
|
scale: 0;
|
||||||
|
animation: scale-in 0.3s ease-out forwards;
|
||||||
|
|
||||||
|
.choice-letter {
|
||||||
|
font-size: 4vw;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 3vw;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong-add-time-icon {
|
||||||
|
width: 10vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-wrong-icon {
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.none {
|
||||||
|
background-color: #F6F6F6;
|
||||||
|
|
||||||
|
.choice-letter {
|
||||||
|
color: #2E2E2E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-text {
|
||||||
|
color: #5B5C5B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&.correct {
|
||||||
|
background-color: #7396C4;
|
||||||
|
|
||||||
|
.choice-letter {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wrong,
|
||||||
|
&.miss {
|
||||||
|
background-color: #000000;
|
||||||
|
|
||||||
|
.choice-letter {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.quiz-progress {
|
||||||
|
bottom: 8%;
|
||||||
|
top: unset;
|
||||||
|
height: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot .dot-item {
|
||||||
|
background-color: #E2ECF9;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #7396C4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -25,7 +25,7 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lines">
|
<div class="lines">
|
||||||
<div class="line" v-for="(item, index) in leaderBoard.slice(3, 7)" :key="index"
|
<div class="line" v-for="(item, index) in leaderBoard.slice(3, 9)" :key="index"
|
||||||
:style="{ animationDelay: index * 100 + 400 + 'ms' }">
|
:style="{ animationDelay: index * 100 + 400 + 'ms' }">
|
||||||
<div class="rank">{{ index + 4 }}</div>
|
<div class="rank">{{ index + 4 }}</div>
|
||||||
<div class="name">{{ item.name }}</div>
|
<div class="name">{{ item.name }}</div>
|
||||||
@ -40,7 +40,7 @@ defineProps<{
|
|||||||
|
|
||||||
.scoreboard {
|
.scoreboard {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 44%;
|
top: 16.4%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.bar-container {
|
.bar-container {
|
||||||
@ -63,8 +63,8 @@ defineProps<{
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1.5vw;
|
gap: 1.5vw;
|
||||||
animation: bar-in 0.7s ease-out forwards;
|
animation: bar-in 0.5s ease-out forwards;
|
||||||
transform: translateY(150%);
|
opacity: 0;
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-size: 4.5vw;
|
font-size: 4.5vw;
|
||||||
@ -90,7 +90,7 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.first {
|
&.first {
|
||||||
top: 3vw;
|
top: -5vw;
|
||||||
background-image: url('../../assets/game/first.webp');
|
background-image: url('../../assets/game/first.webp');
|
||||||
animation-delay: 0ms;
|
animation-delay: 0ms;
|
||||||
}
|
}
|
||||||
@ -104,17 +104,17 @@ defineProps<{
|
|||||||
&.third {
|
&.third {
|
||||||
background-image: url('../../assets/game/third.webp');
|
background-image: url('../../assets/game/third.webp');
|
||||||
animation-delay: 500ms;
|
animation-delay: 500ms;
|
||||||
top: 9vw;
|
top: 23vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lines {
|
.lines {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -5vw;
|
top: 16vw;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1vw;
|
gap: 2vw;
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -127,7 +127,8 @@ defineProps<{
|
|||||||
height: 8vw;
|
height: 8vw;
|
||||||
border-radius: 8vw;
|
border-radius: 8vw;
|
||||||
animation: line-in 0.5s ease-out forwards;
|
animation: line-in 0.5s ease-out forwards;
|
||||||
transform: translateX(-150%);
|
box-shadow: 0 0 2vw rgba(0, 0, 0, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
.rank {
|
.rank {
|
||||||
// italic
|
// italic
|
||||||
|
|||||||