Commit e89882f9 authored by 董政锦's avatar 董政锦

feat: game4 PC端合并,(奖次排名数据渲染页面待做)

parent 298716ce
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
<svg width="117" height="50" viewBox="0 0 117 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M109 0C113.418 0 117 3.58172 117 8V22C117 26.4183 113.418 30 109 30H103.547L92 50L80.4531 30H8C3.58172 30 2.25497e-07 26.4183 0 22V8C0 3.58172 3.58172 2.1341e-07 8 0H109Z" fill="white" fill-opacity="0.7"/>
</svg>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import Loading from "./views/LoadingView.vue";
import Rank1 from "./views/Rank1View.vue";
import Rank2 from "./views/Rank2View.vue";
import { useAdminGameSocket } from "@/composables/useAdminGameSocket";
import { onSocketMessage } from "@/commons/ws";
import type Player from "@/commons/player.ts";
type GameScreen = "loading" | "rank1" | "rank2";
const PLAYER_LIMIT = 20;
const RANK_LIMIT = 10;
const emptyLoadingPlayer = () => ({
score: 0,
telephone: "",
userid: "",
wechat_id: "",
nickname: "虚位以待",
avatar: "",
});
const emptyRankPlayer = (index: number): Player => ({
active: false,
wechat_id: `empty-${index}`,
nickname: "虚位以待",
avatar: "",
score: 0,
});
const screen = ref<GameScreen>("loading");
const loadingPlayers = ref<any[]>(
Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer),
);
const playerCount = ref(0);
const rankPlayers = ref<Player[]>(
Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index)),
);
const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null);
let isPlayingStartAnimation = false;
// H5 操控传来的加速度: { [userid]: boost(叠加系数, 0~N) }
const horseBoosts = ref<Record<string, number>>({});
let offHorseBoost: (() => void) | undefined;
// 监听 WebSocket 获取 H5 玩家操控的马匹加速度
offHorseBoost = onSocketMessage((evt, payload) => {
const data = payload?.data ?? payload;
if (!Array.isArray(data)) return;
const boosts: Record<string, number> = {};
let hasBoost = false;
data.forEach((item: any) => {
const id = item.userid || item.wechat_id || "";
if (!id) return;
// 尝试多种字段名: boost / acceleration / speed / progress
const raw = item.boost ?? item.acceleration ?? item.speed ?? item.progress;
if (raw === undefined || raw === null) return;
const val = Number(raw);
if (isNaN(val)) return;
// boost 范围 0~N,0 表示无加速(基准60秒跑完),>0 越快
boosts[id] = Math.max(0, val);
hasBoost = true;
});
if (hasBoost) {
horseBoosts.value = { ...boosts };
}
});
const confirmRefresh = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = "";
};
const renderPlayers = (data: any) => {
const players = Array.isArray(data?.list) ? data.list : [];
playerCount.value = Number(data?.count ?? players.length);
loadingPlayers.value = Array.from(
{ length: PLAYER_LIMIT },
(_, index) => players[index] ?? emptyLoadingPlayer(),
);
};
const updateRankPlayers = (data: any) => {
if (!Array.isArray(data)) return;
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => data[index] ?? emptyRankPlayer(index),
);
};
const gotoLoading = () => {
screen.value = "loading";
};
const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return;
isPlayingStartAnimation = true;
screen.value = "rank1";
await nextTick();
await rank1Ref.value?.startIntro();
screen.value = "rank2";
isPlayingStartAnimation = false;
};
function handleRoomState(data: any) {
if (Array.isArray(data?.list)) {
renderPlayers(data);
}
switch (data?.status) {
case 0:
gotoLoading();
createRoom(1);
break;
case 1:
gotoLoading();
break;
case 2:
screen.value = "rank1";
break;
case 3:
screen.value = "rank2";
break;
}
}
const { startGame, createRoom, requestRoomState } = useAdminGameSocket({
gameId: "game4",
// loginRedirectPath: '/game4',
onGameStartRejected: gotoLoading,
onRoomState: handleRoomState,
onRoomJoin: (data) => {
renderPlayers(data);
gotoLoading();
},
onGameStart: gotoRank1WithIntro,
onRoomRank: updateRankPlayers,
});
const nextRound = () => {
gotoLoading();
createRoom(1);
setTimeout(() => {
requestRoomState();
}, 200);
};
onMounted(() => {
window.addEventListener("beforeunload", confirmRefresh);
});
onUnmounted(() => {
window.removeEventListener("beforeunload", confirmRefresh);
offHorseBoost?.();
});
</script>
<template>
<Loading
v-if="screen === 'loading'"
:players="loadingPlayers"
:player-count="playerCount"
@start="startGame"
/>
<!-- v-else-if="screen === 'rank1'" -->
<Rank1 ref="rank1Ref" :rank-list="rankPlayers" :horse-boosts="horseBoosts" @start="startGame" />
<!-- <Rank2 v-else :rank-list="rankPlayers" @next="nextRound" /> -->
</template>
<script setup lang="ts">
import { ref } from "vue";
import { $format_str } from "@/commons/utils.ts";
import DesignStage from "@/components/DesignStage.vue";
import { assetUrl, cssAssetUrl } from "@/commons/assets.ts";
defineProps<{
players: any[];
playerCount: number;
}>();
const emit = defineEmits<{
start: [];
}>();
const imageUrls = {
bg: cssAssetUrl("game4/bg.svg"),
logo: cssAssetUrl("game1/logo.png"),
title1: cssAssetUrl("game4/title1.svg"),
title2: cssAssetUrl("game4/title2.svg"),
qrcode: assetUrl("game4/qrcode.svg"),
gift: cssAssetUrl("game4/gift.svg"),
gu: cssAssetUrl("game4/big-brum.svg"),
smallGu: cssAssetUrl("game4/small-brum.svg"),
bottomLayers: cssAssetUrl("game1/bottom-layers.png"),
avatarAnimation: cssAssetUrl("avatar_animation.png"),
avatar: cssAssetUrl("avatar.png"),
line: cssAssetUrl("game1/line.png"),
start1: cssAssetUrl("game1/start-1.png"),
start2: cssAssetUrl("game1/start-2.png"),
};
const isStartPressed = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
emit("start");
};
const releaseStartButton = () => {
isStartPressed.value = false;
};
</script>
<template>
<DesignStage>
<div class="bg">
<div class="img-logo"></div>
<div class="img-title1"></div>
<div class="img-title2"></div>
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div>
</div>
<div class="img-gift"></div>
<div class="img-small-gu"></div>
<div class="img-gu"></div>
<div class="list-container">
<div class="txt-count">
当前在线人数 <span>{{ playerCount }}</span>
</div>
<div class="list-container-left">
<div
v-for="(item, index) in players"
:key="item.userid || item.wechat_id || `empty-${index}`"
>
<div>
<div class="img-avatar-container">
<div class="img-avatar"></div>
</div>
<div class="txt-nickname">
{{ $format_str(item.nickname, 12) }}
</div>
</div>
</div>
</div>
<div class="list-container-line"></div>
<div class="list-container-right">
<div
class="btn-start"
:class="{ pressed: isStartPressed }"
@pointerdown="pressStartButton"
@pointerup="releaseStartButton"
@pointerleave="releaseStartButton"
@pointercancel="releaseStartButton"
></div>
</div>
</div>
</div>
</DesignStage>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind("imageUrls.bg") center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.img-logo {
background: v-bind("imageUrls.logo");
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title1 {
background: v-bind("imageUrls.title1") no-repeat;
width: 831px;
height: 314px;
margin: 0 auto;
position: relative;
top: 60px;
}
.img-title2 {
background: v-bind("imageUrls.title2") no-repeat;
width: 643px;
height: 140px;
margin: 0 auto;
position: relative;
top: 80px;
}
.img-qrcode-container {
width: 144px;
height: 177px;
position: absolute;
z-index: 1;
top: 40px;
right: 40px;
.img-qrcode {
width: 100%;
height: 144px;
border-radius: 10px;
img {
width: 140px;
height: 140px;
}
}
.txt-qrcode {
font-size: 24px;
color: white;
}
}
.img-gift {
background: v-bind("imageUrls.gift") no-repeat;
width: 315px;
height: 202px;
position: absolute;
top: 124px;
right: 320px;
}
.img-small-gu {
background: v-bind("imageUrls.smallGu") no-repeat;
width: 161px;
height: 150px;
position: absolute;
top: 500px;
right: 340px;
}
.img-gu {
background: v-bind("imageUrls.gu") no-repeat;
width: 320px;
height: 297px;
position: absolute;
top: 387px;
right: 20px;
}
.list-container {
background: v-bind("imageUrls.bottomLayers");
width: 1800px;
height: 397px;
position: absolute;
left: 60px;
bottom: 60px;
.txt-count {
font-size: 36px;
margin-left: 30px;
margin-top: 20px;
color: #aa0000;
font-weight: bold;
> span {
margin-left: 20px;
font-size: 48px;
position: relative;
top: 5px;
}
}
.list-container-left {
float: left;
display: grid;
grid-template-columns: repeat(10, 1fr);
row-gap: 20px;
width: 73%;
margin-top: 30px;
height: 250px;
align-content: center;
.img-avatar-container {
position: relative;
margin: 0 auto;
width: 72px;
height: 72px;
border-radius: 36px;
}
.img-avatar-container::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind("imageUrls.avatarAnimation");
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
position: relative;
top: 6px;
z-index: 1;
margin: 0 auto;
width: 60px;
height: 60px;
border-radius: 30px;
background: v-bind("imageUrls.avatar");
}
.txt-nickname {
color: #666;
font-size: 20px;
text-align: center;
}
}
.list-container-line {
float: left;
width: 1px;
height: 281px;
margin-left: 50px;
background: v-bind("imageUrls.line") center no-repeat;
}
.list-container-right {
float: left;
.btn-start {
width: 316px;
height: 105px;
margin-left: 60px;
margin-top: 80px;
background: v-bind("imageUrls.start1");
cursor: pointer;
}
.btn-start.pressed {
background: v-bind("imageUrls.start2");
}
}
}
</style>
This diff is collapsed.
<script setup lang="ts">
import { ref, watch } from "vue";
import DesignStage from "../../../components/DesignStage.vue";
import type Player from "../../../commons/player.ts";
import { cssAssetUrl } from "@/commons/assets.ts";
const props = defineProps<{
rankList: Player[];
}>();
const emit = defineEmits<{
next: [];
}>();
const FINAL_RANK_LIMIT = 10;
const imageUrls = {
bg: cssAssetUrl("game4/bg.png"),
logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game1/title3.png"),
title2: cssAssetUrl("game1/title4.png"),
podium: cssAssetUrl("game1/podium.png"),
fireworks: cssAssetUrl("game1/fireworks.png"),
avatar: cssAssetUrl("avatar.png"),
guan1: cssAssetUrl("game1/guan1.png"),
pai1: cssAssetUrl("game1/pai1.png"),
guan2: cssAssetUrl("game1/guan2.png"),
pai2: cssAssetUrl("game1/pai2.png"),
guan3: cssAssetUrl("game1/guan3.png"),
pai3: cssAssetUrl("game1/pai3.png"),
rankIcon: cssAssetUrl("game1/no-icon.png"),
avatarAnimation: cssAssetUrl("avatar_animation.png"),
next: cssAssetUrl("game1/next-1.png"),
};
const emptyPlayer = (index: number): Player => ({
active: false,
wechat_id: `empty-${index}`,
nickname: "虚位以待",
avatar: "",
score: 0,
});
const list = ref<Player[]>(
Array.from({ length: FINAL_RANK_LIMIT }, (_, index) => emptyPlayer(index)),
);
const updateFinalRank = (rankList: Player[]) => {
list.value = Array.from(
{ length: FINAL_RANK_LIMIT },
(_, index) => rankList[index] ?? emptyPlayer(index),
);
};
watch(
() => props.rankList,
(rankList) => updateFinalRank(rankList),
{ immediate: true, deep: true },
);
</script>
<template>
<DesignStage>
<div class="bg">
<div class="img-logo"></div>
<div class="img-title"></div>
<div class="img-title2"></div>
<div class="podium">
<div class="rank-no1">
<div class="gu-burst"></div>
<div class="avatar"></div>
<div class="nickname">{{ list[0]?.nickname }}</div>
<div class="guan"></div>
<div class="pai"></div>
</div>
<div class="rank-no2">
<div class="gu-burst"></div>
<div class="avatar"></div>
<div class="nickname">{{ list[1]?.nickname }}</div>
<div class="guan"></div>
<div class="pai"></div>
</div>
<div class="rank-no3">
<div class="gu-burst"></div>
<div class="avatar"></div>
<div class="nickname">{{ list[2]?.nickname }}</div>
<div class="guan"></div>
<div class="pai"></div>
</div>
</div>
<div class="rank-container">
<div
v-for="(item, index) in list.slice(3)"
:key="item.userid || item.wechat_id || `rank-${index}`"
>
<div class="no-icon">{{ index + 4 }}</div>
<div>
<div class="img-avatar-container">
<div class="img-avatar"></div>
</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
<div class="btn-next" @click="emit('next')"></div>
</div>
</DesignStage>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind("imageUrls.bg") center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.img-logo {
background: v-bind("imageUrls.logo");
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title {
background: v-bind("imageUrls.title");
width: 967px;
height: 122px;
margin: 0 auto;
margin-top: 37px;
}
.img-title2 {
background: v-bind("imageUrls.title2") no-repeat center;
width: 457px;
height: 57px;
margin: 0 auto;
margin-top: 37px;
}
.podium {
background: v-bind("imageUrls.podium") no-repeat center;
width: 1347px;
height: 269px;
position: absolute;
top: 427px;
left: 287px;
.gu-burst {
width: 200px;
height: 200px;
position: absolute;
z-index: 1;
border-radius: 50%;
opacity: 1;
transform: scale(0.6);
transform-origin: center center;
background: v-bind("imageUrls.fireworks") center / cover no-repeat;
pointer-events: none;
animation: gu-burst-expand 1.2s ease-out infinite;
}
.rank-no1 {
position: relative;
.gu-burst {
left: 580px;
top: -200px;
}
.avatar {
background: v-bind("imageUrls.avatar") center / cover no-repeat;
width: 120px;
height: 120px;
border-radius: 60px;
position: absolute;
left: 610px;
top: -136px;
}
.nickname {
width: 167px;
height: 46px;
background: #ffcf44;
color: white;
text-align: center;
line-height: 46px;
position: absolute;
left: 590px;
top: -25px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind("imageUrls.guan1");
width: 64px;
height: 64px;
position: absolute;
left: 580px;
top: -153px;
}
.pai {
background: v-bind("imageUrls.pai1");
width: 212px;
height: 200px;
position: absolute;
left: 570px;
top: 40px;
}
}
.rank-no2 {
position: relative;
.gu-burst {
left: 140px;
top: -170px;
}
.avatar {
background: v-bind("imageUrls.avatar") center / cover no-repeat;
width: 120px;
height: 120px;
border-radius: 60px;
position: absolute;
left: 180px;
top: -110px;
}
.nickname {
width: 167px;
height: 46px;
background: #ca9600;
color: white;
text-align: center;
line-height: 46px;
position: absolute;
left: 157px;
top: 2px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind("imageUrls.guan2") no-repeat center;
width: 96px;
height: 96px;
position: absolute;
left: 134px;
top: -144px;
}
.pai {
background: v-bind("imageUrls.pai2") no-repeat center;
width: 212px;
height: 200px;
position: absolute;
left: 140px;
top: 50px;
}
}
.rank-no3 {
position: relative;
.gu-burst {
left: 1020px;
top: -170px;
}
.avatar {
background: v-bind("imageUrls.avatar") center / cover no-repeat;
width: 120px;
height: 120px;
border-radius: 60px;
position: absolute;
left: 1046px;
top: -110px;
}
.nickname {
width: 167px;
height: 46px;
background: #e2e2e2;
color: #afafaf;
text-align: center;
line-height: 46px;
position: absolute;
left: 1028px;
top: 2px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind("imageUrls.guan3") no-repeat center;
width: 96px;
height: 96px;
position: absolute;
left: 1003px;
top: -146px;
}
.pai {
background: v-bind("imageUrls.pai3") no-repeat center;
width: 212px;
height: 200px;
position: absolute;
right: 130px;
top: 50px;
}
}
}
.rank-container {
display: flex;
align-items: center;
width: 1270px;
height: 115px;
position: absolute;
bottom: 243px;
left: 325px;
> div {
flex: 1;
display: grid;
width: 100px;
height: 100%;
.no-icon {
background: v-bind("imageUrls.rankIcon") no-repeat center;
width: 40px;
height: 40px;
position: absolute;
text-align: center;
line-height: 40px;
color: white;
font-weight: bold;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
margin-top: 10px;
width: 72px;
height: 72px;
border-radius: 36px;
}
.img-avatar-container::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind("imageUrls.avatarAnimation");
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
position: relative;
top: 6px;
z-index: 1;
margin: 0 auto;
width: 60px;
height: 60px;
border-radius: 30px;
background: v-bind("imageUrls.avatar");
}
.txt-nickname {
color: white;
font-size: 20px;
text-align: center;
}
}
}
.btn-next {
position: absolute;
bottom: 100px;
left: 820px;
width: 316px;
height: 105px;
cursor: pointer;
background: v-bind("imageUrls.next") center / cover no-repeat;
}
</style>
...@@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router' ...@@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils' import { $read } from '@/commons/utils'
import Login from '@/pages/Login.vue' import Login from '@/pages/Login.vue'
import Game1 from '@/pages/game1/Game1.vue' import Game1 from '@/pages/game1/Game1.vue'
import Game4 from '@/pages/game4/Game4.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
...@@ -23,6 +24,14 @@ const router = createRouter({ ...@@ -23,6 +24,14 @@ const router = createRouter({
requiresAuth: true, requiresAuth: true,
}, },
}, },
{
path: '/game4',
name: 'Game4',
component: Game4,
meta: {
requiresAuth: true,
},
},
// { // {
// path: '/:pathMatch(.*)*', // path: '/:pathMatch(.*)*',
// redirect: '/', // redirect: '/',
......
...@@ -37,6 +37,7 @@ export default defineConfig(({ command }) => ({ ...@@ -37,6 +37,7 @@ export default defineConfig(({ command }) => ({
copyImageAssets(), copyImageAssets(),
], ],
server: { server: {
host: true,
proxy: { proxy: {
'/socket.io': { '/socket.io': {
target: 'http://localhost:8082', target: 'http://localhost:8082',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment