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

fix: 页面修改

parent 7179132e
......@@ -10,7 +10,7 @@ use serde_json::{Value, json};
use socketioxide::SocketIo;
use socketioxide::extract::{Data, SocketRef};
use socketioxide::socket::DisconnectReason;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
......@@ -436,6 +436,25 @@ fn broadcast_room_rank(ranking: Vec<Value>) {
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) {
if userid.is_empty() {
return;
......@@ -454,7 +473,7 @@ fn is_current_player_socket(userid: &str, socket: &SocketRef) -> 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 ranking = RoomState::ranking_payload(
&full_ranking
......@@ -468,13 +487,15 @@ fn push_room_score_ranking() -> bool {
.enumerate()
.map(|(index, player)| (player.wechat, json!(index + 1)))
.collect::<Vec<_>>();
let current_player_ids = state.players.keys().cloned().collect::<HashSet<_>>();
(ranking, player_ranks)
(ranking, player_ranks, current_player_ids)
} else {
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 {
emit_msgpack_to_user(&userid, "submit_score_result", &ok_resp(Some(rank)));
}
......@@ -584,6 +605,16 @@ async fn handle_room_command(
) {
let evt = &format!("{cmd}_result");
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_back返回当前游戏的Loading状态
// 只有管理员可以创建房间
......@@ -657,13 +688,6 @@ async fn handle_room_command(
}
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;
} else if matches!(state.status, RoomStatus::Running) {
//如果是正在游戏中
......@@ -683,35 +707,15 @@ async fn handle_room_command(
emit_msgpack(&socket, "room_recover_result", &ok_resp(reconnect_payload));
return;
} else {
emit_msgpack(
&socket,
evt,
&err_resp(
&format!(
"游戏 - {} 已经开始,您暂时无法进入",
state.get_game_tilte(None)
),
Some(json!(gameId)),
),
);
let ranking = RoomState::ranking_payload(&state.score_ranking());
drop(state);
emit_room_rank(socket, ranking);
return;
}
} else if matches!(state.status, RoomStatus::Submit | RoomStatus::Ended) {
//如果是在提交或游戏结束阶段断线重连
// if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(userid) {
// player.0 = 1;
// }
emit_msgpack(
&socket,
evt,
&err_resp(
&format!(
"游戏 - {} 正在结算,您暂时无法进入",
state.get_game_tilte(None)
),
Some(json!(gameId)),
),
);
let ranking = RoomState::ranking_payload(&state.score_ranking());
drop(state);
emit_room_rank(socket, ranking);
return;
}
let entry_order = state.next_entry_order;
......@@ -930,17 +934,20 @@ async fn handle_room_command(
return;
}
let mut status = "".to_string();
let mut title = "".to_string();
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();
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));
}
// 获取房间状态
......@@ -982,7 +989,6 @@ async fn handle_room_command(
}
pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value>) {
let is_create_user = false;
let userid = data
.get("userid")
.and_then(|v| v.as_str())
......@@ -1093,10 +1099,8 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
socket.on_disconnect({
move |socket: SocketRef, _reason: DisconnectReason| async move {
if !is_create_user {
if is_admin {
let Some(admin_session_version) =
clear_current_admin_if_match(&userid, &socket)
let Some(admin_session_version) = clear_current_admin_if_match(&userid, &socket)
else {
return;
};
......@@ -1109,21 +1113,22 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
spawn_deactivate_players_if_admin_absent(admin_session_version);
}
} else {
let should_remove = CLIENT_PLAYER_MAP
if !CLIENT_PLAYER_MAP
.get(&userid)
.map(|player| player.1.id == socket.id)
.unwrap_or(false);
if !should_remove {
.unwrap_or(false)
{
return;
}
CLIENT_PLAYER_MAP.remove(&userid);
if let Ok(mut state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Loading {
if let Some(player) = state.players.get_mut(&userid) {
player.online = false;
}
return;
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;
}
......@@ -1135,6 +1140,19 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
&ok_resp(Some(json!({"list":ranking,"count":count}))),
);
}
RoomStatus::Running | RoomStatus::Submit | RoomStatus::Ended => {
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));
}
}
}
}
}
......
......@@ -201,7 +201,9 @@
this.message = msg;
},
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",
auth: args.auth,
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
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
Put local development images in `src/assets/images`.
......@@ -40,14 +53,14 @@ Default behavior:
For OSS/CDN production builds, create `.env.production`:
```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:
```env
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;
interface ImportMetaEnv {
readonly VITE_ASSET_BASE_URL?: string;
readonly VITE_ASSET_BUILD_LOCAL?: string;
readonly VITE_ASSET_DEV_LOCAL?: string;
}
......
......@@ -2,8 +2,8 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>游戏-H5</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>互动游戏-H5</title>
<style>body{background: #FA6047;}</style>
<script>localStorage.setItem('domain', '');</script>
</head>
......
......@@ -5,8 +5,10 @@
"type": "module",
"scripts": {
"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",
"serve:local": "vite preview --host 0.0.0.0",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
......
......@@ -43,6 +43,8 @@ body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
button,
......
......@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
}
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 ?? (
import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false'
);
......@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
return localAssetUrl(normalizedPath);
}
const baseUrl = options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL;
if (baseUrl) {
return joinUrl(baseUrl, normalizedPath);
if (configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
}
return localAssetUrl(normalizedPath);
......
import game1MusicUrl from '@/assets/images/game1.mp3'
import game6Music1Url from '@/assets/images/game6_1.mp3'
import game6Music2Url from '@/assets/images/game6_2.mp3'
import go321 from '@/assets/images/321go.mp3'
import { assetUrl } from '@/commons/assets'
const game1MusicUrl = assetUrl('game1.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 playingEffects = new Set<HTMLAudioElement>()
const exclusiveEffects = new Map<string, HTMLAudioElement>()
let pendingPlay = false
let pendingRetry: (() => void) | null = null
export function stopAllMusic() {
stopGame1Music();
stopGame2Music();
stopGame3Music();
// stopGame4Music();
// stopGame5Music();
stopGame6Music();
playingEffects.clear();
}
function getEffectTemplate(music: string) {
let template = effectTemplates.get(music)
......@@ -48,13 +67,25 @@ function setPendingRetry(retry: () => void) {
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
audio.loop = false
audio.volume = 1
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('error', release, { once: true })
......@@ -64,7 +95,7 @@ async function playEffect(music: string, retry: () => void) {
clearPendingRetry()
}
} catch {
releaseEffect(audio)
release()
if (!pendingPlay) {
setPendingRetry(retry)
......@@ -78,6 +109,7 @@ function stopEffects() {
releaseEffect(audio)
}
playingEffects.clear()
exclusiveEffects.clear()
}
export async function playGame1Music() {
......@@ -90,6 +122,7 @@ export function stopGame1Music() {
stopEffects()
}
/* 3 2 1 go */
export async function playCountdownMusic() {
await playEffect(go321, playCountdownMusic)
}
......@@ -100,6 +133,35 @@ export function stopCountdownMusic() {
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
......
......@@ -83,7 +83,7 @@ export function $toast(message?: string, duration = 2000) {
'border-radius: 18px',
'background: rgba(0, 0, 0, 0.72)',
'color: #fff',
'font-size: 22px',
'font-size: 16px',
'line-height: 1.4',
'text-align: center',
'transform: translate(-50%, -40%)',
......@@ -306,6 +306,25 @@ function $query<T extends string | string[]>(q: T): 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 {
const q = $query(['token', 'nickname', 'avatar']);
if (q && q.length == 3) {
......@@ -316,7 +335,6 @@ export function $getWechat(): any | null {
avatar: q[2],
};
}
// 调试模式:无真实微信参数时使用模拟数据
if (__DEBUG__) {
const mockToken = 'debug_mock_token_' + Date.now()
......
......@@ -57,6 +57,7 @@ export function initSocket(args: SocketInitArgs): Socket | null {
upgrade: false,
forceNew: true,
timeout: 10000,
// autoConnect: false,
});
socket.on('connect', () => {
......
......@@ -14,6 +14,7 @@ type GameSocketOptions = {
gameId: string,
auth: any,
onConnect?: () => void,
onShowRank?: (data: any) => void,
onGameStart?: (data: any) => void
onScoreSubmitted?: (is_save: boolean, data: any) => void
onRescoreSubmitted?: (recount: number) => void
......@@ -64,7 +65,10 @@ export function useGameSocket(options: GameSocketOptions) {
onConnect: (socket) => {
options.onConnect?.();
},
onDisconnect: (_reason: string) => { },
onDisconnect: (_reason: string) => {
// $toast('网络中断,尝试重新连接...')
socket = null;
},
onError: (error: any) => {
// console.error('socket.io connect_error:', error)
},
......@@ -74,13 +78,18 @@ export function useGameSocket(options: GameSocketOptions) {
const { msg, state, data } = payload
if (state === 0) {
if (evt == 'room_rank_result') {
// options.show_rank?.(data);
// return;
}
if (evt == 'connect_result') {
router.replace('/loading').then(() => { });
// $toast(msg);
// router.replace('/loading').then(() => { });
return;
}
if (evt === 'room_join_result') {
$toast(msg);
router.replace('/loading').then(() => { });
// $toast(msg);
// router.replace('/loading').then(() => { });
return;
}
if (evt == 'submit_score_save') {
......@@ -106,25 +115,25 @@ export function useGameSocket(options: GameSocketOptions) {
switch (evt) {
case 'room_create_result': {
if (`game${data}` == options.gameId) {
// location.reload();
options.onConnect?.();
// debugger
return;
}
const nextPath = getGameHomePath(data)
if (nextPath && router.currentRoute.value.path !== nextPath) {
router.replace(nextPath)
}
break
// if (`game${data}` == options.gameId) {
// // location.reload();
// options.onConnect?.();
// // debugger
// return;
// }
// const nextPath = getGameHomePath(data)
// if (nextPath && router.currentRoute.value.path !== nextPath) {
// router.replace(nextPath)
// }
// break
}
case 'room_join_result': {
if (data != options.gameId) {
debugger
// debugger
return;
}
$toast('您已进入游戏房间')
$toast('您已进入当前游戏房间')
userJoinStatus.value = true
break
}
......@@ -162,6 +171,11 @@ export function useGameSocket(options: GameSocketOptions) {
$toast('管理员关闭了游戏房间')
break;
}
case 'room_rank_result': {
//如果是游戏开始中再进入会收到这个排行榜消息
options.onShowRank?.(data);
return;
}
default: {
debugger
}
......
......@@ -37,7 +37,7 @@ if (wechat) {
_offSocketMessage.value = offSocketMessage
}
onMounted(() => {
document.title = '互动游戏'
document.title = '互动游戏 - 等待中...'
})
onBeforeUnmount(() => {
......
......@@ -2,12 +2,12 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
import { $getWechat, $toast } from '@/commons/utils.ts'
import { playGame1Music } from '@/commons/music'
import { playGame1Music, stopAllMusic } from '@/commons/music'
type GameView = 'loading' | 'playing' | 'score'
......@@ -131,6 +131,7 @@ function startGameView() {
}
function showScoreView() {
showGameRule.value = false;
stopGameCountdown()
setDivDescVisible(false)
currentView.value = 'score'
......@@ -178,6 +179,10 @@ if (wechat) {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
onShowRank: (data) => {
//第一个游戏没有排行榜,只显示结果
showScoreView();
},
onGameStart: async () => {
showGameRule.value = false;
await mobileStageRef.value?.startCountdown()
......@@ -216,10 +221,6 @@ if (wechat) {
$toast('管理员关闭了游戏房间')
},
onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
})
_offSocketMessage.value = offSocketMessage
......@@ -238,6 +239,7 @@ onBeforeUnmount(() => {
window.clearTimeout(guFrameTimer)
}
stopGameCountdown()
stopAllMusic();
if (token.value) {
window.removeEventListener('beforeunload', confirmRefresh)
}
......
......@@ -34,7 +34,8 @@ onMounted(() => {
<div>击鼓积分</div>
<div>{{ tick }}</div>
<div>最终排名</div>
<div><label>{{ rank }}</label></div>
<div v-if="rank>0"><label>{{ rank }}</label></div>
<div v-else>未上榜</div>
</div>
</div>
</div>
......
......@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
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 RankPlayer = {
......@@ -22,6 +22,7 @@ const imageUrls: Record<string, string> = {
close: cssAssetUrl('game1/close.png'),
clock: cssAssetUrl('game1/clock.png'),
bg2: cssAssetUrl('game2/bg2.png'),
point: cssAssetUrl('game2/point.png'),
rule: cssAssetUrl('game1/rule.png'),
coin: cssAssetUrl('game2/coin.png'),
process: cssAssetUrl('game2/process.png'),
......@@ -166,6 +167,7 @@ function showGameOverRank() {
const touchHandler = (currentScore: number) => {
score.value = currentScore
playGame2Music();
submitScore(false, currentScore)
}
......@@ -181,6 +183,12 @@ if (wechat) {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
onShowRank: (data) => {
//第一个游戏没有排行榜,只显示结果
showScoreView();
rankList.value = data;
showGameRank.value = true;
},
onGameStart: async () => {
showGameRule.value = false;
await mobileStageRef.value?.startCountdown()
......@@ -219,7 +227,7 @@ if (wechat) {
rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0
showGameRule.value = false
showGameRank.value = false
// showGameRank.value = false
currentView.value = 'playing'
startGameCountdown(
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0,
......@@ -232,19 +240,21 @@ if (wechat) {
$toast('管理员关闭了游戏房间')
},
onRoomBack: () => {
console.log('onRoomBack');
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
})
_offSocketMessage.value = offSocketMessage
}
function showScoreView() {
showGameRule.value = false;
stopGameCountdown()
setDivDescVisible(false)
currentView.value = 'loading'
}
onMounted(() => {
// currentView.value = 'playing'
document.title = '互动游戏 - 福运当头'
document.title = '互动游戏 - 聚宝接福'
if (token.value) {
window.addEventListener('beforeunload', confirmRefresh)
}
......@@ -252,7 +262,7 @@ onMounted(() => {
onBeforeUnmount(() => {
_offSocketMessage.value?.()
stopGame6Music()
stopAllMusic()
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
}
......
......@@ -18,14 +18,14 @@ onMounted(() => {
<template>
<div class="h5-page game-stage">
<div class="game-desc" @click="$emit('touchGameRule')">游戏规则</div>
<div class="img-logo"></div>
<div class="img-coin"></div>
<!-- <div class="img-logo"></div> -->
<!-- <div class="img-coin"></div> -->
<div class="txt-bottom" v-if="userJoinStatus">您已成功加入游戏<br />等待主持人开始</div>
</div>
</template>
<style scoped>
.img-coin {
/* .img-coin {
background: v-bind('imageUrls.coin') center / cover no-repeat;
width: 744px;
height: 756px;
......@@ -34,16 +34,18 @@ onMounted(() => {
top: 50%;
transform: translate(-50%, -50%) scale(0.6);
transform-origin: center;
}
} */
.txt-bottom {
position: absolute;
bottom: 130px;
bottom: 230px;
width: 100%;
line-height: 40px;
color: white;
font-size: 28px;
text-align: center;
-webkit-text-stroke: 1px #E61903;
paint-order: stroke fill;
animation: loading-text-scale 1.2s ease-in-out infinite;
}
......
......@@ -128,8 +128,8 @@ onBeforeUnmount(stopInertia)
<span class="desc-time">{{ countdownInterval }}</span>
</div>
</Transition>
<div class="img-logo"></div>
<div class="img-hydt"></div>
<!-- <div class="img-logo"></div>
<div class="img-hydt"></div> -->
<div class="rank-content">
<div class="txt-left">
<div>排名</div>
......@@ -140,6 +140,7 @@ onBeforeUnmount(stopInertia)
<div class="txt-bold">{{ tick }}</div>
</div>
</div>
<div class="img-pointer"></div>
<div ref="coinRef" class="img-coin" :style="{ '--coin-rotation': `${coinRotation}deg` }"
@pointerdown="startRotation" @pointermove="rotateCoin" @pointerup="stopRotation"
@pointercancel="stopRotation"></div>
......@@ -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 {
position: absolute;
......@@ -239,13 +253,13 @@ onBeforeUnmount(stopInertia)
position: absolute;
left: 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;
touch-action: none;
user-select: none;
cursor: grab;
will-change: transform;
margin-top:60px;
margin-top: 70px;
}
.img-coin:active {
......
......@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
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 RankPlayer = {
......@@ -101,12 +101,7 @@ function stopGameCountdown() {
}
function submitScore(save: boolean = false, currentTick = score.value) {
if (!wechat && !isLocalMode) return
if (save) {
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 提交最终分数:', currentTick)
return
}
sendGameMessage('submit_score_save', {
score: currentTick,
wechat: wechat.token_origin,
......@@ -116,10 +111,6 @@ function submitScore(save: boolean = false, currentTick = score.value) {
rank: rank.value,
})
} else {
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 实时分数:', currentTick)
return
}
sendGameMessage('submit_score', { score: currentTick })
}
}
......@@ -179,28 +170,17 @@ function startGameView() {
function showGameOverRank() {
stopGameCountdown()
setDivDescVisible(false)
// 保持在 playing 视图,结果页由 PlayingView 内部渲染
isGameOver.value = true
userJoinStatus.value = false
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 游戏结束,最终分数:', score.value)
}
currentView.value = 'loading'
showGameRank.value = true
userJoinStatus.value = false;
}
const touchHandler = (mole: boolean) => {
let scoreDelta = mole ? 5 : -3;
let nextTick = score.value + scoreDelta
if (nextTick < 0) nextTick = 0
score.value = nextTick
if (mole) {
playGame6Music(1);
const scoreChangeHandler = (delta: number) => {
if (delta > 0) {
playGame3Music(1);
} else {
playGame6Music(2);
playGame3Music(2);
}
submitScore(false, nextTick)
}
const scoreChangeHandler = (delta: number) => {
let nextTick = score.value + delta
if (nextTick < 0) nextTick = 0
score.value = nextTick
......@@ -268,35 +248,24 @@ if (wechat) {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
$toast('管理员关闭了房间')
$toast('管理员关闭了游戏房间')
},
onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
})
_offSocketMessage.value = offSocketMessage
}
onMounted(() => {
document.title = '小游戏 - 福运当头'
document.title = '互动游戏 - 马上有福'
if (token.value) {
window.addEventListener('beforeunload', confirmRefresh)
}
// // TODO_LOCAL: 本地模拟游戏开始,稍后部署上线前删除此段
// if ($is_run_local()) {
// showGameRule.value = false
// currentView.value = 'playing'
// startGameCountdown()
// }
})
onBeforeUnmount(() => {
_offSocketMessage.value?.()
stopGame6Music()
stopAllMusic()
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
}
......@@ -324,7 +293,7 @@ const rankCloseHandler = () => {
</script>
<template>
<MobileStage v-if="token || isLocalMode" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef"
<MobileStage v-if="token" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef"
:background="`${renderBG()}`">
<template #gameRule>
<div class="rule-container">
......@@ -373,16 +342,21 @@ const rankCloseHandler = () => {
<div class="col-3">{{ rankScore }}</div>
</div>
</div>
</div>
<div class="img-close" @click="rankCloseHandler"></div>
</div>
</template>
<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" @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"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" />
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</MobileStage>
<div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;">
请使用微信扫码进入游戏
......@@ -510,9 +484,10 @@ const rankCloseHandler = () => {
.img-close {
width: 57px;
height: 57px;
border-radius: 29px;
background: v-bind('imageUrls.close')center/cover;
transform: scale(0.7);
margin-top: 500px;
background: v-bind('imageUrls.close') center / cover no-repeat;
transform: translateX(-50%) scale(1.4);
position: absolute;
top: 0;
left: 100%;
}
</style>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, nextTick, computed } from 'vue';
import { assetUrl } from '@/commons/assets.ts'
import { playGame6Music } from '@/commons/music'
// import { playGame6Music } from '@/commons/music'
const props = defineProps<{
imageUrls: Record<string, string>
......@@ -9,16 +9,10 @@ const props = defineProps<{
isDivDescVisible: boolean
tick: 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<{
touch: [mole: boolean]
// touch: [mole: boolean]
scoreChange: [delta: number]
rankClose: []
}>();
......@@ -275,10 +269,8 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
if (isCoin) {
emit('scoreChange', 5);
playGame6Music(1);
} else {
emit('scoreChange', -3);
playGame6Music(2);
}
const animId = Date.now() + Math.random();
......@@ -431,13 +423,13 @@ watch(() => props.isDivDescVisible, (visible) => {
}, { immediate: true });
// 游戏结束:暂停全部动画,显示结果页
watch(() => props.isGameOver, (over) => {
if (over) {
stopRoadScroll();
stopMainLoop();
stopItemGen();
}
});
// watch(() => props.isGameOver, (over) => {
// if (over) {
// stopRoadScroll();
// stopMainLoop();
// stopItemGen();
// }
// });
</script>
<template>
......@@ -507,7 +499,7 @@ watch(() => props.isGameOver, (over) => {
<div class="img-right" @click="switchToRight"></div>
<!-- 游戏结束结果页覆盖层 -->
<Transition name="rank-fade">
<!-- <Transition name="rank-fade">
<div v-if="isGameOver" class="rank-overlay">
<div class="ranklist-container">
<div class="txt-title">排行</div>
......@@ -541,7 +533,7 @@ watch(() => props.isGameOver, (over) => {
</div>
<div class="img-rank-close" @click="$emit('rankClose')"></div>
</div>
</Transition>
</Transition> -->
</div>
</template>
......
......@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
......@@ -159,15 +159,10 @@ function replayGame() {
}
function backToWaiting() {
resetToLoadingView()
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}
function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}
const touchHandler = () => {
......@@ -188,7 +183,7 @@ if (wechat) {
},
onConnect: () => {
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
onGameStart: async () => {
......
<script setup lang="ts">
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<{
imageUrls: Record<string, string>
......
......@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
......@@ -192,10 +192,7 @@ function showGameOverRank() {
function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
}
const hitHandler = (score: number) => {
......
......@@ -2,11 +2,11 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
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 PlayingView from './views/PlayingView.vue'
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 RankPlayer = {
......@@ -247,10 +247,6 @@ if (wechat) {
$toast('管理员关闭了游戏房间')
},
onRoomBack: () => {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
},
})
_offSocketMessage.value = offSocketMessage
......@@ -265,7 +261,7 @@ onMounted(() => {
onBeforeUnmount(() => {
_offSocketMessage.value?.()
stopGame6Music()
stopAllMusic()
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
}
......@@ -472,12 +468,6 @@ const rankCloseHandler = () => {
}
.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;
height: 57px;
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'
import { cpSync, existsSync, rmSync } from 'node:fs'
import { resolve } from 'node:path'
import basicSsl from '@vitejs/plugin-basic-ssl'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
......@@ -25,7 +25,11 @@ function copyImageAssets() {
}
}
export default defineConfig(({ command }) => ({
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const useLocalAssets = env.VITE_ASSET_BUILD_LOCAL === 'true'
return {
base: command === 'build' ? '/h5/' : '/',
define: {
__APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
......@@ -35,7 +39,7 @@ export default defineConfig(({ command }) => ({
vue(),
vueDevTools(),
basicSsl(),
copyImageAssets(),
...(useLocalAssets || !env.VITE_ASSET_BASE_URL ? [copyImageAssets()] : []),
],
server: {
host: true,
......@@ -56,4 +60,5 @@ export default defineConfig(({ command }) => ({
'@': 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
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
Put local development images in `src/assets/images`.
......
......@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"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",
"build-only": "vite build",
"type-check": "vue-tsc --build"
......
......@@ -31,6 +31,14 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
}
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 ?? (
import.meta.env.DEV && import.meta.env.VITE_ASSET_DEV_LOCAL !== 'false'
);
......@@ -39,10 +47,8 @@ export function assetUrl(path: string, options: AssetUrlOptions = {}): string {
return localAssetUrl(normalizedPath);
}
const baseUrl = options.baseUrl ?? import.meta.env.VITE_ASSET_BASE_URL;
if (baseUrl) {
return joinUrl(baseUrl, normalizedPath);
if (configuredBaseUrl) {
return joinUrl(configuredBaseUrl, normalizedPath);
}
return localAssetUrl(normalizedPath);
......
import game1MusicUrl from '@/assets/images/game1.mp3'
import game2MusicUrl from '@/assets/images/game2.mp3'
import game4MusicUrl from '@/assets/images/cmyf-bgm.mp3'
import game5MusicUrl from '@/assets/images/qcnf-bgm.mp3'
import game6MusicUrl from '@/assets/images/game6.mp3'
import go321 from '@/assets/images/321go.mp3'
import applause from '@/assets/images/applause.mp3'
import { assetUrl } from '@/commons/assets'
const game1MusicUrl = assetUrl('game1.mp3')
const game2MusicUrl = assetUrl('game2.mp3')
const game4MusicUrl = assetUrl('cmyf-bgm.mp3')
const game5MusicUrl = assetUrl('qcnf-bgm.mp3')
const game6MusicUrl = assetUrl('game6.mp3')
const go321 = assetUrl('321go.mp3')
const applause = assetUrl('applause.mp3')
let backgroundMusic: HTMLAudioElement | null = null
let countdownTemplate: HTMLAudioElement | null = null
......
......@@ -147,6 +147,152 @@ export function $toast(message?: string, duration = 2000) {
}, 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 = {
title?: string;
......
import CryptoJS from 'crypto-js'
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 router from '@/router'
......@@ -13,6 +13,7 @@ type SocketPayload = {
type AdminGameSocketOptions = {
gameId?: string,
loginRedirectPath?: string
onConnect?: () => boolean
onRoomState?: (data: any) => void
onRoomJoin?: (data: any) => void
onGameStart?: (data: any) => void | Promise<void>
......@@ -57,8 +58,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
validuser: CryptoJS.AES.encrypt(token, passphrase).toString(),
},
onConnect: () => {
if (options.onConnect) {
if (!options.onConnect()) {
return;
}
}
window.setTimeout(requestRoomState, 200)
},
onDisconnect: (_reason: string) => {
},
onError: (error: any) => {
// console.error('socket.io connect_error:', error)
},
})
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
......@@ -67,18 +79,16 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
if (evt == 'login_result') {
// alert(msg)
// options.onRelogin?.()
$toast(msg);
// $notify(msg);
$remove_socket_storage();
await router.replace(`/login`);
location.href = location.href;
return;
}
else if (evt == 'new_admin_result') {
// alert(msg);
$toast(msg);
// $notify(msg);
$remove_socket_storage();
await router.replace(`/login`);
location.href = location.href;
return;
}
}
......@@ -88,13 +98,19 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
if (data.gameId) {
//如果这两个值不一致说明浏览器有直接更新游戏房间号
if (data.gameId != options.gameId) {
location.reload();
// location.reload();
return;
}
}
options.onRoomState?.(data)
return
}
case 'player_offline_result': {
// $notify('有玩家掉线')
if (data)
options.onRoomJoin?.(data)
return
}
case 'room_join_result': {
options.onRoomJoin?.(data)
return
......
<script setup lang="ts">
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 { useRouter } from 'vue-router'
......@@ -26,7 +26,7 @@ async function login() {
const data = await res.json()
if (!res.ok || data.state !== 1) {
$toast(data.msg)
// $notify(data.msg)
//alert(data.msg || 'Login failed')
// throw new Error(data.msg || 'Login failed')
}
......@@ -38,7 +38,7 @@ const loginHandler = async () => {
if (state === 0) {
// alert(msg)
$toast(msg)
// $notify(msg)
return
}
for (let i in data) {
......
......@@ -41,31 +41,11 @@ const gameHandler = async (index: number) => {
}
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({})
onMounted(() => {
window.addEventListener('beforeunload', confirmRefresh)
})
onUnmounted(() => {
window.removeEventListener('beforeunload', confirmRefresh)
})
</script>
<template>
<!-- <Login v-if="show_login" @loginSuccess="loginSuccessHandler" /> -->
<div class="bg">
<div class="img-logo"></div>
<div class="img-title1"></div>
......
......@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
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()
......
<script setup lang="ts">
import { ref } from 'vue'
import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
......@@ -33,6 +32,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
......@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div>
<div class="img-title2"></div>
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
......@@ -70,7 +73,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
......@@ -138,6 +141,7 @@ const renderAvatar = (item: any | null) => {
width: 100%;
height: 144px;
border-radius: 10px;
cursor: pointer;
}
.txt-qrcode {
......@@ -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 {
background: v-bind('imageUrls.gu');
width: 539px;
......@@ -188,6 +213,10 @@ const renderAvatar = (item: any | null) => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -217,9 +246,15 @@ const renderAvatar = (item: any | null) => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
......
......@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
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()
......
<script setup lang="ts">
import { ref } from 'vue'
import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
......@@ -19,7 +18,7 @@ const imageUrls = {
bg: cssAssetUrl('game2/bg.png'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('game2/title.png'),
qrcode: assetUrl('game6/qrcode.png'),
qrcode: assetUrl('game2/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'),
// bg: cssAssetUrl('game1/bg.png'),
......@@ -43,6 +42,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
......@@ -68,9 +68,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div>
<!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
......@@ -80,7 +83,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
......@@ -98,6 +101,9 @@ const renderAvatar = (item: any | null) => {
</template>
<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 {
width: 1920px;
height: 1080px;
......@@ -191,6 +197,10 @@ const renderAvatar = (item: any | null) => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -220,9 +230,15 @@ const renderAvatar = (item: any | null) => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
......
......@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
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()
......
<script setup lang="ts">
import { ref } from 'vue'
import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
......@@ -44,6 +43,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
......@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div>
<!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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-rabbit"></div>
<div class="list-container">
......@@ -82,7 +85,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
......@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => {
</template>
<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 {
width: 1920px;
height: 1080px;
......@@ -216,6 +222,10 @@ const renderAvatar = (item: any | null) => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -245,9 +255,15 @@ const renderAvatar = (item: any | null) => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
......
......@@ -8,7 +8,7 @@ import { useAdminGameSocket } from "@/composables/useAdminGameSocket";
import { onSocketMessage } from "@/commons/ws";
import type Player from "@/commons/player.ts";
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()
......
<script setup lang="ts">
import { ref } from "vue";
import { $format_str } from "@/commons/utils.ts";
import DesignStage from "@/components/DesignStage.vue";
import { assetUrl, cssAssetUrl } from "@/commons/assets.ts";
......@@ -34,6 +33,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
......@@ -59,9 +59,12 @@ const releaseStartButton = () => {
<div class="img-title1"></div>
<div class="img-title2"></div>
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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-small-gu"></div>
<div class="img-gu"></div>
......@@ -76,7 +79,7 @@ const releaseStartButton = () => {
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">
{{ $format_str(item.nickname, 8) }}
{{ item.nickname }}
</div>
</div>
</div>
......@@ -95,6 +98,9 @@ const releaseStartButton = () => {
</template>
<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 {
width: 1920px;
height: 1080px;
......@@ -218,6 +224,10 @@ const releaseStartButton = () => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -246,9 +256,15 @@ const releaseStartButton = () => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
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<{
const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const imageUrls = {
bgTop: cssAssetUrl("game4/bg-top.webp"),
bgBottom: cssAssetUrl("game4/bg-bottom.webp"),
logo: cssAssetUrl("game1/logo.png"),
titleBg: cssAssetUrl("game4/horse-title-bg.svg"),
bigBrum: cssAssetUrl("game4/big-brum.svg"),
......@@ -586,7 +588,7 @@ defineExpose({
.bg-top-inner {
width: 3840px;
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;
flex-shrink: 0;
}
......@@ -605,7 +607,7 @@ defineExpose({
.bg-bottom-inner {
width: 3840px;
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;
flex-shrink: 0;
}
......
......@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
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()
......
<script setup lang="ts">
import { ref } from 'vue'
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
......@@ -33,6 +32,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
......@@ -58,9 +58,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div>
<!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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-right"></div>
<div class="list-container">
......@@ -71,7 +74,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
......@@ -94,6 +97,9 @@ const renderAvatar = (item: any | null) => {
</template>
<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 {
width: 1920px;
height: 1080px;
......@@ -208,6 +214,10 @@ const renderAvatar = (item: any | null) => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -237,9 +247,15 @@ const renderAvatar = (item: any | null) => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
......
......@@ -21,6 +21,7 @@ const gameStarted = ref(false);
const showResult = ref(false);
const imageUrls = {
runway: cssAssetUrl("game5/paodao.webp"),
bg: cssAssetUrl("game5/bg.svg"),
logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game5/title3.webp"),
......@@ -500,7 +501,7 @@ const rankIcon = (index: number) => {
.runway-inner {
width: 3840px;
height: 543px;
background: url("@/assets/images/game5/paodao.webp") 0 0 / 3840px 543px
background: v-bind("imageUrls.runway") 0 0 / 3840px 543px
no-repeat;
flex-shrink: 0;
}
......
......@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
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()
......
<script setup lang="ts">
import { ref } from 'vue'
import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
......@@ -44,6 +43,7 @@ const imageUrls = {
};
const isStartPressed = ref(false);
const isQrcodeExpanded = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
......@@ -69,9 +69,12 @@ const renderAvatar = (item: any | null) => {
<div class="img-title1"></div>
<!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</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-rabbit"></div>
<div class="list-container">
......@@ -82,7 +85,7 @@ const renderAvatar = (item: any | null) => {
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 8) }}</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
......@@ -100,6 +103,9 @@ const renderAvatar = (item: any | null) => {
</template>
<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 {
width: 1920px;
height: 1080px;
......@@ -211,6 +217,10 @@ const renderAvatar = (item: any | null) => {
height: 250px;
align-content: center;
>div {
min-width: 0;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
......@@ -240,9 +250,15 @@ const renderAvatar = (item: any | null) => {
}
.txt-nickname {
display: block;
width: calc(100% - 12px);
margin: 4px auto 0;
overflow: hidden;
color: #666;
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
......
......@@ -2,7 +2,7 @@ import { fileURLToPath, URL } from 'node:url'
import { cpSync, existsSync, rmSync } from 'node:fs'
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
......@@ -25,7 +25,11 @@ function copyImageAssets() {
}
}
export default defineConfig(({ command }) => ({
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const useLocalAssets = env.VITE_ASSET_BUILD_LOCAL === 'true'
return {
base: '/',//command === 'build' ? '/cc/pc/' : '/',
define: {
__APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
......@@ -34,7 +38,7 @@ export default defineConfig(({ command }) => ({
plugins: [
vue(),
vueDevTools(),
copyImageAssets(),
...(useLocalAssets || !env.VITE_ASSET_BASE_URL ? [copyImageAssets()] : []),
],
server: {
host: true,
......@@ -55,4 +59,5 @@ export default defineConfig(({ command }) => ({
'@': 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