Commit 8f4557d9 authored by 陈冲's avatar 陈冲

feat: 提交

parent 40a1a7b0
...@@ -6,38 +6,22 @@ mod router; ...@@ -6,38 +6,22 @@ mod router;
use commons::utils::CONFIG as config; use commons::utils::CONFIG as config;
use db::init_db; use db::init_db;
// #[derive(rust_embed::RustEmbed)] // 后台测试页,正式线上时不要代理该路径
// #[folder = "src/web"] #[derive(rust_embed::RustEmbed)]
// struct AssetsWeb; #[folder = "src/web"]
struct AssetsWeb;
// #[derive(rust_embed::RustEmbed)]
// #[folder = "src/large"]
// struct AssetsLarge;
// #[derive(rust_embed::RustEmbed)]
// #[folder = "src/wechat"]
// struct AssetsWechat;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
if let Err(err) = init_db().await { if let Err(err) = init_db().await {
panic!("init db failed: {err}"); panic!("init db failed: {err}");
} }
let router = Router::new() let router = Router::new().push(router::config_router())
.push(router::config_router()) .push(
// .push( Router::with_path("/web/{**path}")
// Router::with_path("/web/{**path}") .get(salvo::serve_static::static_embed::<AssetsWeb>().fallback("index.html")),
// .get(salvo::serve_static::static_embed::<AssetsWeb>().fallback("index.html")), )
// ) ;
// .push(
// Router::with_path("/wechat/{**path}")
// .get(salvo::serve_static::static_embed::<AssetsWechat>().fallback("index.html")),
// )
// .push(
// Router::with_path("/large/{**path}")
// .get(salvo::serve_static::static_embed::<AssetsLarge>().fallback("index.html")),
// )
;
let host = config.host.clone(); let host = config.host.clone();
dbg!(&config); dbg!(&config);
println!("http://{}", host); println!("http://{}", host);
......
This diff is collapsed.
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.
:root{--color-background:#fbf7ef;--color-surface:#fff;--color-text:#231f20;--color-muted:#7c726b;--color-primary:#fa6047;--color-primary-dark:#d94835;--color-border:#231f201f}*,:before,:after{box-sizing:border-box;-webkit-tap-highlight-color:transparent;margin:0;font-weight:400}html,body{width:100%;height:100%}body{color:var(--color-text);background:var(--color-background);text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,PingFang SC,Microsoft YaHei,sans-serif;font-size:16px;line-height:1.5}button,input{font:inherit}button{cursor:pointer;border:0}img{max-width:100%;display:block}#app{-webkit-user-select:none;user-select:none;width:100vw;height:100vh;overflow:hidden}.h5-page{width:100%;min-height:100%;padding:max(18px, env(safe-area-inset-top)) 18px max(18px, env(safe-area-inset-bottom))}.h5-title{letter-spacing:0;font-size:26px;font-weight:800;line-height:1.15}.h5-subtitle{color:var(--color-muted);margin-top:8px;font-size:14px}.h5-panel{border:1px solid var(--color-border);background:#ffffffe0;border-radius:8px;box-shadow:0 16px 36px #5d36231f}.h5-button{background:var(--color-primary);color:#fff;border-radius:8px;width:100%;min-height:48px;font-size:17px;font-weight:800;line-height:1}.h5-button:active{background:var(--color-primary-dark)}.mobile-stage-viewport[data-v-3129d68a]{overscroll-behavior:none;width:100%;height:100%;position:relative;overflow:hidden}.mobile-stage-background[data-v-3129d68a]{transform-origin:0 0;z-index:0;position:absolute}.mobile-stage[data-v-3129d68a]{transform-origin:0 0;z-index:1;position:absolute;overflow:visible}.countdown-modal[data-v-3129d68a]{z-index:10;pointer-events:none;background-color:#0003;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.countdown-number[data-v-3129d68a]{color:#fff;text-shadow:0 18px 38px #00000040;font-size:300pt;font-weight:800;line-height:1;animation:1s both countdown-pop-3129d68a}@keyframes countdown-pop-3129d68a{0%{opacity:0;transform:scale(.45)}18%{opacity:1;transform:scale(1.14)}58%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.84)}}.login-page[data-v-d76172b7]{flex-direction:column;justify-content:space-between;gap:32px;display:flex}.brand[data-v-d76172b7]{align-items:center;gap:14px;padding-top:18px;display:flex}.brand-mark[data-v-d76172b7]{background:var(--color-primary);color:#fff;border-radius:8px;place-items:center;width:58px;height:58px;font-size:20px;font-weight:900;display:grid}.login-form[data-v-d76172b7]{gap:20px;padding:18px;display:grid}.field[data-v-d76172b7]{color:var(--color-muted);gap:8px;font-size:14px;display:grid}.field input[data-v-d76172b7]{border:1px solid var(--color-border);width:100%;height:48px;color:var(--color-text);background:#fff;border-radius:8px;outline:none;padding:0 14px}.field input[data-v-d76172b7]:focus{border-color:var(--color-primary)}.game-stage[data-v-53db301a]{width:750px;height:1624px;padding:0;position:absolute}.game-desc[data-v-53db301a]{top:50%;left:var(--stage-viewport-left,0);color:#fff;letter-spacing:10px;text-align:center;text-orientation:upright;writing-mode:vertical-rl;background:#280c0885;border-radius:0 12px 12px 0;justify-content:center;align-items:center;width:86px;height:256px;font-size:32pt;line-height:1.25;display:flex;position:absolute;transform:translateY(-50%)}.bg-bottom[data-v-53db301a]{bottom:0;left:var(--stage-viewport-left,0);width:var(--stage-viewport-width,750px);background:var(--c3e37f42) center/cover;opacity:.6;height:479px;position:absolute}.img-logo[data-v-53db301a]{background:var(--v23be7c19) center/cover;width:428px;height:77px;position:absolute;top:66px;left:50%;transform:translate(-50%)}.img-title[data-v-53db301a]{background:var(--v547f39aa) center/cover;width:609px;height:243px;position:absolute;top:200px;left:50%;transform:translate(-50%)}.img-avatar[data-v-53db301a]{background:var(--v1baa9447) center/cover;width:148px;height:148px;position:absolute;top:480px;left:50%;transform:translate(-50%)}.txt-nickname[data-v-53db301a]{color:#e00000;font-size:25pt;font-weight:700;position:absolute;top:650px;left:50%;transform:translate(-50%)}.txt-loading[data-v-53db301a]{color:#e00000;text-align:center;width:288px;font-size:25pt;font-weight:700;position:absolute;top:710px;left:50%;transform:translate(-50%)}.txt-loading label[data-v-53db301a]{transform-origin:50%;animation:1.2s ease-in-out infinite loading-text-scale-53db301a;display:inline-block}@keyframes loading-text-scale-53db301a{0%,to{transform:scale(1)}50%{transform:scale(1.08)}}.loading-gu[data-v-53db301a]{background:var(--ab7af248) center/cover;width:600px;height:534px;position:absolute;bottom:100px;left:50%;transform:translate(-50%)}.game-stage[data-v-a126c659]{width:750px;height:1624px;padding:0;position:absolute}.div-desc[data-v-a126c659]{top:150px;left:var(--stage-viewport-left,0);z-index:2;color:#fff;background:#280c087a;border-radius:0 56px 56px 0;justify-content:center;align-items:center;gap:24px;width:180px;height:80px;display:flex;position:absolute}.div-desc .desc-clock[data-v-a126c659]{background:var(--v1ed51044) center/cover;border-radius:50%;width:38px;height:44px;position:absolute;left:20px}.div-desc .desc-time[data-v-a126c659]{color:#fff;font-size:35pt;line-height:1;position:absolute;right:40px}.div-desc-slide-enter-active[data-v-a126c659]{transition:transform .36s ease-out .22s,opacity .36s ease-out .22s}.div-desc-slide-leave-active[data-v-a126c659]{transition:transform .26s ease-in,opacity .26s ease-in}.div-desc-slide-enter-from[data-v-a126c659],.div-desc-slide-leave-to[data-v-a126c659]{opacity:0;transform:translate(-110%)}.div-desc-slide-enter-to[data-v-a126c659],.div-desc-slide-leave-from[data-v-a126c659]{opacity:1;transform:translate(0)}.bg-bottom[data-v-a126c659]{bottom:0;left:var(--stage-viewport-left,0);width:var(--stage-viewport-width,750px);background:var(--ea746746) center/cover;opacity:.6;height:479px;position:absolute}.img-logo[data-v-a126c659]{background:var(--v640f204a) center/cover;width:428px;height:77px;position:absolute;top:66px;left:50%;transform:translate(-50%)}.play-content[data-v-a126c659]{color:#fff;text-align:center;background:#280c0885;border-radius:25px;margin-top:240px;padding:30px 0;font-size:28pt}.play-content>div[data-v-a126c659]:nth-child(2){color:#0000;transform-origin:50%;background:linear-gradient(#ffe68d 0%,#fad500 100%);-webkit-background-clip:text;background-clip:text;margin:10px 0;font-size:48pt;font-weight:700;display:inline-block;transform:scaleY(1.2)}.play-content>div[data-v-a126c659]:nth-child(3){position:relative;top:-10px}.play-content>div:nth-child(3) label[data-v-a126c659]{color:#fff;font-size:42pt}.play-gu[data-v-a126c659]{width:400px;height:400px;position:absolute;bottom:350px;left:50%;transform:translate(-50%)scale(2.4)}.game-stage[data-v-ae12dba1]{width:750px;height:1624px;padding:0;position:absolute}.bg-bottom[data-v-ae12dba1]{bottom:0;left:var(--stage-viewport-left,0);width:var(--stage-viewport-width,750px);background:var(--v303763f5) center/cover;opacity:.6;height:479px;position:absolute}.img-logo[data-v-ae12dba1]{background:var(--v528c697a) center/cover;width:428px;height:77px;position:absolute;top:66px;left:50%;transform:translate(-50%)}.img-score[data-v-ae12dba1]{background:var(--v3d51dddf) center/cover;width:465px;height:731px;position:absolute;top:500px;left:50%;transform:translate(-50%)scale(1.2)}.score-avatar[data-v-ae12dba1]{background:var(--v05e27e1e) center/cover;width:60px;height:60px;position:absolute;top:-20px;left:50%;transform:translate(-50%)scale(2)}.score-content[data-v-ae12dba1]{text-align:center;width:100%;height:calc(100% - 100px);font-size:28pt;position:absolute;bottom:0;left:0}.score-content>div[data-v-ae12dba1]:first-child{color:#e00000;font-size:32pt;font-weight:700}.score-content>div[data-v-ae12dba1]:nth-child(2){margin-top:20px;font-size:32pt}.score-content>div[data-v-ae12dba1]:nth-child(3){margin-top:40px}.score-content>div[data-v-ae12dba1]:nth-child(4){color:#e00000;margin-top:30px;font-size:32pt;font-weight:700;transform:scaleY(2.4)}.score-content>div[data-v-ae12dba1]:nth-child(5){margin-top:40px}.score-content>div[data-v-ae12dba1]:nth-child(6){color:#e00000;margin-top:20px;font-size:32pt;font-weight:700}.score-content>div:nth-child(6) label[data-v-ae12dba1]{color:#e00000;margin:0 10px;font-size:42pt}.btn-group[data-v-ae12dba1]{gap:40px;display:flex;position:absolute;top:1354px;left:50%;transform:translate(-50%)}.btn-replay[data-v-ae12dba1],.btn-back[data-v-ae12dba1]{width:315px;height:100px}.btn-replay[data-v-ae12dba1]{background:var(--v1928477f) center/cover}.btn-back[data-v-ae12dba1]{background:var(--v5295eb02) center/cover}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>小游戏-H5</title>
<style>body{background:#fbf7ef;}</style>
<script type="module" crossorigin src="/assets/index-BsTKH5UX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DWVl7jhI.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
This diff is collapsed.
...@@ -74,20 +74,16 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -74,20 +74,16 @@ export function useGameSocket(options: GameSocketOptions) {
const { msg, state, data } = payload const { msg, state, data } = payload
if (state === 0) { if (state === 0) {
if (evt == 'connect_result') {
router.replace('/loading').then(() => { });
return;
}
if (evt === 'room_join_result') { if (evt === 'room_join_result') {
switch (data) { $toast(msg);
case 1: userJoinStatus.value = true; return; router.replace('/loading').then(() => { });
case 2: { return;
// $toast(msg) }
alert(msg) if (evt == 'submit_score_save') {
return;
}
case 3:
// alert(msg);
$toast(msg)
return;
}
} else if (evt == 'submit_score_save') {
//提交分数的情况下有问题的情况下,只重试3次 //提交分数的情况下有问题的情况下,只重试3次
if (resubmitcount >= 3) { if (resubmitcount >= 3) {
return; return;
...@@ -96,13 +92,9 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -96,13 +92,9 @@ export function useGameSocket(options: GameSocketOptions) {
resubmitcount++; resubmitcount++;
options.onRescoreSubmitted?.(resubmitcount); options.onRescoreSubmitted?.(resubmitcount);
}, 200); }, 200);
} else if (evt == 'room_close_result') {
userJoinStatus.value = false
if (data == options.gameId) {
options.onRoomClosed?.(data)
}
return; return;
} }
debugger
return; return;
} }
...@@ -113,10 +105,6 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -113,10 +105,6 @@ export function useGameSocket(options: GameSocketOptions) {
} }
switch (evt) { switch (evt) {
case 'connect_result': {
debugger
break;
}
case 'room_create_result': { case 'room_create_result': {
if (`game${data}` == options.gameId) { if (`game${data}` == options.gameId) {
// location.reload(); // location.reload();
...@@ -170,6 +158,10 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -170,6 +158,10 @@ export function useGameSocket(options: GameSocketOptions) {
options.onRoomBack?.(data); options.onRoomBack?.(data);
break; break;
} }
case 'room_close_result': {
$toast('管理员关闭了该房间')
break;
}
default: { default: {
debugger debugger
} }
......
<script setup lang="ts">
import { cssAssetUrl } from '@/commons/assets'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const imageUrls = {
bg: cssAssetUrl('loading-bg.png'),
loading: cssAssetUrl('loading.png'),
icon: cssAssetUrl('loading-icon.png')
// bg2: cssAssetUrl('game1/bg2.png'),
// logo: cssAssetUrl('logo.png'),
// title: cssAssetUrl('game1/title.png'),
// avatar: cssAssetUrl('avatar-yellow.png'),
// gu: cssAssetUrl('game1/gu.png'),
// clock: cssAssetUrl('game1/clock.png'),
// gu01: cssAssetUrl('game1/gu01.png'),
// gu02: cssAssetUrl('game1/gu02.png'),
// gu03: cssAssetUrl('game1/gu03.png'),
// scoreBg: cssAssetUrl('game1/score-bg.png'),
// back: cssAssetUrl('game1/back.png'),
// replay: cssAssetUrl('game1/replay.png'),
// close: cssAssetUrl('game1/close.png'),
// rule: cssAssetUrl('game1/rule.png')
}
onMounted(() => {
document.title = '小游戏'
})
onBeforeUnmount(() => {
})
</script>
<template>
<div class="bg">
<div class="loading">
<div class="icon"></div>
</div>
</div>
</template>
<style scoped>
.bg {
width: 100vw;
height: 100vh;
background: v-bind('imageUrls.bg') center no-repeat;
position: relative;
.loading {
width: 473px;
height: 733px;
background: v-bind('imageUrls.loading') center / cover no-repeat;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.72);
.icon {
width: 190px;
height: 232px;
background: v-bind('imageUrls.icon') center / cover no-repeat;
position: absolute;
left: 50%;
top: 50%;
transform-origin: center center;
animation: swing 0.8s ease-in-out infinite alternate;
}
}
}
@keyframes swing {
0% {
transform: translateX(-50%) scale(0.72) rotate(-30deg);
}
100% {
transform: translateX(-50%) scale(0.72) rotate(15deg);
}
}
</style>
...@@ -120,8 +120,22 @@ onMounted(() => { ...@@ -120,8 +120,22 @@ onMounted(() => {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
transform: translateX(-50%); transform: translateX(-50%);
animation: loading-text-scale 1.2s ease-in-out infinite;
} }
@keyframes loading-text-scale {
0%,
100% {
transform: translateX(-50%) scale(1);
}
50% {
transform: translateX(-50%) scale(1.08);
}
}
.txt-loading2 { .txt-loading2 {
width: 500px; width: 500px;
height: 90px; height: 90px;
......
...@@ -5,7 +5,7 @@ import MobileStage from '@/components/MobileStage.vue' ...@@ -5,7 +5,7 @@ import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket'
import LoadingView from './views/LoadingView.vue' import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue' import PlayingView from './views/PlayingView.vue'
import { $getWechat, $toast } from '@/commons/utils.ts' import { $format_str, $getWechat, $toast } from '@/commons/utils.ts'
import { playGame6Music, stopGame6Music } from '@/commons/music'; import { playGame6Music, stopGame6Music } from '@/commons/music';
type GameView = 'loading' | 'playing' type GameView = 'loading' | 'playing'
...@@ -326,7 +326,7 @@ const rankCloseHandler = () => { ...@@ -326,7 +326,7 @@ const rankCloseHandler = () => {
<div class="col-2"> <div class="col-2">
<div class="avatar" <div class="avatar"
:style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div> :style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div>
<div>{{ item.nickname }}</div> <div>{{ $format_str(item.nickname,12) }}</div>
</div> </div>
<div class="col-3">{{ item.score }}</div> <div class="col-3">{{ item.score }}</div>
</div> </div>
......
...@@ -56,8 +56,10 @@ onMounted(() => { ...@@ -56,8 +56,10 @@ onMounted(() => {
color: white; color: white;
letter-spacing: 2pt; letter-spacing: 2pt;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.txt-bottom { .txt-bottom {
position: absolute; position: absolute;
bottom: 130px; bottom: 130px;
...@@ -66,6 +68,20 @@ onMounted(() => { ...@@ -66,6 +68,20 @@ onMounted(() => {
color: white; color: white;
font-size: 28px; font-size: 28px;
text-align: center; text-align: center;
animation: loading-text-scale 1.2s ease-in-out infinite;
}
@keyframes loading-text-scale {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
} }
.game-stage { .game-stage {
...@@ -105,8 +121,10 @@ onMounted(() => { ...@@ -105,8 +121,10 @@ onMounted(() => {
height: 479px; height: 479px;
background: v-bind('imageUrls.bg2') center/cover; background: v-bind('imageUrls.bg2') center/cover;
opacity: 0.6; opacity: 0.6;
} }
.img-logo { .img-logo {
position: absolute; position: absolute;
top: 66px; top: 66px;
......
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils' import { $read } from '@/commons/utils'
import Loading from '@/pages/Loading.vue'
import Game1 from '@/pages/game1/Game.vue' import Game1 from '@/pages/game1/Game.vue'
import Game4 from '@/pages/game4/Game4.vue' import Game4 from '@/pages/game4/Game4.vue'
import Game5 from '@/pages/game5/Game5.vue' import Game5 from '@/pages/game5/Game5.vue'
...@@ -17,6 +18,11 @@ const router = createRouter({ ...@@ -17,6 +18,11 @@ const router = createRouter({
// redirect: () => (getWechatId() ? '/game1' : '/login'), // redirect: () => (getWechatId() ? '/game1' : '/login'),
// }, // },
{ {
path: '/loading',
name: 'Loading',
component: Loading,
},
{
path: '/game1', path: '/game1',
name: 'Game1', name: 'Game1',
component: Game1, component: Game1,
......
...@@ -23,24 +23,6 @@ export function stopAllMusic() { ...@@ -23,24 +23,6 @@ export function stopAllMusic() {
backgroundMusic = null; backgroundMusic = null;
} }
function playAudioTimes(audio: HTMLAudioElement, times: number) {
let count = 1
audio.currentTime = 0
audio.play()
audio.onended = () => {
if (count >= times) {
audio.onended = null
return
}
count++
audio.currentTime = 0
audio.play()
}
}
function getBackgroundMusic(music: string, force: boolean = true) { function getBackgroundMusic(music: string, force: boolean = true) {
if (force) { // if (force) { //
if (backgroundMusic) { if (backgroundMusic) {
...@@ -63,10 +45,12 @@ export async function playApplauseMusic() { ...@@ -63,10 +45,12 @@ export async function playApplauseMusic() {
const audio = getBackgroundMusic(applause) const audio = getBackgroundMusic(applause)
try { try {
playAudioTimes(audio, 2); //播放两次停止 await audio.play()
window.removeEventListener('pointerdown', playApplauseMusic)
window.removeEventListener('keydown', playApplauseMusic)
} catch { } catch {
window.addEventListener('pointerdown', playApplauseMusic, { once: false }) window.addEventListener('pointerdown', playApplauseMusic, { once: true })
window.addEventListener('keydown', playApplauseMusic, { once: false }) window.addEventListener('keydown', playApplauseMusic, { once: true })
} }
} }
......
...@@ -73,7 +73,7 @@ export function $read_socket_storge(): any { ...@@ -73,7 +73,7 @@ export function $read_socket_storge(): any {
} }
export function $format_str(str: string, num: number) { export function $format_str(str: string, num: number) {
if (str.length <= num - 2) { if (str.length <= num) {
return str; return str;
} }
return str.substring(0, num) + '...'; return str.substring(0, num) + '...';
......
...@@ -70,7 +70,7 @@ const renderAvatar = (item: any | null) => { ...@@ -70,7 +70,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -61,63 +61,34 @@ const emptyPlayer = (index: number): Player => ({ ...@@ -61,63 +61,34 @@ const emptyPlayer = (index: number): Player => ({
}); });
const list = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index))); const list = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index)));
const playerKey = (player: Player, index = 0) => player.userid || player.wechat_id || `empty-${index}`; const playerKey = (player: Player, index = 0) => {
const mockRankStep = ref(0); if (player.userid || player.wechat_id) {
return player.userid || player.wechat_id || `empty-${index}`;
}
if (player.nickname !== '-') {
return `player-${player.nickname}-${player.avatar || ''}`;
}
return `empty-${index}`;
};
const makeMockPlayer = (id: number, score: number): Player => ({ const playRankAnimation = (rankList: Player[]) => {
wechat_id: `mock-${id}`, const oldRanks = new Map(list.value.map((player, index) => [playerKey(player, index), index]));
nickname: `Player ${id}`, const next = Array.from(
avatar: '', { length: RANK_LIMIT },
score, (_, index) => rankList[index] ? { ...rankList[index] } : emptyPlayer(index),
}); );
const changed = next.some((player, index) => {
const current = list.value[index] ?? emptyPlayer(index);
return playerKey(player, index) !== playerKey(current, index)
|| player.score !== current.score;
});
const mockRankRounds: Player[][] = [ if (!changed) return;
[
makeMockPlayer(1, 980),
makeMockPlayer(2, 900),
makeMockPlayer(3, 860),
makeMockPlayer(4, 820),
makeMockPlayer(5, 780),
makeMockPlayer(6, 720),
makeMockPlayer(7, 680),
makeMockPlayer(8, 640),
makeMockPlayer(9, 600),
makeMockPlayer(10, 560),
],
[
makeMockPlayer(4, 1040),
makeMockPlayer(1, 1010),
makeMockPlayer(7, 960),
makeMockPlayer(2, 940),
makeMockPlayer(11, 880),
makeMockPlayer(3, 850),
makeMockPlayer(5, 830),
makeMockPlayer(8, 790),
makeMockPlayer(6, 760),
makeMockPlayer(10, 700),
],
[
makeMockPlayer(7, 1180),
makeMockPlayer(11, 1090),
makeMockPlayer(4, 1060),
makeMockPlayer(12, 1000),
makeMockPlayer(1, 980),
makeMockPlayer(8, 930),
makeMockPlayer(2, 890),
makeMockPlayer(5, 850),
makeMockPlayer(13, 810),
makeMockPlayer(3, 780),
],
];
const updateRankList = (rankList: Player[]) => {
const oldRanks = new Map(list.value.map((player, index) => [playerKey(player, index), index]));
const next = Array.from({ length: RANK_LIMIT }, (_, index) => rankList[index] ?? emptyPlayer(index));
const effects: Record<string, "up" | "down" | "new"> = {}; const effects: Record<string, "up" | "down" | "new"> = {};
next.forEach((player, index) => { next.forEach((player, index) => {
const key = playerKey(player, index); const key = playerKey(player, index);
if (!key || key.startsWith("empty-")) return; if (key.startsWith('empty-')) return;
const oldIndex = oldRanks.get(key); const oldIndex = oldRanks.get(key);
if (oldIndex === undefined) effects[key] = "new"; if (oldIndex === undefined) effects[key] = "new";
...@@ -134,17 +105,10 @@ const updateRankList = (rankList: Player[]) => { ...@@ -134,17 +105,10 @@ const updateRankList = (rankList: Player[]) => {
watch( watch(
() => props.rankList, () => props.rankList,
(rankList) => updateRankList(rankList), (rankList) => playRankAnimation(rankList),
{ immediate: true, deep: true }, { immediate: true, deep: true, flush: 'post' },
); );
const playMockRankAnimation = () => {
if ($is_run_local()) {
updateRankList(mockRankRounds[mockRankStep.value] ?? mockRankRounds[0] ?? []);
mockRankStep.value = (mockRankStep.value + 1) % mockRankRounds.length;
}
};
const startGame = () => { const startGame = () => {
if ($is_run_local()) { if ($is_run_local()) {
emit('start'); emit('start');
...@@ -263,7 +227,7 @@ const avatarStyle = (item: Player) => { ...@@ -263,7 +227,7 @@ const avatarStyle = (item: Player) => {
</div> </div>
</div> </div>
<div class="rank-container"> <div class="rank-container">
<div class="rank-txt" @click="startGame()" @contextmenu.prevent="playMockRankAnimation()"></div> <div class="rank-txt" @click="startGame()"></div>
<TransitionGroup name="rank" tag="div" class="rank-list"> <TransitionGroup name="rank" tag="div" class="rank-list">
<div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item" <div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item"
:class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']"> :class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']">
...@@ -461,12 +425,13 @@ const avatarStyle = (item: Player) => { ...@@ -461,12 +425,13 @@ const avatarStyle = (item: Player) => {
} }
.rank-move { .rank-move {
transition: transform 0.45s ease; z-index: 1;
transition: transform 0.65s cubic-bezier(0.22, 1, 0.36, 1);
} }
.rank-enter-active, .rank-enter-active,
.rank-leave-active { .rank-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease; transition: opacity 0.35s ease, transform 0.35s ease;
} }
.rank-leave-active { .rank-leave-active {
...@@ -477,7 +442,7 @@ const avatarStyle = (item: Player) => { ...@@ -477,7 +442,7 @@ const avatarStyle = (item: Player) => {
.rank-enter-from, .rank-enter-from,
.rank-leave-to { .rank-leave-to {
opacity: 0; opacity: 0;
transform: translateY(90px); transform: translateY(70px);
} }
.rank-item.up, .rank-item.up,
......
...@@ -76,7 +76,7 @@ const releaseStartButton = () => { ...@@ -76,7 +76,7 @@ const releaseStartButton = () => {
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname"> <div class="txt-nickname">
{{ $format_str(item.nickname, 12) }} {{ $format_str(item.nickname, 8) }}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -71,7 +71,7 @@ const renderAvatar = (item: any | null) => { ...@@ -71,7 +71,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -301,7 +301,7 @@ const rankIcon = (index: number) => { ...@@ -301,7 +301,7 @@ const rankIcon = (index: number) => {
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-score">{{ item.score }}</div> <div class="txt-score">{{ item.score }}</div>
</div> </div>
</div> </div>
......
...@@ -82,7 +82,7 @@ const renderAvatar = (item: any | null) => { ...@@ -82,7 +82,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -370,7 +370,7 @@ const rankIcon = (index: number) => { ...@@ -370,7 +370,7 @@ const rankIcon = (index: number) => {
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div> <div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-score">{{ item.score }}</div> <div class="txt-score">{{ item.score }}</div>
</div> </div>
</div> </div>
......
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