Commit 2a8c14a9 authored by 陈冲's avatar 陈冲

fix: 页面修改

parent 7179132e
...@@ -10,7 +10,7 @@ use serde_json::{Value, json}; ...@@ -10,7 +10,7 @@ use serde_json::{Value, json};
use socketioxide::SocketIo; use socketioxide::SocketIo;
use socketioxide::extract::{Data, SocketRef}; use socketioxide::extract::{Data, SocketRef};
use socketioxide::socket::DisconnectReason; use socketioxide::socket::DisconnectReason;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
...@@ -436,6 +436,25 @@ fn broadcast_room_rank(ranking: Vec<Value>) { ...@@ -436,6 +436,25 @@ fn broadcast_room_rank(ranking: Vec<Value>) {
broadcast_msgpack_all_admin("room_rank_result", &ok_resp(Some(json!(ranking)))); broadcast_msgpack_all_admin("room_rank_result", &ok_resp(Some(json!(ranking))));
} }
fn emit_room_rank(socket: &SocketRef, ranking: Vec<Value>) {
emit_msgpack(socket, "room_rank_result", &ok_resp(Some(json!(ranking))));
}
fn broadcast_room_rank_to_spectators(ranking: &[Value], current_player_ids: &HashSet<String>) {
let payload = ok_resp(Some(json!(ranking)));
let Some(payload) = encode_msgpack(&payload) else {
return;
};
for player in CLIENT_PLAYER_MAP.iter() {
let userid = player.key();
let (state, socket) = player.value();
if *state == 0 && !current_player_ids.contains(userid) {
_ = socket.emit("room_rank_result", &payload);
}
}
}
fn emit_msgpack_to_user(userid: &str, event: &str, payload: &Value) { fn emit_msgpack_to_user(userid: &str, event: &str, payload: &Value) {
if userid.is_empty() { if userid.is_empty() {
return; return;
...@@ -454,7 +473,7 @@ fn is_current_player_socket(userid: &str, socket: &SocketRef) -> bool { ...@@ -454,7 +473,7 @@ fn is_current_player_socket(userid: &str, socket: &SocketRef) -> bool {
} }
fn push_room_score_ranking() -> bool { fn push_room_score_ranking() -> bool {
let (ranking, player_ranks) = if let Ok(state) = ROOM_STATE.lock() { let (ranking, player_ranks, current_player_ids) = if let Ok(state) = ROOM_STATE.lock() {
let full_ranking = state.full_score_ranking(); let full_ranking = state.full_score_ranking();
let ranking = RoomState::ranking_payload( let ranking = RoomState::ranking_payload(
&full_ranking &full_ranking
...@@ -468,13 +487,15 @@ fn push_room_score_ranking() -> bool { ...@@ -468,13 +487,15 @@ fn push_room_score_ranking() -> bool {
.enumerate() .enumerate()
.map(|(index, player)| (player.wechat, json!(index + 1))) .map(|(index, player)| (player.wechat, json!(index + 1)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let current_player_ids = state.players.keys().cloned().collect::<HashSet<_>>();
(ranking, player_ranks) (ranking, player_ranks, current_player_ids)
} else { } else {
return false; return false;
}; };
broadcast_room_rank(ranking); broadcast_room_rank(ranking.clone());
broadcast_room_rank_to_spectators(&ranking, &current_player_ids);
for (userid, rank) in player_ranks { for (userid, rank) in player_ranks {
emit_msgpack_to_user(&userid, "submit_score_result", &ok_resp(Some(rank))); emit_msgpack_to_user(&userid, "submit_score_result", &ok_resp(Some(rank)));
} }
...@@ -584,6 +605,16 @@ async fn handle_room_command( ...@@ -584,6 +605,16 @@ async fn handle_room_command(
) { ) {
let evt = &format!("{cmd}_result"); let evt = &format!("{cmd}_result");
match cmd { match cmd {
//回到首页时
"home"=>{
if !is_admin{
return;
}
GAME_ROUND_ID.fetch_add(1, Ordering::SeqCst);
if let Ok(mut state) = ROOM_STATE.lock() {
state.reset();
}
}
"room_create" | "room_back" => { "room_create" | "room_back" => {
//room_back返回当前游戏的Loading状态 //room_back返回当前游戏的Loading状态
// 只有管理员可以创建房间 // 只有管理员可以创建房间
...@@ -657,13 +688,6 @@ async fn handle_room_command( ...@@ -657,13 +688,6 @@ async fn handle_room_command(
} }
if matches!(state.status, RoomStatus::Closed) { if matches!(state.status, RoomStatus::Closed) {
let title = state.get_game_tilte(None);
drop(state);
emit_msgpack(
&socket,
evt,
&err_resp(&format!("当前游戏 - {} 房间已关闭", title), None),
);
return; return;
} else if matches!(state.status, RoomStatus::Running) { } else if matches!(state.status, RoomStatus::Running) {
//如果是正在游戏中 //如果是正在游戏中
...@@ -683,35 +707,15 @@ async fn handle_room_command( ...@@ -683,35 +707,15 @@ async fn handle_room_command(
emit_msgpack(&socket, "room_recover_result", &ok_resp(reconnect_payload)); emit_msgpack(&socket, "room_recover_result", &ok_resp(reconnect_payload));
return; return;
} else { } else {
emit_msgpack( let ranking = RoomState::ranking_payload(&state.score_ranking());
&socket, drop(state);
evt, emit_room_rank(socket, ranking);
&err_resp(
&format!(
"游戏 - {} 已经开始,您暂时无法进入",
state.get_game_tilte(None)
),
Some(json!(gameId)),
),
);
return; return;
} }
} else if matches!(state.status, RoomStatus::Submit | RoomStatus::Ended) { } else if matches!(state.status, RoomStatus::Submit | RoomStatus::Ended) {
//如果是在提交或游戏结束阶段断线重连 let ranking = RoomState::ranking_payload(&state.score_ranking());
// if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(userid) { drop(state);
// player.0 = 1; emit_room_rank(socket, ranking);
// }
emit_msgpack(
&socket,
evt,
&err_resp(
&format!(
"游戏 - {} 正在结算,您暂时无法进入",
state.get_game_tilte(None)
),
Some(json!(gameId)),
),
);
return; return;
} }
let entry_order = state.next_entry_order; let entry_order = state.next_entry_order;
...@@ -930,17 +934,20 @@ async fn handle_room_command( ...@@ -930,17 +934,20 @@ async fn handle_room_command(
return; return;
} }
let mut status = "".to_string();
let mut title = "".to_string();
if let Ok(mut state) = ROOM_STATE.lock() { if let Ok(mut state) = ROOM_STATE.lock() {
status = state.current_game_id.clone().unwrap_or_default();
title = state.get_game_tilte(None);
state.reset(); state.reset();
drop(state);
} }
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(status))));
broadcast_room_rank(Vec::new()); let player_sockets = CLIENT_PLAYER_MAP
.iter()
.map(|player| player.value().1.clone())
.collect::<Vec<_>>();
CLIENT_PLAYER_MAP.clear();
for player_socket in player_sockets {
_ = player_socket.disconnect();
}
emit_msgpack(&socket, evt, &ok_resp(None)); emit_msgpack(&socket, evt, &ok_resp(None));
} }
// 获取房间状态 // 获取房间状态
...@@ -982,7 +989,6 @@ async fn handle_room_command( ...@@ -982,7 +989,6 @@ async fn handle_room_command(
} }
pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value>) { pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value>) {
let is_create_user = false;
let userid = data let userid = data
.get("userid") .get("userid")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
...@@ -1093,47 +1099,59 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value ...@@ -1093,47 +1099,59 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
socket.on_disconnect({ socket.on_disconnect({
move |socket: SocketRef, _reason: DisconnectReason| async move { move |socket: SocketRef, _reason: DisconnectReason| async move {
if !is_create_user { if is_admin {
if is_admin { let Some(admin_session_version) = clear_current_admin_if_match(&userid, &socket)
let Some(admin_session_version) = else {
clear_current_admin_if_match(&userid, &socket) return;
else { };
return; // 前端页面刷新时房间状态丢失体验会不好
}; if get_admin_count() == 0 {
// 前端页面刷新时房间状态丢失体验会不好
if get_admin_count() == 0 {
if let Ok(mut state) = ROOM_STATE.lock() {
state.reset(); // 如果管理员都退出了,就把房间状态重置。
drop(state);
}
spawn_deactivate_players_if_admin_absent(admin_session_version);
}
} else {
let should_remove = CLIENT_PLAYER_MAP
.get(&userid)
.map(|player| player.1.id == socket.id)
.unwrap_or(false);
if !should_remove {
return;
}
CLIENT_PLAYER_MAP.remove(&userid);
if let Ok(mut state) = ROOM_STATE.lock() { if let Ok(mut state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Loading { state.reset(); // 如果管理员都退出了,就把房间状态重置。
if let Some(player) = state.players.get_mut(&userid) { drop(state);
player.online = false; }
spawn_deactivate_players_if_admin_absent(admin_session_version);
}
} else {
if !CLIENT_PLAYER_MAP
.get(&userid)
.map(|player| player.1.id == socket.id)
.unwrap_or(false)
{
return;
}
if let Ok(mut state) = ROOM_STATE.lock() {
match state.status {
RoomStatus::Closed => {
CLIENT_PLAYER_MAP.remove(&userid);
state.players.remove(&userid);
}
RoomStatus::Loading => {
CLIENT_PLAYER_MAP.remove(&userid);
if state.players.remove(&userid).is_none() {
return;
} }
return; let count = state.players.len();
let ranking = state.current_ranking_payload();
drop(state);
broadcast_msgpack_all_admin(
"room_join_result",
&ok_resp(Some(json!({"list":ranking,"count":count}))),
);
} }
if state.players.remove(&userid).is_none() { RoomStatus::Running | RoomStatus::Submit | RoomStatus::Ended => {
return; let Some(player) = state.players.get_mut(&userid) else {
return;
};
player.online = false;
if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(&userid) {
player.0 = 0;
}
drop(state);
// 暂不发送
// broadcast_msgpack_all_admin("player_offline_result", &ok_resp(None));
} }
let count = state.players.len();
let ranking = state.current_ranking_payload();
drop(state);
broadcast_msgpack_all_admin(
"room_join_result",
&ok_resp(Some(json!({"list":ranking,"count":count}))),
);
} }
} }
} }
......
...@@ -201,7 +201,9 @@ ...@@ -201,7 +201,9 @@
this.message = msg; this.message = msg;
}, },
create_sio: function (args) { create_sio: function (args) {
let socket = io(`ws://127.0.0.1:8082/ws`, { let ws = 'https://hddp.guocai365.org.cn/ws';
ws = 'ws://127.0.0.1:8082/ws';
let socket = io(ws, {
path: "/socket.io", path: "/socket.io",
auth: args.auth, auth: args.auth,
transports: ["websocket"], transports: ["websocket"],
......
VITE_ASSET_BUILD_LOCAL=true
VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/games/h5/assets/images/
VITE_ASSET_BASE_URL=https://your-bucket.oss-cn-shanghai.aliyuncs.com VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/games/h5/assets/images/
...@@ -20,6 +20,19 @@ npm run dev ...@@ -20,6 +20,19 @@ npm run dev
npm run build npm run build
``` ```
### Build With OSS or Local Assets
```sh
# Use VITE_ASSET_BASE_URL from .env.production
npm run build:oss
# Bundle with local assets and ignore the OSS URL
npm run build:local
# Serve the local build over HTTP
npm run serve:local
```
## Image Assets ## Image Assets
Put local development images in `src/assets/images`. Put local development images in `src/assets/images`.
...@@ -40,14 +53,14 @@ Default behavior: ...@@ -40,14 +53,14 @@ Default behavior:
For OSS/CDN production builds, create `.env.production`: For OSS/CDN production builds, create `.env.production`:
```env ```env
VITE_ASSET_BASE_URL=https://your-bucket.oss-cn-shanghai.aliyuncs.com VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/games/h5/assets/images/
``` ```
If you need to test OSS during development: If you need to test OSS during development:
```env ```env
VITE_ASSET_DEV_LOCAL=false VITE_ASSET_DEV_LOCAL=false
VITE_ASSET_BASE_URL=https://your-bucket.oss-cn-shanghai.aliyuncs.com VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/games/h5/assets/images/
``` ```
Production image resources should be uploaded to OSS/CDN, or served from `/assets/images/...` if no OSS base URL is configured. H5 resources should be uploaded under `games/h5/assets/images/`. Production image resources are served from `/h5/assets/images/...` when using the local-assets build.
...@@ -8,6 +8,7 @@ declare const __DEBUG__: boolean; ...@@ -8,6 +8,7 @@ declare const __DEBUG__: boolean;
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_ASSET_BASE_URL?: string; readonly VITE_ASSET_BASE_URL?: string;
readonly VITE_ASSET_BUILD_LOCAL?: string;
readonly VITE_ASSET_DEV_LOCAL?: string; readonly VITE_ASSET_DEV_LOCAL?: string;
} }
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>游戏-H5</title> <title>互动游戏-H5</title>
<style>body{background: #FA6047;}</style> <style>body{background: #FA6047;}</style>
<script>localStorage.setItem('domain', '');</script> <script>localStorage.setItem('domain', '');</script>
</head> </head>
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build:oss": "npm run type-check && vite build --mode production",
"build:local": "npm run type-check && vite build --mode local-assets",
"preview": "vite preview", "preview": "vite preview",
"serve:local": "vite preview --host 0.0.0.0",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
}, },
......
...@@ -43,6 +43,8 @@ body { ...@@ -43,6 +43,8 @@ body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
} }
button, button,
......
...@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string { ...@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
} }
const normalizedPath = normalizeAssetPath(path); const normalizedPath = normalizeAssetPath(path);
const configuredBaseUrl = import.meta.env.VITE_ASSET_BUILD_LOCAL === 'true'
? undefined
: options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL;
if (import.meta.env.PROD && configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
}
const preferLocal = options.preferLocal ?? ( const preferLocal = options.preferLocal ?? (
import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false' import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false'
); );
...@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string { ...@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
return localAssetUrl(normalizedPath); return localAssetUrl(normalizedPath);
} }
const baseUrl = options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL; if (configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
if (baseUrl) {
return joinUrl(baseUrl, normalizedPath);
} }
return localAssetUrl(normalizedPath); return localAssetUrl(normalizedPath);
......
import game1MusicUrl from '@/assets/images/game1.mp3' import { assetUrl } from '@/commons/assets'
import game6Music1Url from '@/assets/images/game6_1.mp3'
import game6Music2Url from '@/assets/images/game6_2.mp3' const game1MusicUrl = assetUrl('game1.mp3')
import go321 from '@/assets/images/321go.mp3' const game2MusicUrl = assetUrl('game2.mp3')
const game3Music1Url = assetUrl('game3_1.mp3')
const game3Music2Url = assetUrl('game3_2.mp3')
const game6Music1Url = assetUrl('game6_1.mp3')
const game6Music2Url = assetUrl('game6_2.mp3')
const go321 = assetUrl('321go.mp3')
const effectTemplates = new Map<string, HTMLAudioElement>() const effectTemplates = new Map<string, HTMLAudioElement>()
const playingEffects = new Set<HTMLAudioElement>() const playingEffects = new Set<HTMLAudioElement>()
const exclusiveEffects = new Map<string, HTMLAudioElement>()
let pendingPlay = false let pendingPlay = false
let pendingRetry: (() => void) | null = null let pendingRetry: (() => void) | null = null
export function stopAllMusic() {
stopGame1Music();
stopGame2Music();
stopGame3Music();
// stopGame4Music();
// stopGame5Music();
stopGame6Music();
playingEffects.clear();
}
function getEffectTemplate(music: string) { function getEffectTemplate(music: string) {
let template = effectTemplates.get(music) let template = effectTemplates.get(music)
...@@ -48,13 +67,25 @@ function setPendingRetry(retry: () => void) { ...@@ -48,13 +67,25 @@ function setPendingRetry(retry: () => void) {
window.addEventListener('keydown', retry, { once: true }) window.addEventListener('keydown', retry, { once: true })
} }
async function playEffect(music: string, retry: () => void) { async function playEffect(music: string, retry: () => void, exclusive = false) {
if (exclusive && exclusiveEffects.has(music)) {
return
}
const audio = getEffectTemplate(music).cloneNode(true) as HTMLAudioElement const audio = getEffectTemplate(music).cloneNode(true) as HTMLAudioElement
audio.loop = false audio.loop = false
audio.volume = 1 audio.volume = 1
playingEffects.add(audio) playingEffects.add(audio)
if (exclusive) {
exclusiveEffects.set(music, audio)
}
const release = () => releaseEffect(audio) const release = () => {
if (exclusiveEffects.get(music) === audio) {
exclusiveEffects.delete(music)
}
releaseEffect(audio)
}
audio.addEventListener('ended', release, { once: true }) audio.addEventListener('ended', release, { once: true })
audio.addEventListener('error', release, { once: true }) audio.addEventListener('error', release, { once: true })
...@@ -64,7 +95,7 @@ async function playEffect(music: string, retry: () => void) { ...@@ -64,7 +95,7 @@ async function playEffect(music: string, retry: () => void) {
clearPendingRetry() clearPendingRetry()
} }
} catch { } catch {
releaseEffect(audio) release()
if (!pendingPlay) { if (!pendingPlay) {
setPendingRetry(retry) setPendingRetry(retry)
...@@ -78,6 +109,7 @@ function stopEffects() { ...@@ -78,6 +109,7 @@ function stopEffects() {
releaseEffect(audio) releaseEffect(audio)
} }
playingEffects.clear() playingEffects.clear()
exclusiveEffects.clear()
} }
export async function playGame1Music() { export async function playGame1Music() {
...@@ -90,6 +122,7 @@ export function stopGame1Music() { ...@@ -90,6 +122,7 @@ export function stopGame1Music() {
stopEffects() stopEffects()
} }
/* 3 2 1 go */
export async function playCountdownMusic() { export async function playCountdownMusic() {
await playEffect(go321, playCountdownMusic) await playEffect(go321, playCountdownMusic)
} }
...@@ -100,6 +133,35 @@ export function stopCountdownMusic() { ...@@ -100,6 +133,35 @@ export function stopCountdownMusic() {
stopEffects() stopEffects()
} }
export async function playGame2Music() {
await playEffect(game2MusicUrl, playGame2Music, true)
}
export function stopGame2Music() {
window.removeEventListener('pointerdown', playGame2Music)
window.removeEventListener('keydown', playGame2Music)
stopEffects()
}
/*
接到红包3-1
接到炸弹3-2
*/
export async function playGame3Music(index: number) {
let arr: [string, string, string] = ['', game3Music1Url, game3Music2Url];
const music = arr[index]
if (!music) {
return
}
const retry = () => { playGame3Music(index) }
await playEffect(music, retry)
}
export function stopGame3Music() {
stopEffects()
}
/* /*
打地鼠击中爆炸6-1 打地鼠击中爆炸6-1
......
...@@ -83,7 +83,7 @@ export function $toast(message?: string, duration = 2000) { ...@@ -83,7 +83,7 @@ export function $toast(message?: string, duration = 2000) {
'border-radius: 18px', 'border-radius: 18px',
'background: rgba(0, 0, 0, 0.72)', 'background: rgba(0, 0, 0, 0.72)',
'color: #fff', 'color: #fff',
'font-size: 22px', 'font-size: 16px',
'line-height: 1.4', 'line-height: 1.4',
'text-align: center', 'text-align: center',
'transform: translate(-50%, -40%)', 'transform: translate(-50%, -40%)',
...@@ -306,6 +306,25 @@ function $query<T extends string | string[]>(q: T): T { ...@@ -306,6 +306,25 @@ function $query<T extends string | string[]>(q: T): T {
return params.get(q) as T; return params.get(q) as T;
} }
export function $remove_socket_storage(): void {
localStorage.removeItem('nickname');
localStorage.removeItem('token');
localStorage.removeItem('avatar');
localStorage.removeItem('token_origin');
}
export function $read_socket_storge(): any | null {
let token = $read('token', '');
let nickname = $read('nickname', '');
const avatar = $read('avatar', '');
const token_origin = $read('token_origin', '');
if (token && token_origin) {
return { nickname, avatar, token, token_origin }
}
return null;
}
export function $getWechat(): any | null { export function $getWechat(): any | null {
const q = $query(['token', 'nickname', 'avatar']); const q = $query(['token', 'nickname', 'avatar']);
if (q && q.length == 3) { if (q && q.length == 3) {
...@@ -316,7 +335,6 @@ export function $getWechat(): any | null { ...@@ -316,7 +335,6 @@ export function $getWechat(): any | null {
avatar: q[2], avatar: q[2],
}; };
} }
// 调试模式:无真实微信参数时使用模拟数据 // 调试模式:无真实微信参数时使用模拟数据
if (__DEBUG__) { if (__DEBUG__) {
const mockToken = 'debug_mock_token_' + Date.now() const mockToken = 'debug_mock_token_' + Date.now()
......
...@@ -57,6 +57,7 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -57,6 +57,7 @@ export function initSocket(args: SocketInitArgs): Socket | null {
upgrade: false, upgrade: false,
forceNew: true, forceNew: true,
timeout: 10000, timeout: 10000,
// autoConnect: false,
}); });
socket.on('connect', () => { socket.on('connect', () => {
......
...@@ -14,6 +14,7 @@ type GameSocketOptions = { ...@@ -14,6 +14,7 @@ type GameSocketOptions = {
gameId: string, gameId: string,
auth: any, auth: any,
onConnect?: () => void, onConnect?: () => void,
onShowRank?: (data: any) => void,
onGameStart?: (data: any) => void onGameStart?: (data: any) => void
onScoreSubmitted?: (is_save: boolean, data: any) => void onScoreSubmitted?: (is_save: boolean, data: any) => void
onRescoreSubmitted?: (recount: number) => void onRescoreSubmitted?: (recount: number) => void
...@@ -64,7 +65,10 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -64,7 +65,10 @@ export function useGameSocket(options: GameSocketOptions) {
onConnect: (socket) => { onConnect: (socket) => {
options.onConnect?.(); options.onConnect?.();
}, },
onDisconnect: (_reason: string) => { }, onDisconnect: (_reason: string) => {
// $toast('网络中断,尝试重新连接...')
socket = null;
},
onError: (error: any) => { onError: (error: any) => {
// console.error('socket.io connect_error:', error) // console.error('socket.io connect_error:', error)
}, },
...@@ -74,13 +78,18 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -74,13 +78,18 @@ export function useGameSocket(options: GameSocketOptions) {
const { msg, state, data } = payload const { msg, state, data } = payload
if (state === 0) { if (state === 0) {
if (evt == 'room_rank_result') {
// options.show_rank?.(data);
// return;
}
if (evt == 'connect_result') { if (evt == 'connect_result') {
router.replace('/loading').then(() => { }); // $toast(msg);
// router.replace('/loading').then(() => { });
return; return;
} }
if (evt === 'room_join_result') { if (evt === 'room_join_result') {
$toast(msg); // $toast(msg);
router.replace('/loading').then(() => { }); // router.replace('/loading').then(() => { });
return; return;
} }
if (evt == 'submit_score_save') { if (evt == 'submit_score_save') {
...@@ -106,25 +115,25 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -106,25 +115,25 @@ export function useGameSocket(options: GameSocketOptions) {
switch (evt) { switch (evt) {
case 'room_create_result': { case 'room_create_result': {
if (`game${data}` == options.gameId) { // if (`game${data}` == options.gameId) {
// location.reload(); // // location.reload();
options.onConnect?.(); // options.onConnect?.();
// debugger // // debugger
return; // return;
} // }
const nextPath = getGameHomePath(data) // const nextPath = getGameHomePath(data)
if (nextPath && router.currentRoute.value.path !== nextPath) { // if (nextPath && router.currentRoute.value.path !== nextPath) {
router.replace(nextPath) // router.replace(nextPath)
} // }
break // break
} }
case 'room_join_result': { case 'room_join_result': {
if (data != options.gameId) { if (data != options.gameId) {
debugger // debugger
return; return;
} }
$toast('您已进入游戏房间') $toast('您已进入当前游戏房间')
userJoinStatus.value = true userJoinStatus.value = true
break break
} }
...@@ -162,6 +171,11 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -162,6 +171,11 @@ export function useGameSocket(options: GameSocketOptions) {
$toast('管理员关闭了游戏房间') $toast('管理员关闭了游戏房间')
break; break;
} }
case 'room_rank_result': {
//如果是游戏开始中再进入会收到这个排行榜消息
options.onShowRank?.(data);
return;
}
default: { default: {
debugger debugger
} }
......
...@@ -37,7 +37,7 @@ if (wechat) { ...@@ -37,7 +37,7 @@ if (wechat) {
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
} }
onMounted(() => { onMounted(() => {
document.title = '互动游戏' document.title = '互动游戏 - 等待中...'
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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 ScoreView from './views/ScoreView.vue' import ScoreView from './views/ScoreView.vue'
import { $getWechat, $toast } from '@/commons/utils.ts' import { $getWechat, $toast } from '@/commons/utils.ts'
import { playGame1Music } from '@/commons/music' import { playGame1Music, stopAllMusic } from '@/commons/music'
type GameView = 'loading' | 'playing' | 'score' type GameView = 'loading' | 'playing' | 'score'
...@@ -131,6 +131,7 @@ function startGameView() { ...@@ -131,6 +131,7 @@ function startGameView() {
} }
function showScoreView() { function showScoreView() {
showGameRule.value = false;
stopGameCountdown() stopGameCountdown()
setDivDescVisible(false) setDivDescVisible(false)
currentView.value = 'score' currentView.value = 'score'
...@@ -178,6 +179,10 @@ if (wechat) { ...@@ -178,6 +179,10 @@ if (wechat) {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200) }, 200)
}, },
onShowRank: (data) => {
//第一个游戏没有排行榜,只显示结果
showScoreView();
},
onGameStart: async () => { onGameStart: async () => {
showGameRule.value = false; showGameRule.value = false;
await mobileStageRef.value?.startCountdown() await mobileStageRef.value?.startCountdown()
...@@ -216,10 +221,6 @@ if (wechat) { ...@@ -216,10 +221,6 @@ if (wechat) {
$toast('管理员关闭了游戏房间') $toast('管理员关闭了游戏房间')
}, },
onRoomBack: () => { onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}, },
}) })
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
...@@ -238,6 +239,7 @@ onBeforeUnmount(() => { ...@@ -238,6 +239,7 @@ onBeforeUnmount(() => {
window.clearTimeout(guFrameTimer) window.clearTimeout(guFrameTimer)
} }
stopGameCountdown() stopGameCountdown()
stopAllMusic();
if (token.value) { if (token.value) {
window.removeEventListener('beforeunload', confirmRefresh) window.removeEventListener('beforeunload', confirmRefresh)
} }
......
...@@ -34,7 +34,8 @@ onMounted(() => { ...@@ -34,7 +34,8 @@ onMounted(() => {
<div>击鼓积分</div> <div>击鼓积分</div>
<div>{{ tick }}</div> <div>{{ tick }}</div>
<div>最终排名</div> <div>最终排名</div>
<div><label>{{ rank }}</label></div> <div v-if="rank>0"><label>{{ rank }}</label></div>
<div v-else>未上榜</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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 { $format_str, $getWechat, $toast } from '@/commons/utils.ts' import { $format_str, $getWechat, $toast } from '@/commons/utils.ts'
import { playGame6Music, stopGame6Music } from '@/commons/music'; import { playGame2Music, stopAllMusic } from '@/commons/music';
type GameView = 'loading' | 'playing' type GameView = 'loading' | 'playing'
type RankPlayer = { type RankPlayer = {
...@@ -22,6 +22,7 @@ const imageUrls: Record<string, string> = { ...@@ -22,6 +22,7 @@ const imageUrls: Record<string, string> = {
close: cssAssetUrl('game1/close.png'), close: cssAssetUrl('game1/close.png'),
clock: cssAssetUrl('game1/clock.png'), clock: cssAssetUrl('game1/clock.png'),
bg2: cssAssetUrl('game2/bg2.png'), bg2: cssAssetUrl('game2/bg2.png'),
point: cssAssetUrl('game2/point.png'),
rule: cssAssetUrl('game1/rule.png'), rule: cssAssetUrl('game1/rule.png'),
coin: cssAssetUrl('game2/coin.png'), coin: cssAssetUrl('game2/coin.png'),
process: cssAssetUrl('game2/process.png'), process: cssAssetUrl('game2/process.png'),
...@@ -166,6 +167,7 @@ function showGameOverRank() { ...@@ -166,6 +167,7 @@ function showGameOverRank() {
const touchHandler = (currentScore: number) => { const touchHandler = (currentScore: number) => {
score.value = currentScore score.value = currentScore
playGame2Music();
submitScore(false, currentScore) submitScore(false, currentScore)
} }
...@@ -181,6 +183,12 @@ if (wechat) { ...@@ -181,6 +183,12 @@ if (wechat) {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200) }, 200)
}, },
onShowRank: (data) => {
//第一个游戏没有排行榜,只显示结果
showScoreView();
rankList.value = data;
showGameRank.value = true;
},
onGameStart: async () => { onGameStart: async () => {
showGameRule.value = false; showGameRule.value = false;
await mobileStageRef.value?.startCountdown() await mobileStageRef.value?.startCountdown()
...@@ -219,7 +227,7 @@ if (wechat) { ...@@ -219,7 +227,7 @@ if (wechat) {
rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0 rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0 score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0
showGameRule.value = false showGameRule.value = false
showGameRank.value = false // showGameRank.value = false
currentView.value = 'playing' currentView.value = 'playing'
startGameCountdown( startGameCountdown(
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0, Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0,
...@@ -232,19 +240,21 @@ if (wechat) { ...@@ -232,19 +240,21 @@ if (wechat) {
$toast('管理员关闭了游戏房间') $toast('管理员关闭了游戏房间')
}, },
onRoomBack: () => { onRoomBack: () => {
console.log('onRoomBack');
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}, },
}) })
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
} }
function showScoreView() {
showGameRule.value = false;
stopGameCountdown()
setDivDescVisible(false)
currentView.value = 'loading'
}
onMounted(() => { onMounted(() => {
// currentView.value = 'playing' document.title = '互动游戏 - 聚宝接福'
document.title = '互动游戏 - 福运当头'
if (token.value) { if (token.value) {
window.addEventListener('beforeunload', confirmRefresh) window.addEventListener('beforeunload', confirmRefresh)
} }
...@@ -252,7 +262,7 @@ onMounted(() => { ...@@ -252,7 +262,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.() _offSocketMessage.value?.()
stopGame6Music() stopAllMusic()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer) window.clearTimeout(guFrameTimer)
} }
...@@ -327,8 +337,8 @@ const rankCloseHandler = () => { ...@@ -327,8 +337,8 @@ const rankCloseHandler = () => {
<div class="col-3">{{ rankScore }}</div> <div class="col-3">{{ rankScore }}</div>
</div> </div>
</div> </div>
<div class="img-close" @click="rankCloseHandler"></div> <div class="img-close" @click="rankCloseHandler"></div>
</div> </div>
</template> </template>
<LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus" <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" /> @touchGameRule="showGameRuleHandler" />
......
...@@ -18,14 +18,14 @@ onMounted(() => { ...@@ -18,14 +18,14 @@ onMounted(() => {
<template> <template>
<div class="h5-page game-stage"> <div class="h5-page game-stage">
<div class="game-desc" @click="$emit('touchGameRule')">游戏规则</div> <div class="game-desc" @click="$emit('touchGameRule')">游戏规则</div>
<div class="img-logo"></div> <!-- <div class="img-logo"></div> -->
<div class="img-coin"></div> <!-- <div class="img-coin"></div> -->
<div class="txt-bottom" v-if="userJoinStatus">您已成功加入游戏<br />等待主持人开始</div> <div class="txt-bottom" v-if="userJoinStatus">您已成功加入游戏<br />等待主持人开始</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.img-coin { /* .img-coin {
background: v-bind('imageUrls.coin') center / cover no-repeat; background: v-bind('imageUrls.coin') center / cover no-repeat;
width: 744px; width: 744px;
height: 756px; height: 756px;
...@@ -34,16 +34,18 @@ onMounted(() => { ...@@ -34,16 +34,18 @@ onMounted(() => {
top: 50%; top: 50%;
transform: translate(-50%, -50%) scale(0.6); transform: translate(-50%, -50%) scale(0.6);
transform-origin: center; transform-origin: center;
} } */
.txt-bottom { .txt-bottom {
position: absolute; position: absolute;
bottom: 130px; bottom: 230px;
width: 100%; width: 100%;
line-height: 40px; line-height: 40px;
color: white; color: white;
font-size: 28px; font-size: 28px;
text-align: center; text-align: center;
-webkit-text-stroke: 1px #E61903;
paint-order: stroke fill;
animation: loading-text-scale 1.2s ease-in-out infinite; animation: loading-text-scale 1.2s ease-in-out infinite;
} }
......
...@@ -128,8 +128,8 @@ onBeforeUnmount(stopInertia) ...@@ -128,8 +128,8 @@ onBeforeUnmount(stopInertia)
<span class="desc-time">{{ countdownInterval }}</span> <span class="desc-time">{{ countdownInterval }}</span>
</div> </div>
</Transition> </Transition>
<div class="img-logo"></div> <!-- <div class="img-logo"></div>
<div class="img-hydt"></div> <div class="img-hydt"></div> -->
<div class="rank-content"> <div class="rank-content">
<div class="txt-left"> <div class="txt-left">
<div>排名</div> <div>排名</div>
...@@ -140,6 +140,7 @@ onBeforeUnmount(stopInertia) ...@@ -140,6 +140,7 @@ onBeforeUnmount(stopInertia)
<div class="txt-bold">{{ tick }}</div> <div class="txt-bold">{{ tick }}</div>
</div> </div>
</div> </div>
<div class="img-pointer"></div>
<div ref="coinRef" class="img-coin" :style="{ '--coin-rotation': `${coinRotation}deg` }" <div ref="coinRef" class="img-coin" :style="{ '--coin-rotation': `${coinRotation}deg` }"
@pointerdown="startRotation" @pointermove="rotateCoin" @pointerup="stopRotation" @pointerdown="startRotation" @pointermove="rotateCoin" @pointerup="stopRotation"
@pointercancel="stopRotation"></div> @pointercancel="stopRotation"></div>
...@@ -222,6 +223,19 @@ onBeforeUnmount(stopInertia) ...@@ -222,6 +223,19 @@ onBeforeUnmount(stopInertia)
} }
} }
.img-pointer {
width: 354px;
height: 354px;
background: v-bind('imageUrls.point');
position: absolute;
left: 50%;
top: 50%;
margin-left: 141px;
margin-top: -9px;
z-index: 9;
transform: scale(0.9);
}
/* /*
.txt-tick { .txt-tick {
position: absolute; position: absolute;
...@@ -239,13 +253,13 @@ onBeforeUnmount(stopInertia) ...@@ -239,13 +253,13 @@ onBeforeUnmount(stopInertia)
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%) scale(0.5) rotate(var(--coin-rotation, 0deg)); transform: translate(-50%, -50%) scale(0.6) rotate(var(--coin-rotation, 0deg));
transform-origin: center; transform-origin: center;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
cursor: grab; cursor: grab;
will-change: transform; will-change: transform;
margin-top:60px; margin-top: 70px;
} }
.img-coin:active { .img-coin:active {
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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, $is_run_local } from '@/commons/utils.ts' import { $getWechat, $toast, $is_run_local } from '@/commons/utils.ts'
import { playGame6Music, stopGame6Music } from '@/commons/music'; import { playGame3Music, stopAllMusic } from '@/commons/music';
type GameView = 'loading' | 'playing' type GameView = 'loading' | 'playing'
type RankPlayer = { type RankPlayer = {
...@@ -101,12 +101,7 @@ function stopGameCountdown() { ...@@ -101,12 +101,7 @@ function stopGameCountdown() {
} }
function submitScore(save: boolean = false, currentTick = score.value) { function submitScore(save: boolean = false, currentTick = score.value) {
if (!wechat && !isLocalMode) return
if (save) { if (save) {
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 提交最终分数:', currentTick)
return
}
sendGameMessage('submit_score_save', { sendGameMessage('submit_score_save', {
score: currentTick, score: currentTick,
wechat: wechat.token_origin, wechat: wechat.token_origin,
...@@ -116,10 +111,6 @@ function submitScore(save: boolean = false, currentTick = score.value) { ...@@ -116,10 +111,6 @@ function submitScore(save: boolean = false, currentTick = score.value) {
rank: rank.value, rank: rank.value,
}) })
} else { } else {
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 实时分数:', currentTick)
return
}
sendGameMessage('submit_score', { score: currentTick }) sendGameMessage('submit_score', { score: currentTick })
} }
} }
...@@ -179,28 +170,17 @@ function startGameView() { ...@@ -179,28 +170,17 @@ function startGameView() {
function showGameOverRank() { function showGameOverRank() {
stopGameCountdown() stopGameCountdown()
setDivDescVisible(false) setDivDescVisible(false)
// 保持在 playing 视图,结果页由 PlayingView 内部渲染 currentView.value = 'loading'
isGameOver.value = true showGameRank.value = true
userJoinStatus.value = false userJoinStatus.value = false;
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 游戏结束,最终分数:', score.value)
}
} }
const touchHandler = (mole: boolean) => { const scoreChangeHandler = (delta: number) => {
let scoreDelta = mole ? 5 : -3; if (delta > 0) {
let nextTick = score.value + scoreDelta playGame3Music(1);
if (nextTick < 0) nextTick = 0
score.value = nextTick
if (mole) {
playGame6Music(1);
} else { } else {
playGame6Music(2); playGame3Music(2);
} }
submitScore(false, nextTick)
}
const scoreChangeHandler = (delta: number) => {
let nextTick = score.value + delta let nextTick = score.value + delta
if (nextTick < 0) nextTick = 0 if (nextTick < 0) nextTick = 0
score.value = nextTick score.value = nextTick
...@@ -215,7 +195,7 @@ if (wechat) { ...@@ -215,7 +195,7 @@ if (wechat) {
userid: wechat.token, userid: wechat.token,
}, },
onConnect: () => { onConnect: () => {
window.setTimeout(() => { window.setTimeout(() => {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200) }, 200)
}, },
...@@ -268,35 +248,24 @@ if (wechat) { ...@@ -268,35 +248,24 @@ if (wechat) {
stopGameCountdown() stopGameCountdown()
currentView.value = 'loading' currentView.value = 'loading'
setDivDescVisible(false) setDivDescVisible(false)
$toast('管理员关闭了房间') $toast('管理员关闭了游戏房间')
}, },
onRoomBack: () => { onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}, },
}) })
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
} }
onMounted(() => { onMounted(() => {
document.title = '小游戏 - 福运当头' document.title = '互动游戏 - 马上有福'
if (token.value) { if (token.value) {
window.addEventListener('beforeunload', confirmRefresh) window.addEventListener('beforeunload', confirmRefresh)
} }
// // TODO_LOCAL: 本地模拟游戏开始,稍后部署上线前删除此段
// if ($is_run_local()) {
// showGameRule.value = false
// currentView.value = 'playing'
// startGameCountdown()
// }
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.() _offSocketMessage.value?.()
stopGame6Music() stopAllMusic()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer) window.clearTimeout(guFrameTimer)
} }
...@@ -324,7 +293,7 @@ const rankCloseHandler = () => { ...@@ -324,7 +293,7 @@ const rankCloseHandler = () => {
</script> </script>
<template> <template>
<MobileStage v-if="token || isLocalMode" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef" <MobileStage v-if="token" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef"
:background="`${renderBG()}`"> :background="`${renderBG()}`">
<template #gameRule> <template #gameRule>
<div class="rule-container"> <div class="rule-container">
...@@ -373,16 +342,21 @@ const rankCloseHandler = () => { ...@@ -373,16 +342,21 @@ const rankCloseHandler = () => {
<div class="col-3">{{ rankScore }}</div> <div class="col-3">{{ rankScore }}</div>
</div> </div>
</div> </div>
</div> <div class="img-close" @click="rankCloseHandler"></div>
<div class="img-close" @click="rankCloseHandler"></div> </div>
</template> </template>
<LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus" <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" /> @touchGameRule="showGameRuleHandler" />
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval" <PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" />
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" />
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver" :is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore" :rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:user-nickname="nickname" :user-avatar="avatar" :user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> @touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</MobileStage> </MobileStage>
<div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"> <div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;">
请使用微信扫码进入游戏 请使用微信扫码进入游戏
...@@ -510,9 +484,10 @@ const rankCloseHandler = () => { ...@@ -510,9 +484,10 @@ const rankCloseHandler = () => {
.img-close { .img-close {
width: 57px; width: 57px;
height: 57px; height: 57px;
border-radius: 29px; background: v-bind('imageUrls.close') center / cover no-repeat;
background: v-bind('imageUrls.close')center/cover; transform: translateX(-50%) scale(1.4);
transform: scale(0.7); position: absolute;
margin-top: 500px; top: 0;
left: 100%;
} }
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, nextTick, computed } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch, nextTick, computed } from 'vue';
import { assetUrl } from '@/commons/assets.ts' import { assetUrl } from '@/commons/assets.ts'
import { playGame6Music } from '@/commons/music' // import { playGame6Music } from '@/commons/music'
const props = defineProps<{ const props = defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
...@@ -9,16 +9,10 @@ const props = defineProps<{ ...@@ -9,16 +9,10 @@ const props = defineProps<{
isDivDescVisible: boolean isDivDescVisible: boolean
tick: number tick: number
rank: number rank: number
isGameOver: boolean
rankList: Array<{ rank: number; nickname: string; avatar: string; score: number | string }>
userRank: number
userScore: number | string
userNickname: string
userAvatar: string
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
touch: [mole: boolean] // touch: [mole: boolean]
scoreChange: [delta: number] scoreChange: [delta: number]
rankClose: [] rankClose: []
}>(); }>();
...@@ -275,10 +269,8 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => { ...@@ -275,10 +269,8 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
if (isCoin) { if (isCoin) {
emit('scoreChange', 5); emit('scoreChange', 5);
playGame6Music(1);
} else { } else {
emit('scoreChange', -3); emit('scoreChange', -3);
playGame6Music(2);
} }
const animId = Date.now() + Math.random(); const animId = Date.now() + Math.random();
...@@ -431,13 +423,13 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -431,13 +423,13 @@ watch(() => props.isDivDescVisible, (visible) => {
}, { immediate: true }); }, { immediate: true });
// 游戏结束:暂停全部动画,显示结果页 // 游戏结束:暂停全部动画,显示结果页
watch(() => props.isGameOver, (over) => { // watch(() => props.isGameOver, (over) => {
if (over) { // if (over) {
stopRoadScroll(); // stopRoadScroll();
stopMainLoop(); // stopMainLoop();
stopItemGen(); // stopItemGen();
} // }
}); // });
</script> </script>
<template> <template>
...@@ -507,7 +499,7 @@ watch(() => props.isGameOver, (over) => { ...@@ -507,7 +499,7 @@ watch(() => props.isGameOver, (over) => {
<div class="img-right" @click="switchToRight"></div> <div class="img-right" @click="switchToRight"></div>
<!-- 游戏结束结果页覆盖层 --> <!-- 游戏结束结果页覆盖层 -->
<Transition name="rank-fade"> <!-- <Transition name="rank-fade">
<div v-if="isGameOver" class="rank-overlay"> <div v-if="isGameOver" class="rank-overlay">
<div class="ranklist-container"> <div class="ranklist-container">
<div class="txt-title">排行</div> <div class="txt-title">排行</div>
...@@ -541,7 +533,7 @@ watch(() => props.isGameOver, (over) => { ...@@ -541,7 +533,7 @@ watch(() => props.isGameOver, (over) => {
</div> </div>
<div class="img-rank-close" @click="$emit('rankClose')"></div> <div class="img-rank-close" @click="$emit('rankClose')"></div>
</div> </div>
</Transition> </Transition> -->
</div> </div>
</template> </template>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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 ScoreView from './views/ScoreView.vue' import ScoreView from './views/ScoreView.vue'
...@@ -159,15 +159,10 @@ function replayGame() { ...@@ -159,15 +159,10 @@ function replayGame() {
} }
function backToWaiting() { function backToWaiting() {
resetToLoadingView()
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
} }
function handleRoomBack() { function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
} }
const touchHandler = () => { const touchHandler = () => {
...@@ -187,8 +182,8 @@ if (wechat) { ...@@ -187,8 +182,8 @@ if (wechat) {
userid: wechat.token, userid: wechat.token,
}, },
onConnect: () => { onConnect: () => {
window.setTimeout(() => { window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200) }, 200)
}, },
onGameStart: async () => { onGameStart: async () => {
......
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import shakeSound from '@/assets/images/game4/yaoyiyao.mp3' import { assetUrl } from '@/commons/assets'
const shakeSound = assetUrl('game4/yaoyiyao.mp3')
const props = defineProps<{ const props = defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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 ScoreView from './views/ScoreView.vue' import ScoreView from './views/ScoreView.vue'
...@@ -192,10 +192,7 @@ function showGameOverRank() { ...@@ -192,10 +192,7 @@ function showGameOverRank() {
function handleRoomBack() { function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
} }
const hitHandler = (score: number) => { const hitHandler = (score: number) => {
...@@ -212,7 +209,7 @@ if (wechat) { ...@@ -212,7 +209,7 @@ if (wechat) {
userid: wechat.token, userid: wechat.token,
}, },
onConnect: () => { onConnect: () => {
window.setTimeout(() => { window.setTimeout(() => {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200) }, 200)
}, },
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts' import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue' import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket' import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } 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 { $format_str, $getWechat, $toast } from '@/commons/utils.ts' import { $format_str, $getWechat, $toast } from '@/commons/utils.ts'
import { playGame6Music, stopGame6Music } from '@/commons/music'; import { playGame6Music, stopAllMusic } from '@/commons/music';
type GameView = 'loading' | 'playing' type GameView = 'loading' | 'playing'
type RankPlayer = { type RankPlayer = {
...@@ -247,10 +247,6 @@ if (wechat) { ...@@ -247,10 +247,6 @@ if (wechat) {
$toast('管理员关闭了游戏房间') $toast('管理员关闭了游戏房间')
}, },
onRoomBack: () => { onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}, },
}) })
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
...@@ -265,7 +261,7 @@ onMounted(() => { ...@@ -265,7 +261,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.() _offSocketMessage.value?.()
stopGame6Music() stopAllMusic()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer) window.clearTimeout(guFrameTimer)
} }
...@@ -472,12 +468,6 @@ const rankCloseHandler = () => { ...@@ -472,12 +468,6 @@ const rankCloseHandler = () => {
} }
.img-close { .img-close {
/* width: 57px;
height: 57px;
border-radius: 29px;
background: v-bind('imageUrls.close')center/cover;
transform: scale(0.7);
margin-top: 500px; */
width: 57px; width: 57px;
height: 57px; height: 57px;
background: v-bind('imageUrls.close') center / cover no-repeat; background: v-bind('imageUrls.close') center / cover no-repeat;
......
<!-- <script setup lang="ts">
import { $getWechat } from '@/commons/utils';
import { onMounted, ref } from 'vue';
defineProps<{
imageUrls: Record<string, string>
tick: number
rank: number
}>()
defineEmits<{
showRank: []
replay: []
back: []
}>()
const nickname = ref('');
const avatar = ref('');
onMounted(() => {
const wechat = $getWechat();
nickname.value = wechat.nickname;
avatar.value = wechat.avatar;
});
</script>
<template>
<div class="h5-page game-stage">
<div class="img-logo"></div>
</div>
</template>
<style scoped>
.game-stage {
position: absolute;
width: 750px;
height: 1624px;
padding: 0;
}
.img-logo {
position: absolute;
top: 66px;
left: 50%;
width: 428px;
height: 77px;
background: v-bind('imageUrls.logo') center/cover;
transform: translateX(-50%);
}
</style> -->
...@@ -2,7 +2,7 @@ import { fileURLToPath, URL } from 'node:url' ...@@ -2,7 +2,7 @@ import { fileURLToPath, URL } from 'node:url'
import { cpSync, existsSync, rmSync } from 'node:fs' import { cpSync, existsSync, rmSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import basicSsl from '@vitejs/plugin-basic-ssl' import basicSsl from '@vitejs/plugin-basic-ssl'
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
...@@ -25,35 +25,40 @@ function copyImageAssets() { ...@@ -25,35 +25,40 @@ function copyImageAssets() {
} }
} }
export default defineConfig(({ command }) => ({ export default defineConfig(({ command, mode }) => {
base: command === 'build' ? '/h5/' : '/', const env = loadEnv(mode, process.cwd(), '')
define: { const useLocalAssets = env.VITE_ASSET_BUILD_LOCAL === 'true'
__APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
__DEBUG__: JSON.stringify(command === 'serve'), return {
}, base: command === 'build' ? '/h5/' : '/',
plugins: [ define: {
vue(), __APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
vueDevTools(), __DEBUG__: JSON.stringify(command === 'serve'),
basicSsl(), },
copyImageAssets(), plugins: [
], vue(),
server: { vueDevTools(),
host: true, basicSsl(),
proxy: { ...(useLocalAssets || !env.VITE_ASSET_BASE_URL ? [copyImageAssets()] : []),
'/socket.io': { ],
target: 'http://localhost:8082', server: {
changeOrigin: true, host: true,
ws: true, proxy: {
}, '/socket.io': {
'/manager': { target: 'http://localhost:8082',
target: 'http://localhost:8082', changeOrigin: true,
changeOrigin: true, ws: true,
},
'/manager': {
target: 'http://localhost:8082',
changeOrigin: true,
},
}, },
}, },
}, resolve: {
resolve: { alias: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)) },
}, },
}, }
})) })
VITE_ASSET_BUILD_LOCAL=true
VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/games/pc/assets/images/
VITE_ASSET_BASE_URL=https://your-bucket.oss-cn-shanghai.aliyuncs.com VITE_ASSET_BASE_URL=https://xjfcoss.guocai365.org.cn/jfsc/storage/
VITE_ASSET_DEV_LOCAL=false
\ No newline at end of file
...@@ -41,6 +41,16 @@ npm run dev ...@@ -41,6 +41,16 @@ npm run dev
npm run build npm run build
``` ```
### Build With OSS or Local Assets
```sh
# Use VITE_ASSET_BASE_URL from .env.production
npm run build:oss
# Bundle with local assets and ignore the OSS URL
npm run build:local
```
## Image Assets ## Image Assets
Put local development images in `src/assets/images`. Put local development images in `src/assets/images`.
......
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build:oss": "npm run type-check && vite build --mode production",
"build:local": "npm run type-check && vite build --mode local-assets",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
......
...@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string { ...@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
} }
const normalizedPath = normalizeAssetPath(path); const normalizedPath = normalizeAssetPath(path);
const configuredBaseUrl = import.meta.env.VITE_ASSET_BUILD_LOCAL === 'true'
? undefined
: options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL;
if (import.meta.env.PROD && configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
}
const preferLocal = options.preferLocal ?? ( const preferLocal = options.preferLocal ?? (
import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false' import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false'
); );
...@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string { ...@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
return localAssetUrl(normalizedPath); return localAssetUrl(normalizedPath);
} }
const baseUrl = options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL; if (configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
if (baseUrl) {
return joinUrl(baseUrl, normalizedPath);
} }
return localAssetUrl(normalizedPath); return localAssetUrl(normalizedPath);
......
import game1MusicUrl from '@/assets/images/game1.mp3' import { assetUrl } from '@/commons/assets'
import game2MusicUrl from '@/assets/images/game2.mp3'
import game4MusicUrl from '@/assets/images/cmyf-bgm.mp3' const game1MusicUrl = assetUrl('game1.mp3')
import game5MusicUrl from '@/assets/images/qcnf-bgm.mp3' const game2MusicUrl = assetUrl('game2.mp3')
import game6MusicUrl from '@/assets/images/game6.mp3' const game4MusicUrl = assetUrl('cmyf-bgm.mp3')
import go321 from '@/assets/images/321go.mp3' const game5MusicUrl = assetUrl('qcnf-bgm.mp3')
import applause from '@/assets/images/applause.mp3' const game6MusicUrl = assetUrl('game6.mp3')
const go321 = assetUrl('321go.mp3')
const applause = assetUrl('applause.mp3')
let backgroundMusic: HTMLAudioElement | null = null let backgroundMusic: HTMLAudioElement | null = null
let countdownTemplate: HTMLAudioElement | null = null let countdownTemplate: HTMLAudioElement | null = null
......
...@@ -147,6 +147,152 @@ export function $toast(message?: string, duration = 2000) { ...@@ -147,6 +147,152 @@ export function $toast(message?: string, duration = 2000) {
}, duration); }, duration);
} }
export type NotifyType = 'info' | 'success' | 'warning' | 'error';
export type NotifyOptions = {
title?: string;
message?: string;
type?: NotifyType;
duration?: number;
}
let notifyElement: HTMLDivElement | undefined;
let notifyTitleElement: HTMLDivElement | undefined;
let notifyMessageElement: HTMLDivElement | undefined;
let notifyTimer: ReturnType<typeof window.setTimeout> | undefined;
let notifyHideTimer: ReturnType<typeof window.setTimeout> | undefined;
function closeNotify() {
if (!notifyElement) {
return;
}
if (notifyTimer) {
window.clearTimeout(notifyTimer);
notifyTimer = undefined;
}
if (notifyHideTimer) {
window.clearTimeout(notifyHideTimer);
}
notifyElement.style.opacity = '0';
notifyElement.style.transform = 'translate(-50%, -130%)';
notifyHideTimer = window.setTimeout(() => {
if (notifyElement) {
notifyElement.style.display = 'none';
}
notifyHideTimer = undefined;
}, 260);
}
function ensureNotifyElement() {
if (notifyElement) {
return;
}
notifyElement = document.createElement('div');
notifyElement.style.cssText = [
'position: fixed',
'left: 50%',
'top: 24px',
'z-index: 100000',
'display: none',
'width: min(560px, calc(100vw - 40px))',
'box-sizing: border-box',
'padding: 18px 54px 18px 24px',
'border-radius: 16px',
'background: rgba(55, 55, 55, 0.88)',
'color: #fff',
'box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22)',
'opacity: 0',
'transform: translate(-50%, -130%)',
'transition: opacity 0.26s ease, transform 0.26s ease',
].join(';');
notifyTitleElement = document.createElement('div');
notifyTitleElement.style.cssText = [
'font-size: 22px',
'font-weight: 700',
'line-height: 1.35',
].join(';');
notifyMessageElement = document.createElement('div');
notifyMessageElement.style.cssText = [
'margin-top: 5px',
'color: rgba(255, 255, 255, 0.88)',
'font-size: 18px',
'line-height: 1.45',
'word-break: break-word',
].join(';');
const closeElement = document.createElement('button');
closeElement.type = 'button';
closeElement.textContent = '×';
closeElement.setAttribute('aria-label', 'Close notification');
closeElement.style.cssText = [
'position: absolute',
'top: 12px',
'right: 14px',
'width: 34px',
'height: 34px',
'padding: 0',
'border: 0',
'background: transparent',
'color: rgba(255, 255, 255, 0.8)',
'font-size: 28px',
'line-height: 32px',
'cursor: pointer',
].join(';');
closeElement.onclick = closeNotify;
notifyElement.append(
notifyTitleElement,
notifyMessageElement,
closeElement,
);
document.body.appendChild(notifyElement);
}
export function $notify(message?: string, title?: string): () => void;
export function $notify(options?: NotifyOptions): () => void;
export function $notify(messageOrOptions?: string | NotifyOptions, title?: string): () => void {
const options: NotifyOptions = typeof messageOrOptions === 'object'
? messageOrOptions
: { message: messageOrOptions, title };
if (!options.message) {
return closeNotify;
}
ensureNotifyElement();
if (notifyTimer) {
window.clearTimeout(notifyTimer);
}
if (notifyHideTimer) {
window.clearTimeout(notifyHideTimer);
notifyHideTimer = undefined;
}
notifyTitleElement!.textContent = options.title ?? '通知';
notifyMessageElement!.textContent = options.message;
notifyElement!.style.display = 'block';
window.requestAnimationFrame(() => {
if (!notifyElement) {
return;
}
notifyElement.style.opacity = '1';
notifyElement.style.transform = 'translate(-50%, 0)';
});
if ((options.duration ?? 4000) > 0) {
notifyTimer = window.setTimeout(closeNotify, options.duration ?? 4000);
}
return closeNotify;
}
type ConfirmOptions = { type ConfirmOptions = {
title?: string; title?: string;
...@@ -324,4 +470,4 @@ export function $confirm(messageOrOptions?: string | ConfirmOptions, title?: str ...@@ -324,4 +470,4 @@ export function $confirm(messageOrOptions?: string | ConfirmOptions, title?: str
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
confirmResolve = resolve; confirmResolve = resolve;
}); });
} }
\ No newline at end of file
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
import { onBeforeUnmount, onMounted } from 'vue' import { onBeforeUnmount, onMounted } from 'vue'
import { $read_socket_storge, $remove_socket_storage, $toast } from '@/commons/utils' import { $read_socket_storge, $remove_socket_storage, $notify } from '@/commons/utils'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws' import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
import router from '@/router' import router from '@/router'
...@@ -13,6 +13,7 @@ type SocketPayload = { ...@@ -13,6 +13,7 @@ type SocketPayload = {
type AdminGameSocketOptions = { type AdminGameSocketOptions = {
gameId?: string, gameId?: string,
loginRedirectPath?: string loginRedirectPath?: string
onConnect?: () => boolean
onRoomState?: (data: any) => void onRoomState?: (data: any) => void
onRoomJoin?: (data: any) => void onRoomJoin?: (data: any) => void
onGameStart?: (data: any) => void | Promise<void> onGameStart?: (data: any) => void | Promise<void>
...@@ -57,8 +58,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -57,8 +58,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
validuser: CryptoJS.AES.encrypt(token, passphrase).toString(), validuser: CryptoJS.AES.encrypt(token, passphrase).toString(),
}, },
onConnect: () => { onConnect: () => {
if (options.onConnect) {
if (!options.onConnect()) {
return;
}
}
window.setTimeout(requestRoomState, 200) window.setTimeout(requestRoomState, 200)
}, },
onDisconnect: (_reason: string) => {
},
onError: (error: any) => {
// console.error('socket.io connect_error:', error)
},
}) })
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => { offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
...@@ -67,18 +79,16 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -67,18 +79,16 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
if (evt == 'login_result') { if (evt == 'login_result') {
// alert(msg) // alert(msg)
// options.onRelogin?.() // options.onRelogin?.()
$toast(msg); // $notify(msg);
$remove_socket_storage(); $remove_socket_storage();
await router.replace(`/login`); await router.replace(`/login`);
location.href = location.href;
return; return;
} }
else if (evt == 'new_admin_result') { else if (evt == 'new_admin_result') {
// alert(msg); // alert(msg);
$toast(msg); // $notify(msg);
$remove_socket_storage(); $remove_socket_storage();
await router.replace(`/login`); await router.replace(`/login`);
location.href = location.href;
return; return;
} }
} }
...@@ -88,13 +98,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -88,13 +98,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
if (data.gameId) { if (data.gameId) {
//如果这两个值不一致说明浏览器有直接更新游戏房间号 //如果这两个值不一致说明浏览器有直接更新游戏房间号
if (data.gameId != options.gameId) { if (data.gameId != options.gameId) {
location.reload(); // location.reload();
return; return;
} }
} }
options.onRoomState?.(data) options.onRoomState?.(data)
return return
} }
case 'player_offline_result': {
// $notify('有玩家掉线')
if (data)
options.onRoomJoin?.(data)
return
}
case 'room_join_result': { case 'room_join_result': {
options.onRoomJoin?.(data) options.onRoomJoin?.(data)
return return
......
<script setup lang="ts"> <script setup lang="ts">
import { cssAssetUrl } from '@/commons/assets'; import { cssAssetUrl } from '@/commons/assets';
import { $save, $toast } from '@/commons/utils.ts'; import { $save, $notify, $toast } from '@/commons/utils.ts';
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
...@@ -26,7 +26,7 @@ async function login() { ...@@ -26,7 +26,7 @@ async function login() {
const data = await res.json() const data = await res.json()
if (!res.ok || data.state !== 1) { if (!res.ok || data.state !== 1) {
$toast(data.msg) // $notify(data.msg)
//alert(data.msg || 'Login failed') //alert(data.msg || 'Login failed')
// throw new Error(data.msg || 'Login failed') // throw new Error(data.msg || 'Login failed')
} }
...@@ -38,7 +38,7 @@ const loginHandler = async () => { ...@@ -38,7 +38,7 @@ const loginHandler = async () => {
if (state === 0) { if (state === 0) {
// alert(msg) // alert(msg)
$toast(msg) // $notify(msg)
return return
} }
for (let i in data) { for (let i in data) {
......
...@@ -41,31 +41,11 @@ const gameHandler = async (index: number) => { ...@@ -41,31 +41,11 @@ const gameHandler = async (index: number) => {
} }
await router.replace(`/game${index + 1}`); await router.replace(`/game${index + 1}`);
} }
const loginSuccessHandler = () => {
show_login.value = false;
gameHandler(item_index.value);
}
const confirmRefresh = (event: BeforeUnloadEvent) => {
event.preventDefault()
event.returnValue = ''
}
useAdminGameSocket({}) useAdminGameSocket({})
onMounted(() => {
window.addEventListener('beforeunload', confirmRefresh)
})
onUnmounted(() => {
window.removeEventListener('beforeunload', confirmRefresh)
})
</script> </script>
<template> <template>
<!-- <Login v-if="show_login" @loginSuccess="loginSuccessHandler" /> -->
<div class="bg"> <div class="bg">
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title1"></div> <div class="img-title1"></div>
......
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame1Music, stopAllMusic, stopGame1Music } from '@/commons/music' import { playApplauseMusic, playGame1Music, stopAllMusic, stopGame1Music } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import router from '@/router'; import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
...@@ -33,6 +32,7 @@ const imageUrls = { ...@@ -33,6 +32,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => { const pressStartButton = () => {
isStartPressed.value = true; isStartPressed.value = true;
...@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => { ...@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div> <div class="img-title1"></div>
<div class="img-title2"></div> <div class="img-title2"></div>
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="img-gu"></div> <div class="img-gu"></div>
<div class="list-container"> <div class="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div> <div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
...@@ -70,7 +73,7 @@ const renderAvatar = (item: any | null) => { ...@@ -70,7 +73,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, 8) }}</div> <div class="txt-nickname">{{ item.nickname }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -138,6 +141,7 @@ const renderAvatar = (item: any | null) => { ...@@ -138,6 +141,7 @@ const renderAvatar = (item: any | null) => {
width: 100%; width: 100%;
height: 144px; height: 144px;
border-radius: 10px; border-radius: 10px;
cursor: pointer;
} }
.txt-qrcode { .txt-qrcode {
...@@ -146,6 +150,27 @@ const renderAvatar = (item: any | null) => { ...@@ -146,6 +150,27 @@ const renderAvatar = (item: any | null) => {
} }
} }
.qrcode-preview-mask {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 150px;
background: rgb(0 0 0 / 45%);
cursor: pointer;
}
.qrcode-preview {
width: 480px;
height: 480px;
padding: 18px;
border-radius: 24px;
background: #fff;
box-shadow: 0 18px 60px rgb(0 0 0 / 35%);
}
.img-gu { .img-gu {
background: v-bind('imageUrls.gu'); background: v-bind('imageUrls.gu');
width: 539px; width: 539px;
...@@ -188,6 +213,10 @@ const renderAvatar = (item: any | null) => { ...@@ -188,6 +213,10 @@ const renderAvatar = (item: any | null) => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -217,9 +246,15 @@ const renderAvatar = (item: any | null) => { ...@@ -217,9 +246,15 @@ const renderAvatar = (item: any | null) => {
} }
.txt-nickname { .txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666; color: #666;
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame2Music, stopAllMusic } from '@/commons/music' import { playApplauseMusic, playGame2Music, stopAllMusic } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import router from '@/router'; import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
...@@ -19,7 +18,7 @@ const imageUrls = { ...@@ -19,7 +18,7 @@ const imageUrls = {
bg: cssAssetUrl('game2/bg.png'), bg: cssAssetUrl('game2/bg.png'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('game2/title.png'), title1: cssAssetUrl('game2/title.png'),
qrcode: assetUrl('game6/qrcode.png'), qrcode: assetUrl('game2/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'), // gu: cssAssetUrl('game1/gu.png'),
// bg: cssAssetUrl('game1/bg.png'), // bg: cssAssetUrl('game1/bg.png'),
...@@ -43,6 +42,7 @@ const imageUrls = { ...@@ -43,6 +42,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => { const pressStartButton = () => {
isStartPressed.value = true; isStartPressed.value = true;
...@@ -68,9 +68,12 @@ const renderAvatar = (item: any | null) => { ...@@ -68,9 +68,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div> <div class="img-title1"></div>
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="list-container"> <div class="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div> <div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
...@@ -80,7 +83,7 @@ const renderAvatar = (item: any | null) => { ...@@ -80,7 +83,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, 8) }}</div> <div class="txt-nickname">{{ item.nickname }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -98,6 +101,9 @@ const renderAvatar = (item: any | null) => { ...@@ -98,6 +101,9 @@ const renderAvatar = (item: any | null) => {
</template> </template>
<style scoped> <style scoped>
.qrcode-preview-mask { position: absolute; inset: 0; z-index: 10; display: flex; justify-content: center; padding-top: 150px; background: rgb(0 0 0 / 45%); cursor: pointer; }
.qrcode-preview { width: 480px; height: 480px; padding: 18px; border-radius: 24px; background: #fff; }
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
...@@ -191,6 +197,10 @@ const renderAvatar = (item: any | null) => { ...@@ -191,6 +197,10 @@ const renderAvatar = (item: any | null) => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -220,9 +230,15 @@ const renderAvatar = (item: any | null) => { ...@@ -220,9 +230,15 @@ const renderAvatar = (item: any | null) => {
} }
.txt-nickname { .txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666; color: #666;
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -20,9 +20,10 @@ const HORSE_SPRITE_COLUMNS = 10; ...@@ -20,9 +20,10 @@ const HORSE_SPRITE_COLUMNS = 10;
const HORSE_FRAME_DURATION = 50; const HORSE_FRAME_DURATION = 50;
const RACE_TICK_DURATION = 50; const RACE_TICK_DURATION = 50;
const RACE_DURATION = 60_000; const RACE_DURATION = 60_000;
const FINISH_POSITION_GAP = 55;
const RANK_RUNNING_GAP = 45; const RANK_RUNNING_GAP = 45;
const RANK_SPEED_FACTORS = [1.04, 1.025, 1.01, 0.995, 0.98, 0.965]; const RANK_SPEED_FACTORS = [1.04, 1.025, 1.01, 0.995, 0.98, 0.965];
const FINISH_RANK_OFFSETS = [0, 72, 94, 181, 207, 302];
const TIE_POSITION_OFFSETS = [48, -76, 96, -28, 67, -108];
const RACE_START_X = -225; const RACE_START_X = -225;
const TRACK_MAX_X = 1595; const TRACK_MAX_X = 1595;
const BACKGROUND_WIDTH = 2358; const BACKGROUND_WIDTH = 2358;
...@@ -33,16 +34,22 @@ const raceElapsed = ref(0); ...@@ -33,16 +34,22 @@ const raceElapsed = ref(0);
const raceStarted = ref(false); const raceStarted = ref(false);
const rankedPlayerKeys = ref<string[]>([]); const rankedPlayerKeys = ref<string[]>([]);
const testRankList = ref<Player[]>([]); const testRankList = ref<Player[]>([]);
let horseFrameTimer: ReturnType<typeof setInterval> | undefined; let raceAnimationFrame: number | undefined;
let raceTimer: ReturnType<typeof setInterval> | undefined;
let raceStartTimer: ReturnType<typeof setTimeout> | undefined; let raceStartTimer: ReturnType<typeof setTimeout> | undefined;
let previousAnimationTime = 0;
let horseFrameElapsed = 0;
type Racer = { type Racer = {
key: string; key: string;
lane: number; lane: number;
horseType: number;
player: Player; player: Player;
x: number; x: number;
speed: number; speed: number;
paceAmplitude: number;
paceFrequency: number;
pacePhase: number;
tieOffset: number;
}; };
const START_COUNTDOWN_SECONDS = 3; const START_COUNTDOWN_SECONDS = 3;
const racers = ref<Racer[]>([]); const racers = ref<Racer[]>([]);
...@@ -80,16 +87,41 @@ const horseSprites = [ ...@@ -80,16 +87,41 @@ const horseSprites = [
cssAssetUrl('game2/horse3.png'), cssAssetUrl('game2/horse3.png'),
]; ];
const preloadHorseSprites = () => {
horseSprites.forEach((sprite) => {
const image = new Image();
image.src = sprite.slice(5, -2);
});
};
const playerKey = (player: Player, index = 0) => ( const playerKey = (player: Player, index = 0) => (
String(player.userid || player.wechat_id || player.telephone || `${player.nickname}-${index}`) String(player.userid || player.wechat_id || player.telephone || `${player.nickname}-${index}`)
); );
const hashKey = (key: string) => Array.from(key).reduce(
(hash, character) => ((hash << 5) - hash + character.charCodeAt(0)) | 0,
0,
);
const createPaceProfile = (key: string) => {
const hash = Math.abs(hashKey(key));
return {
paceAmplitude: 14 + hash % 18,
paceFrequency: 1.5 + (hash % 5) * 0.2,
pacePhase: (hash % 360) * Math.PI / 180,
tieOffset: TIE_POSITION_OFFSETS[hash % TIE_POSITION_OFFSETS.length]!,
};
};
const isRealPlayer = (player: Player) => ( const isRealPlayer = (player: Player) => (
Boolean(player) Boolean(player)
&& !String(player.wechat_id || '').startsWith('empty-') && !String(player.wechat_id || '').startsWith('empty-')
&& player.nickname !== '-' && player.nickname !== '-'
); );
const scoreOf = (player: Player) => Number(player.score || 0);
const renderRaceBackground = () => { const renderRaceBackground = () => {
const progress = Math.min(raceElapsed.value / RACE_DURATION, 1); const progress = Math.min(raceElapsed.value / RACE_DURATION, 1);
const backgroundX = -(BACKGROUND_WIDTH - STAGE_WIDTH) * progress; const backgroundX = -(BACKGROUND_WIDTH - STAGE_WIDTH) * progress;
...@@ -100,28 +132,50 @@ const renderRaceBackground = () => { ...@@ -100,28 +132,50 @@ const renderRaceBackground = () => {
}; };
}; };
const targetXForRank = (rankIndex: number) => { const targetXForRacer = (racer: Racer, rankIndex: number) => {
const safeRankIndex = Math.max(0, Math.min(rankIndex, HORSE_COUNT - 1)); const safeRankIndex = Math.max(0, Math.min(rankIndex, HORSE_COUNT - 1));
const progress = Math.min(raceElapsed.value / RACE_DURATION, 1); const progress = Math.min(raceElapsed.value / RACE_DURATION, 1);
const runningGap = RANK_RUNNING_GAP * Math.sin(Math.PI * progress); const runningGap = RANK_RUNNING_GAP * Math.sin(Math.PI * progress);
const finishGap = FINISH_POSITION_GAP * progress; const finishOffset = FINISH_RANK_OFFSETS[safeRankIndex]! * progress;
const paceFade = Math.sin(Math.PI * progress);
const paceOffset = Math.sin(
racer.pacePhase + progress * Math.PI * 2 * racer.paceFrequency,
) * racer.paceAmplitude * paceFade;
const tiedCount = racers.value.filter(
({ player }) => scoreOf(player) === scoreOf(racer.player),
).length;
const tieOffset = tiedCount > 1 ? racer.tieOffset * progress : 0;
return RACE_START_X return RACE_START_X
+ (TRACK_MAX_X - RACE_START_X) * progress + (TRACK_MAX_X - RACE_START_X) * progress
- safeRankIndex * (runningGap + finishGap); - safeRankIndex * runningGap
- finishOffset
+ paceOffset
+ tieOffset;
}; };
const shuffledLanes = () => { const createRandomLanesByRank = () => {
const lanes = Array.from({ length: HORSE_COUNT }, (_, lane) => lane); // 每项表示从顶部到底部的名次索引,均为明显的前后交替排列。
const ranksByLaneTemplates = [
for (let index = lanes.length - 1; index > 0; index -= 1) { [5, 0, 4, 1, 3, 2],
const randomIndex = Math.floor(Math.random() * (index + 1)); [4, 1, 5, 0, 3, 2],
[lanes[index], lanes[randomIndex]] = [lanes[randomIndex]!, lanes[index]!]; [3, 0, 5, 1, 4, 2],
} [5, 1, 3, 0, 4, 2],
[4, 0, 3, 1, 5, 2],
return lanes; [2, 5, 0, 4, 1, 3],
];
const ranksByLane = ranksByLaneTemplates[
Math.floor(Math.random() * ranksByLaneTemplates.length)
]!;
return Array.from(
{ length: HORSE_COUNT },
(_, rankIndex) => ranksByLane.indexOf(rankIndex),
);
}; };
let lanesByRank = createRandomLanesByRank();
const syncRacers = (rankList: Player[]) => { const syncRacers = (rankList: Player[]) => {
const topPlayers = rankList.filter(isRealPlayer).slice(0, HORSE_COUNT); const topPlayers = rankList.filter(isRealPlayer).slice(0, HORSE_COUNT);
const rankedEntries = topPlayers.map((player, index) => ({ const rankedEntries = topPlayers.map((player, index) => ({
...@@ -144,24 +198,29 @@ const syncRacers = (rankList: Player[]) => { ...@@ -144,24 +198,29 @@ const syncRacers = (rankList: Player[]) => {
{ length: HORSE_COUNT }, { length: HORSE_COUNT },
(_, lane) => lane, (_, lane) => lane,
).filter((lane) => !retained.some((racer) => racer.lane === lane)); ).filter((lane) => !retained.some((racer) => racer.lane === lane));
const freeLanes = racers.value.length === 0 ? shuffledLanes() : availableLanes; const freeLanes = racers.value.length === 0 ? lanesByRank : availableLanes;
const newcomers = rankedEntries const newcomers = rankedEntries
.filter(({ key }) => !retainedKeys.has(key)) .filter(({ key }) => !retainedKeys.has(key))
.map(({ key, player }, index) => ({ .map(({ key, player }, index) => {
key, const previousRacer = previousRacersByLane.get(freeLanes[index]!);
player,
lane: freeLanes[index]!, return {
x: previousRacersByLane.get(freeLanes[index]!)?.x key,
?? RACE_START_X, player,
speed: previousRacersByLane.get(freeLanes[index]!)?.speed ?? 0, lane: freeLanes[index]!,
})); horseType: previousRacer?.horseType ?? freeLanes[index]! % horseSprites.length,
x: previousRacer?.x ?? RACE_START_X,
speed: previousRacer?.speed ?? 0,
...createPaceProfile(key),
};
});
racers.value = [...retained, ...newcomers] racers.value = [...retained, ...newcomers]
.sort((a, b) => a.lane - b.lane); .sort((a, b) => a.lane - b.lane);
rankedPlayerKeys.value = topKeys; rankedPlayerKeys.value = topKeys;
}; };
const updateRacePositions = () => { const updateRacePositions = (tickScale = 1) => {
if (raceElapsed.value >= RACE_DURATION) { if (raceElapsed.value >= RACE_DURATION) {
racers.value.forEach((racer) => { racers.value.forEach((racer) => {
racer.speed = 0; racer.speed = 0;
...@@ -171,29 +230,55 @@ const updateRacePositions = () => { ...@@ -171,29 +230,55 @@ const updateRacePositions = () => {
racers.value.forEach((racer) => { racers.value.forEach((racer) => {
const rankIndex = rankedPlayerKeys.value.indexOf(racer.key); const rankIndex = rankedPlayerKeys.value.indexOf(racer.key);
const effectiveRank = rankIndex >= 0 ? rankIndex : HORSE_COUNT - 1; const racerScore = scoreOf(racer.player);
const targetX = targetXForRank(effectiveRank); const sameScoreRank = rankedPlayerKeys.value.findIndex((key) => {
const rankedRacer = racers.value.find((item) => item.key === key);
return rankedRacer && scoreOf(rankedRacer.player) === racerScore;
});
const effectiveRank = sameScoreRank >= 0
? sameScoreRank
: rankIndex >= 0 ? rankIndex : HORSE_COUNT - 1;
const targetX = targetXForRacer(racer, effectiveRank);
const speedFactor = RANK_SPEED_FACTORS[effectiveRank] ?? RANK_SPEED_FACTORS.at(-1)!; const speedFactor = RANK_SPEED_FACTORS[effectiveRank] ?? RANK_SPEED_FACTORS.at(-1)!;
const desiredSpeed = Math.max((targetX - racer.x) * 0.18 * speedFactor, 0); const desiredSpeed = Math.max((targetX - racer.x) * 0.18 * speedFactor, 0);
const speedBlend = 1 - Math.pow(1 - 0.24, tickScale);
racer.speed += (desiredSpeed - racer.speed) * 0.24; racer.speed += (desiredSpeed - racer.speed) * speedBlend;
racer.x = Math.min(racer.x + Math.max(racer.speed, 0), TRACK_MAX_X); racer.x = Math.min(racer.x + Math.max(racer.speed, 0) * tickScale, TRACK_MAX_X);
}); });
}; };
const animateRace = (time: number) => {
if (!previousAnimationTime) {
previousAnimationTime = time;
}
const delta = Math.min(time - previousAnimationTime, 100);
previousAnimationTime = time;
horseFrameElapsed += delta;
if (horseFrameElapsed >= HORSE_FRAME_DURATION) {
const elapsedFrames = Math.floor(horseFrameElapsed / HORSE_FRAME_DURATION);
horseFrameIndex.value = (horseFrameIndex.value + elapsedFrames) % HORSE_FRAME_COUNT;
horseFrameElapsed %= HORSE_FRAME_DURATION;
}
if (raceStarted.value) {
raceElapsed.value = Math.min(raceElapsed.value + delta, RACE_DURATION);
updateRacePositions(delta / RACE_TICK_DURATION);
}
raceAnimationFrame = window.requestAnimationFrame(animateRace);
};
const resetRace = (startImmediately = true) => { const resetRace = (startImmediately = true) => {
raceElapsed.value = 0; raceElapsed.value = 0;
raceStarted.value = startImmediately; raceStarted.value = startImmediately;
lanesByRank = createRandomLanesByRank();
const lanes = shuffledLanes();
const leaderKey = rankedPlayerKeys.value[0];
const leaderIndex = racers.value.findIndex((racer) => racer.key === leaderKey);
if (leaderIndex >= 0 && lanes[leaderIndex] === 0 && lanes.length > 1) {
[lanes[leaderIndex], lanes[1]] = [lanes[1]!, lanes[leaderIndex]!];
}
racers.value.forEach((racer) => { racers.value.forEach((racer) => {
racer.lane = lanes[racers.value.indexOf(racer)]!; const rankIndex = rankedPlayerKeys.value.indexOf(racer.key);
racer.lane = lanesByRank[Math.max(rankIndex, 0)]!;
racer.x = RACE_START_X; racer.x = RACE_START_X;
racer.speed = 0; racer.speed = 0;
}); });
...@@ -205,9 +290,9 @@ const renderHorseFrame = (racer: Racer) => { ...@@ -205,9 +290,9 @@ const renderHorseFrame = (racer: Racer) => {
const row = Math.floor(horseFrameIndex.value / HORSE_SPRITE_COLUMNS); const row = Math.floor(horseFrameIndex.value / HORSE_SPRITE_COLUMNS);
return { return {
backgroundImage: horseSprites[racer.lane % horseSprites.length], backgroundImage: horseSprites[racer.horseType],
backgroundPosition: `${-column * HORSE_FRAME_SIZE}px ${-row * HORSE_FRAME_SIZE}px`, backgroundPosition: `${-column * HORSE_FRAME_SIZE}px ${-row * HORSE_FRAME_SIZE}px`,
transform: `translateX(${racer.x}px) scale(1.5)`, transform: `translate3d(${racer.x}px, 0, 0) scale(1.5)`,
}; };
}; };
...@@ -247,25 +332,13 @@ const updateTestRank = () => { ...@@ -247,25 +332,13 @@ const updateTestRank = () => {
}; };
onMounted(() => { onMounted(() => {
horseFrameTimer = setInterval(() => { preloadHorseSprites();
horseFrameIndex.value = (horseFrameIndex.value + 1) % HORSE_FRAME_COUNT; raceAnimationFrame = window.requestAnimationFrame(animateRace);
}, HORSE_FRAME_DURATION);
raceTimer = setInterval(() => {
if (!raceStarted.value) return;
raceElapsed.value = Math.min(raceElapsed.value + RACE_TICK_DURATION, RACE_DURATION);
updateRacePositions();
}, RACE_TICK_DURATION);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (horseFrameTimer) { if (raceAnimationFrame !== undefined) {
clearInterval(horseFrameTimer); window.cancelAnimationFrame(raceAnimationFrame);
}
if (raceTimer) {
clearInterval(raceTimer);
} }
if (raceStartTimer) { if (raceStartTimer) {
clearTimeout(raceStartTimer); clearTimeout(raceStartTimer);
...@@ -389,7 +462,9 @@ watch( ...@@ -389,7 +462,9 @@ watch(
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 1500px 1500px; background-size: 1500px 1500px;
transform-origin: left bottom; transform-origin: left bottom;
transition: transform 50ms linear; backface-visibility: hidden;
will-change: transform, background-position;
transform-style: preserve-3d;
.name-container { .name-container {
display: flex; display: flex;
...@@ -398,8 +473,8 @@ watch( ...@@ -398,8 +473,8 @@ watch(
width: 150px; width: 150px;
height: 42px; height: 42px;
position: absolute; position: absolute;
left: -100px; left: -70px;
top: 50px; top: 35px;
.nickname { .nickname {
max-width: 150px; max-width: 150px;
......
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame6Music, stopAllMusic, stopGame6Music } from '@/commons/music' import { playApplauseMusic, playGame6Music, stopAllMusic, stopGame6Music } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import router from '@/router'; import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
...@@ -44,6 +43,7 @@ const imageUrls = { ...@@ -44,6 +43,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => { const pressStartButton = () => {
isStartPressed.value = true; isStartPressed.value = true;
...@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => { ...@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div> <div class="img-title1"></div>
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="img-mole"></div> <div class="img-mole"></div>
<div class="img-rabbit"></div> <div class="img-rabbit"></div>
<div class="list-container"> <div class="list-container">
...@@ -82,7 +85,7 @@ const renderAvatar = (item: any | null) => { ...@@ -82,7 +85,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">{{ item.nickname }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => { ...@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => {
</template> </template>
<style scoped> <style scoped>
.qrcode-preview-mask { position: absolute; inset: 0; z-index: 10; display: flex; justify-content: center; padding-top: 150px; background: rgb(0 0 0 / 45%); cursor: pointer; }
.qrcode-preview { width: 480px; height: 480px; padding: 18px; border-radius: 24px; background: #fff; }
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
...@@ -216,6 +222,10 @@ const renderAvatar = (item: any | null) => { ...@@ -216,6 +222,10 @@ const renderAvatar = (item: any | null) => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -245,9 +255,15 @@ const renderAvatar = (item: any | null) => { ...@@ -245,9 +255,15 @@ const renderAvatar = (item: any | null) => {
} }
.txt-nickname { .txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666; color: #666;
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -8,7 +8,7 @@ import { useAdminGameSocket } from "@/composables/useAdminGameSocket"; ...@@ -8,7 +8,7 @@ import { useAdminGameSocket } from "@/composables/useAdminGameSocket";
import { onSocketMessage } from "@/commons/ws"; import { onSocketMessage } from "@/commons/ws";
import type Player from "@/commons/player.ts"; import type Player from "@/commons/player.ts";
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { $confirm, $remove_socket_storage, $toast } from "@/commons/utils.ts"; import { $confirm, $remove_socket_storage, $notify, $toast } from "@/commons/utils.ts";
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { $format_str } from "@/commons/utils.ts";
import DesignStage from "@/components/DesignStage.vue"; import DesignStage from "@/components/DesignStage.vue";
import { assetUrl, cssAssetUrl } from "@/commons/assets.ts"; import { assetUrl, cssAssetUrl } from "@/commons/assets.ts";
...@@ -34,6 +33,7 @@ const imageUrls = { ...@@ -34,6 +33,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const renderAvatar = (item: any | null) => { const renderAvatar = (item: any | null) => {
if (item && item.avatar) { if (item && item.avatar) {
...@@ -59,9 +59,12 @@ const releaseStartButton = () => { ...@@ -59,9 +59,12 @@ const releaseStartButton = () => {
<div class="img-title1"></div> <div class="img-title1"></div>
<div class="img-title2"></div> <div class="img-title2"></div>
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="img-gift"></div> <div class="img-gift"></div>
<div class="img-small-gu"></div> <div class="img-small-gu"></div>
<div class="img-gu"></div> <div class="img-gu"></div>
...@@ -76,7 +79,7 @@ const releaseStartButton = () => { ...@@ -76,7 +79,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, 8) }} {{ item.nickname }}
</div> </div>
</div> </div>
</div> </div>
...@@ -95,6 +98,9 @@ const releaseStartButton = () => { ...@@ -95,6 +98,9 @@ const releaseStartButton = () => {
</template> </template>
<style scoped> <style scoped>
.qrcode-preview-mask { position: absolute; inset: 0; z-index: 10; display: flex; justify-content: center; padding-top: 150px; background: rgb(0 0 0 / 45%); cursor: pointer; }
.qrcode-preview { width: 480px; height: 480px; padding: 18px; border-radius: 24px; background: #fff; }
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
...@@ -218,6 +224,10 @@ const releaseStartButton = () => { ...@@ -218,6 +224,10 @@ const releaseStartButton = () => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -246,9 +256,15 @@ const releaseStartButton = () => { ...@@ -246,9 +256,15 @@ const releaseStartButton = () => {
} }
.txt-nickname { .txt-nickname {
color: #666; display: block;
font-size: 20px; width: calc(100% - 12px);
text-align: center; margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -22,6 +22,8 @@ const emit = defineEmits<{ ...@@ -22,6 +22,8 @@ const emit = defineEmits<{
const designStage = ref<InstanceType<typeof DesignStage> | null>(null); const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const imageUrls = { const imageUrls = {
bgTop: cssAssetUrl("game4/bg-top.webp"),
bgBottom: cssAssetUrl("game4/bg-bottom.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
titleBg: cssAssetUrl("game4/horse-title-bg.svg"), titleBg: cssAssetUrl("game4/horse-title-bg.svg"),
bigBrum: cssAssetUrl("game4/big-brum.svg"), bigBrum: cssAssetUrl("game4/big-brum.svg"),
...@@ -586,7 +588,7 @@ defineExpose({ ...@@ -586,7 +588,7 @@ defineExpose({
.bg-top-inner { .bg-top-inner {
width: 3840px; width: 3840px;
height: 1080px; height: 1080px;
background: url("@/assets/images/game4/bg-top.webp") 0 0 / 3840px 1080px background: v-bind("imageUrls.bgTop") 0 0 / 3840px 1080px
no-repeat; no-repeat;
flex-shrink: 0; flex-shrink: 0;
} }
...@@ -605,7 +607,7 @@ defineExpose({ ...@@ -605,7 +607,7 @@ defineExpose({
.bg-bottom-inner { .bg-bottom-inner {
width: 3840px; width: 3840px;
height: 577px; height: 577px;
background: url("@/assets/images/game4/bg-bottom.webp") 0 0 / 3840px 577px background: v-bind("imageUrls.bgBottom") 0 0 / 3840px 577px
no-repeat; no-repeat;
flex-shrink: 0; flex-shrink: 0;
} }
......
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame5Music, stopAllMusic, stopGame5Music } from '@/commons/music' import { playApplauseMusic, playGame5Music, stopAllMusic, stopGame5Music } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
...@@ -33,6 +32,7 @@ const imageUrls = { ...@@ -33,6 +32,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => { const pressStartButton = () => {
isStartPressed.value = true; isStartPressed.value = true;
...@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => { ...@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div> <div class="img-title1"></div>
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="img-left"></div> <div class="img-left"></div>
<div class="img-right"></div> <div class="img-right"></div>
<div class="list-container"> <div class="list-container">
...@@ -71,7 +74,7 @@ const renderAvatar = (item: any | null) => { ...@@ -71,7 +74,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, 8) }}</div> <div class="txt-nickname">{{ item.nickname }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -94,6 +97,9 @@ const renderAvatar = (item: any | null) => { ...@@ -94,6 +97,9 @@ const renderAvatar = (item: any | null) => {
</template> </template>
<style scoped> <style scoped>
.qrcode-preview-mask { position: absolute; inset: 0; z-index: 10; display: flex; justify-content: center; padding-top: 150px; background: rgb(0 0 0 / 45%); cursor: pointer; }
.qrcode-preview { width: 480px; height: 480px; padding: 18px; border-radius: 24px; background: #fff; }
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
...@@ -208,6 +214,10 @@ const renderAvatar = (item: any | null) => { ...@@ -208,6 +214,10 @@ const renderAvatar = (item: any | null) => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -237,9 +247,15 @@ const renderAvatar = (item: any | null) => { ...@@ -237,9 +247,15 @@ const renderAvatar = (item: any | null) => {
} }
.txt-nickname { .txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666; color: #666;
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -21,6 +21,7 @@ const gameStarted = ref(false); ...@@ -21,6 +21,7 @@ const gameStarted = ref(false);
const showResult = ref(false); const showResult = ref(false);
const imageUrls = { const imageUrls = {
runway: cssAssetUrl("game5/paodao.webp"),
bg: cssAssetUrl("game5/bg.svg"), bg: cssAssetUrl("game5/bg.svg"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game5/title3.webp"), title: cssAssetUrl("game5/title3.webp"),
...@@ -500,7 +501,7 @@ const rankIcon = (index: number) => { ...@@ -500,7 +501,7 @@ const rankIcon = (index: number) => {
.runway-inner { .runway-inner {
width: 3840px; width: 3840px;
height: 543px; height: 543px;
background: url("@/assets/images/game5/paodao.webp") 0 0 / 3840px 543px background: v-bind("imageUrls.runway") 0 0 / 3840px 543px
no-repeat; no-repeat;
flex-shrink: 0; flex-shrink: 0;
} }
......
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame6Music, stopAllMusic, stopGame6Music } from '@/commons/music' import { playApplauseMusic, playGame6Music, stopAllMusic, stopGame6Music } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import router from '@/router'; import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
...@@ -44,6 +43,7 @@ const imageUrls = { ...@@ -44,6 +43,7 @@ const imageUrls = {
}; };
const isStartPressed = ref(false); const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => { const pressStartButton = () => {
isStartPressed.value = true; isStartPressed.value = true;
...@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => { ...@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div> <div class="img-title1"></div>
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div> <div class="txt-qrcode">微信扫码参与</div>
</div> </div>
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div>
<div class="img-mole"></div> <div class="img-mole"></div>
<div class="img-rabbit"></div> <div class="img-rabbit"></div>
<div class="list-container"> <div class="list-container">
...@@ -82,7 +85,7 @@ const renderAvatar = (item: any | null) => { ...@@ -82,7 +85,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, 8) }}</div> <div class="txt-nickname">{{ item.nickname }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => { ...@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => {
</template> </template>
<style scoped> <style scoped>
.qrcode-preview-mask { position: absolute; inset: 0; z-index: 10; display: flex; justify-content: center; padding-top: 150px; background: rgb(0 0 0 / 45%); cursor: pointer; }
.qrcode-preview { width: 480px; height: 480px; padding: 18px; border-radius: 24px; background: #fff; }
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
...@@ -211,6 +217,10 @@ const renderAvatar = (item: any | null) => { ...@@ -211,6 +217,10 @@ const renderAvatar = (item: any | null) => {
height: 250px; height: 250px;
align-content: center; align-content: center;
>div {
min-width: 0;
}
.img-avatar-container { .img-avatar-container {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
...@@ -240,9 +250,15 @@ const renderAvatar = (item: any | null) => { ...@@ -240,9 +250,15 @@ const renderAvatar = (item: any | null) => {
} }
.txt-nickname { .txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666; color: #666;
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
......
...@@ -2,7 +2,7 @@ import { fileURLToPath, URL } from 'node:url' ...@@ -2,7 +2,7 @@ import { fileURLToPath, URL } from 'node:url'
import { cpSync, existsSync, rmSync } from 'node:fs' import { cpSync, existsSync, rmSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
...@@ -25,34 +25,39 @@ function copyImageAssets() { ...@@ -25,34 +25,39 @@ function copyImageAssets() {
} }
} }
export default defineConfig(({ command }) => ({ export default defineConfig(({ command, mode }) => {
base: '/',//command === 'build' ? '/cc/pc/' : '/', const env = loadEnv(mode, process.cwd(), '')
define: { const useLocalAssets = env.VITE_ASSET_BUILD_LOCAL === 'true'
__APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
// __APP_DOMAIN__: JSON.stringify(buildDomain), return {
}, base: '/',//command === 'build' ? '/cc/pc/' : '/',
plugins: [ define: {
vue(), __APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
vueDevTools(), // __APP_DOMAIN__: JSON.stringify(buildDomain),
copyImageAssets(), },
], plugins: [
server: { vue(),
host: true, vueDevTools(),
proxy: { ...(useLocalAssets || !env.VITE_ASSET_BASE_URL ? [copyImageAssets()] : []),
'/socket.io': { ],
target: 'http://localhost:8082', server: {
changeOrigin: true, host: true,
ws: true, proxy: {
}, '/socket.io': {
'/manager': { target: 'http://localhost:8082',
target: 'http://localhost:8082', changeOrigin: true,
changeOrigin: true, ws: true,
},
'/manager': {
target: 'http://localhost:8082',
changeOrigin: true,
},
}, },
}, },
}, resolve: {
resolve: { alias: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), },
}, },
}, }
})) })
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