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

更新h5代码

parent 948fdad3
...@@ -1031,6 +1031,32 @@ dependencies = [ ...@@ -1031,6 +1031,32 @@ dependencies = [
] ]
[[package]] [[package]]
name = "hddpServer"
version = "0.1.0"
dependencies = [
"aes",
"async-trait",
"base64",
"bytes",
"cbc",
"chrono",
"dashmap",
"lazy_static",
"md-5",
"rand 0.10.0",
"rmp-serde",
"rust-embed",
"salvo",
"serde",
"serde_json",
"socketioxide",
"sqlx",
"tokio",
"tower",
"tower-http",
]
[[package]]
name = "headers" name = "headers"
version = "0.4.1" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
...@@ -1611,32 +1637,6 @@ dependencies = [ ...@@ -1611,32 +1637,6 @@ dependencies = [
] ]
[[package]] [[package]]
name = "minigame"
version = "0.1.0"
dependencies = [
"aes",
"async-trait",
"base64",
"bytes",
"cbc",
"chrono",
"dashmap",
"lazy_static",
"md-5",
"rand 0.10.0",
"rmp-serde",
"rust-embed",
"salvo",
"serde",
"serde_json",
"socketioxide",
"sqlx",
"tokio",
"tower",
"tower-http",
]
[[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
......
[package] [package]
name = "minigame" name = "hddpServer"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
...@@ -27,7 +27,7 @@ cbc = { version = "0.1.2", features = ["alloc"] } ...@@ -27,7 +27,7 @@ cbc = { version = "0.1.2", features = ["alloc"] }
md5 = { version = "0.10.6", package = "md-5" } md5 = { version = "0.10.6", package = "md-5" }
[[bin]] [[bin]]
name = "minigame" name = "hddpServer"
[target.x86_64-unknown-linux-musl] [target.x86_64-unknown-linux-musl]
linker = "rust-lld" linker = "rust-lld"
......
drop table if exists tb_game; drop table if exists tb_game;
create table tb_game create table tb_game
( (
event_date date, -- 活动日期 wechat_id text, -- 微信id
wechat_id varchar(50), -- 微信id
nickname varchar(50), -- 昵称 nickname varchar(50), -- 昵称
avatar varchar(200), -- 头像 avatar text, -- 头像
round_num int NOT NULL CHECK (round_num between 1 and 3), -- 比赛场次 game_rank int, -- 排名
item_num int not null check (item_num between 1 and 6), -- 游戏项目(1到6) item_num int not null check (item_num between 1 and 6), -- 游戏项目(1到6)
score int not null default 0 check (score >= 0), -- 得分 score int not null default 0 check (score >= 0), -- 得分
create_date timestamptz, -- 创建时间 create_date timestamptz -- 创建时间
unique(event_date,wechat_id,round_num,item_num)
); );
SELECT * FROM tb_game SELECT * FROM tb_game
......
DROP TABLE IF EXISTS tb_game; DROP TABLE IF EXISTS tb_game;
CREATE TABLE tb_game CREATE TABLE tb_game
( (
event_date DATE,
wechat_id VARCHAR(50), wechat_id VARCHAR(50),
nickname VARCHAR(50), nickname VARCHAR(50),
avatar VARCHAR(200), avatar VARCHAR(200),
round_num INT NOT NULL, game_rank INT NOT NULL,
item_num INT NOT NULL, item_num INT NOT NULL,
score INT NOT NULL DEFAULT 0, score INT NOT NULL DEFAULT 0,
create_date DATETIME, create_date DATETIME
UNIQUE KEY uk_tb_game_player_item (event_date, wechat_id, round_num, item_num)
); );
DROP TABLE IF EXISTS tb_log; DROP TABLE IF EXISTS tb_log;
......
...@@ -4,15 +4,13 @@ use serde_json::json; ...@@ -4,15 +4,13 @@ use serde_json::json;
use crate::db::models::log::Log; use crate::db::models::log::Log;
pub struct Game { pub struct Game {
/// 活动日期
pub event_date: NaiveDate,
pub wechat_id: String, pub wechat_id: String,
pub nickname: Option<String>, pub nickname: String,
pub avatar: Option<String>, pub avatar: String,
/// 比赛场次
pub round_num: i32,
/// 游戏项目(1到6) /// 游戏项目(1到6)
pub item_num: i32, pub item_num: i32,
/// 排序
pub game_rank: i32,
/// 得分 /// 得分
pub score: i32, pub score: i32,
/// 创建时间 /// 创建时间
...@@ -22,14 +20,13 @@ pub struct Game { ...@@ -22,14 +20,13 @@ pub struct Game {
impl Game { impl Game {
pub async fn insert(&self) -> bool { pub async fn insert(&self) -> bool {
let res = crate::db::db_query( let res = crate::db::db_query(
r#"insert into tb_game(event_date,wechat_id,nickname,avatar,round_num,item_num,score,create_date) values(?::date,?,?,?,?,?,?,now());"#, r#"insert into tb_game(wechat_id,nickname,avatar,item_num,game_rank,score,create_date) values(?,?,?,?,?,?,now());"#,
vec![ vec![
json!(&self.event_date),
json!(&self.wechat_id), json!(&self.wechat_id),
json!(&self.nickname), json!(&self.nickname),
json!(&self.avatar), json!(&self.avatar),
json!(&self.round_num),
json!(&self.item_num), json!(&self.item_num),
json!(&self.game_rank),
json!(&self.score), json!(&self.score),
], ],
) )
...@@ -46,12 +43,11 @@ impl Game { ...@@ -46,12 +43,11 @@ impl Game {
id: 0, id: 0,
msg: err, msg: err,
params: json!({ params: json!({
"event_date": self.event_date,
"wechat_id": self.wechat_id, "wechat_id": self.wechat_id,
"nickname": self.nickname, "nickname": self.nickname,
"avatar": self.avatar, "avatar": self.avatar,
"round_num": self.round_num,
"item_num": self.item_num, "item_num": self.item_num,
"game_rank":self.game_rank,
"score": self.score, "score": self.score,
}), }),
create_date: None, create_date: None,
......
This diff is collapsed.
import SHA256 from 'crypto-js/sha256'
type StorageRecord = Record<string, unknown>; type StorageRecord = Record<string, unknown>;
function isStorageRecord(value: unknown): value is StorageRecord { function isStorageRecord(value: unknown): value is StorageRecord {
...@@ -54,37 +56,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto ...@@ -54,37 +56,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto
} }
} }
export function $goto_login(evt: string, router: any, path: string): boolean {
if (evt == "login_result") {
$remove_socket_storage();
router.replace(path).then(() => { });
return true;
}
return false;
}
export function $remove_socket_storage(): void {
localStorage.removeItem('socket_path');
localStorage.removeItem('client_ns');
localStorage.removeItem('wechat_id');
localStorage.removeItem('nickname');
localStorage.removeItem('telephone');
localStorage.removeItem('avatar');
}
export function $read_socket_storge(): any {
const domain = $read('domain', '');
const socket_path = $read('socket_path', '');
const client_ns = $read('client_ns', '')
const wechat_id = $read('wechat_id', '');
const nickname = $read('nickname', '');
const telephone = $read('telephone', '');
const avatar = $read('avatar', '');
return {
domain, socket_path, client_ns, wechat_id, nickname, telephone, avatar
}
}
export function $format_str(str: string, num: number) { export function $format_str(str: string, num: number) {
if (str.length <= num - 2) { if (str.length <= num - 2) {
return str; return str;
...@@ -93,8 +64,30 @@ export function $format_str(str: string, num: number) { ...@@ -93,8 +64,30 @@ export function $format_str(str: string, num: number) {
} }
export function $is_run_local(): boolean { export function $is_run_local(): boolean {
if (/localhost|192.168/gim.test(location.href)) { if (/https?:\/\/localhost|192.168/gim.test(location.href)) {
return true; return true;
} }
return false; return false;
} }
function $query<T extends string | string[]>(q: T): T {
const query = location.href.match(/\?([^#]*)/)?.[1]?.replace(/\/$/, '');
const params = new URLSearchParams(query);
if (Array.isArray(q)) {
return q.map((key) => params.get(key)).filter((v): v is string => !!v) as T;
}
return params.get(q) as T;
}
export function $getWechat(): any | null {
const q = $query(['token', 'nickname', 'avatar']);
if (q && q.length == 3) {
return {
token: SHA256(q[0]).toString(), //原token很长,这里压缩下,压缩后不能还原,最后提交成绩时要提交原始token
token_origin: q[0],
nickname: q[1],
avatar: q[2],
};
}
return null;
}
\ No newline at end of file
...@@ -35,8 +35,19 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -35,8 +35,19 @@ export function initSocket(args: SocketInitArgs): Socket | null {
if (socket) { if (socket) {
if (socket.connected) { if (socket.connected) {
args.onConnect?.(socket); args.onConnect?.(socket);
return socket;
} }
return socket; socket.removeAllListeners();
socket.disconnect();
socket = null;
}
if ($is_run_local()) {
console.log('socket.io init:', {
namespace: args.client_ns,
path: args.socket_path,
auth: args.auth,
});
} }
socket = io(args.client_ns, { socket = io(args.client_ns, {
...@@ -49,6 +60,9 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -49,6 +60,9 @@ export function initSocket(args: SocketInitArgs): Socket | null {
}); });
socket.on('connect', () => { socket.on('connect', () => {
if ($is_run_local()) {
console.log('socket.io connected:', socket?.id);
}
args.onConnect?.(socket!); args.onConnect?.(socket!);
}); });
socket.onAny((evt, payload) => { socket.onAny((evt, payload) => {
...@@ -60,6 +74,9 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -60,6 +74,9 @@ export function initSocket(args: SocketInitArgs): Socket | null {
}); });
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
if ($is_run_local()) {
console.log('socket.io disconnect:', reason);
}
args.onDisconnect?.(reason); args.onDisconnect?.(reason);
}); });
...@@ -72,6 +89,9 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -72,6 +89,9 @@ export function initSocket(args: SocketInitArgs): Socket | null {
export function sendSocketMessage(cmd: string, data: any = {}): boolean { export function sendSocketMessage(cmd: string, data: any = {}): boolean {
if (!socket?.connected) { if (!socket?.connected) {
if ($is_run_local()) {
console.log('socket.io send blocked:', cmd, data);
}
return false; return false;
} }
......
...@@ -144,11 +144,11 @@ defineExpose({ ...@@ -144,11 +144,11 @@ defineExpose({
<main class="mobile-stage-viewport"> <main class="mobile-stage-viewport">
<div class="mobile-stage-background" :style="backgroundStyle" /> <div class="mobile-stage-background" :style="backgroundStyle" />
<div class="mobile-stage" :style="stageStyle"> <div class="mobile-stage" :style="stageStyle">
<div v-if="countdownVisible" class="countdown-modal">
<div :key="countdownKey" class="countdown-number">{{ countdown }}</div>
</div>
<slot /> <slot />
</div> </div>
<div v-if="countdownVisible" class="countdown-modal">
<div :key="countdownKey" class="countdown-number">{{ countdown }}</div>
</div>
</main> </main>
</template> </template>
...@@ -187,7 +187,7 @@ defineExpose({ ...@@ -187,7 +187,7 @@ defineExpose({
.countdown-number { .countdown-number {
color: white; color: white;
font-size: 300pt; font-size: 200pt;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
text-shadow: 0 18px 38px rgba(0, 0, 0, 0.25); text-shadow: 0 18px 38px rgba(0, 0, 0, 0.25);
......
import { onBeforeUnmount, onMounted, ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { Socket } from 'socket.io-client' import type { Socket } from 'socket.io-client'
import { $read_socket_storge } from '@/commons/utils'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws' import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
import { $read } from '@/commons/utils'
type SocketPayload = { type SocketPayload = {
msg?: string msg?: string
...@@ -11,9 +11,12 @@ type SocketPayload = { ...@@ -11,9 +11,12 @@ type SocketPayload = {
} }
type GameSocketOptions = { type GameSocketOptions = {
gameId: string gameId: string,
auth: any,
onConnect?: () => void,
onGameStart?: (data: any) => void onGameStart?: (data: any) => void
onScoreSubmitted?: (data: any) => void onScoreSubmitted?: (is_save: boolean, data: any) => void
onRescoreSubmitted?: (recount: number) => void
onRoomClosed?: (data: any) => void onRoomClosed?: (data: any) => void
onEvent?: (evt: string, payload: SocketPayload) => void onEvent?: (evt: string, payload: SocketPayload) => void
} }
...@@ -36,94 +39,106 @@ function getGameHomePath(data: any) { ...@@ -36,94 +39,106 @@ function getGameHomePath(data: any) {
return `/game${gameIndex}` return `/game${gameIndex}`
} }
export function joinSharedRoom(nickname: string, token: string, avatar: string) {
return sendSocketMessage('room_join', { nickname: nickname, token, avatar })
}
export function sendGameMessage(cmd: string, data: any = {}) {
return sendSocketMessage(cmd, data)
}
export const userJoinStatus = ref(false)
export function useGameSocket(options: GameSocketOptions) { export function useGameSocket(options: GameSocketOptions) {
const router = useRouter() const router = useRouter()
const userJoinStatus = ref(false)
let socket: Socket | null = null let socket: Socket | null = null
let offSocketMessage: (() => void) | undefined let offSocketMessage: (() => void) | undefined
const domain = $read('domain', '');
let resubmitcount = 0;
socket = initSocket({
socket_path: '/socket.io',
client_ns: `${domain}/ws`,
auth: options.auth,
onConnect: (socket) => {
options.onConnect?.();
},
onDisconnect: (_reason: string) => { },
onError: (error: any) => {
console.error('socket.io connect_error:', error)
},
})
function joinSharedRoom() { offSocketMessage = onSocketMessage((evt, payload: SocketPayload) => {
const { nickname, telephone, avatar } = $read_socket_storge() const { msg, state, data } = payload
return sendSocketMessage('room_join', { nickname, telephone, avatar })
}
function sendGameMessage(cmd: string, data: any = {}) {
return sendSocketMessage(cmd, data)
}
onMounted(() => { if (state === 0) {
const { domain, socket_path, client_ns, wechat_id } = $read_socket_storge() if (evt === 'room_join_result') {
switch (data) {
socket = initSocket({ case 1: userJoinStatus.value = true; break;
socket_path: `${domain}${socket_path}`, case 3: alert(msg); break;
client_ns,
auth: {
userid: wechat_id,
},
onConnect: () => {
window.setTimeout(joinSharedRoom, 200)
},
onDisconnect: (_reason: string) => { },
onError: (_error: any) => { },
})
offSocketMessage = onSocketMessage((evt, payload: SocketPayload) => {
const { msg, state, data } = payload
if (state === 0 && evt === 'room_join_result') {
if (data === 1) {
userJoinStatus.value = true
} }
return } else if (evt == 'submit_score_save') {
//提交分数的情况下有问题的情况下,只重试3次
if (resubmitcount >= 3) {
return;
}
setTimeout(() => {
resubmitcount++;
options.onRescoreSubmitted?.(resubmitcount);
}, 200);
} }
return;
}
if (state === 0) { if (state === 0) {
alert(msg) alert(msg)
return return
} }
switch (evt) { switch (evt) {
case 'room_create_result': { case 'room_create_result': {
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
} }
case 'room_join_result': { break
userJoinStatus.value = true }
break case 'room_join_result': {
} userJoinStatus.value = true
case 'game_start_result': { break
if (data === options.gameId) { }
options.onGameStart?.(data) case 'game_start_result': {
} if (data === options.gameId) {
break options.onGameStart?.(data)
}
case 'submit_score_result': {
options.onScoreSubmitted?.(data)
break
}
case 'room_close': {
userJoinStatus.value = false
options.onRoomClosed?.(data)
break
} }
break
} }
case 'submit_score_result': {
options.onScoreSubmitted?.(false, data)
break
}
case 'submit_score_save_result': {
options.onScoreSubmitted?.(true, data)
break
}
case 'room_close': {
userJoinStatus.value = false
options.onRoomClosed?.(data)
break
}
}
options.onEvent?.(evt, payload) options.onEvent?.(evt, payload)
})
}) })
onBeforeUnmount(() => { // onBeforeUnmount(() => {
offSocketMessage?.() // offSocketMessage?.()
}) // })
return { return {
socket, socket,
userJoinStatus, userJoinStatus,
joinSharedRoom, offSocketMessage
sendGameMessage,
} }
} }
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MobileStage from '@/components/MobileStage.vue'
import { $save } from '@/commons/utils'
const router = useRouter()
const nickname = ref('')
async function getConn() {
let domain = localStorage.getItem('domain');
const res = await fetch(`${domain}/manager/getConn`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
const data = await res.json()
if (!res.ok || data.state !== 1) {
alert(data.msg || 'Login failed')
throw new Error(data.msg || 'Login failed')
}
return data
}
const loginHandler = async () => {
const { state, msg, data } = await getConn()
if (state === 0) {
alert(msg)
return
}
for (let i in data) {
$save(i, data[i]);
}
$save({
wechat_id: nickname.value,
nickname: nickname.value,
telephone: '13111223344',
avatar: '13111223344',
});
await router.replace('/game1')
}
</script>
<template>
<MobileStage>
<section class="h5-page login-page">
<div class="brand">
<span class="brand-mark">H5</span>
<div>
<h1 class="h5-title">小游戏</h1>
<p class="h5-subtitle">小屏互动端</p>
</div>
</div>
<form class="login-form h5-panel">
<label class="field">
<span>昵称</span>
<input v-model="nickname" maxlength="16" placeholder="请输入昵称" autocomplete="nickname">
</label>
<input class="h5-button" @click="loginHandler" type="button" value="进入游戏">
</form>
</section>
</MobileStage>
</template>
<style scoped>
.login-page {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 32px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
padding-top: 18px;
}
.brand-mark {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border-radius: 8px;
background: var(--color-primary);
color: white;
font-size: 20px;
font-weight: 900;
}
.login-form {
display: grid;
gap: 20px;
padding: 18px;
}
.field {
display: grid;
gap: 8px;
color: var(--color-muted);
font-size: 14px;
}
.field input {
width: 100%;
height: 48px;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0 14px;
background: #fff;
color: var(--color-text);
outline: none;
}
.field input:focus {
border-color: var(--color-primary);
}
</style>
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
import { onBeforeUnmount, onMounted, ref } from 'vue' import { 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 } from '@/composables/useGameSocket' import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket'
import LoadingView from './views/LoadingView.vue' import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue' import PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue' import ScoreView from './views/ScoreView.vue'
import { $getWechat } from '@/commons/utils.ts'
type GameView = 'loading' | 'playing' | 'score' type GameView = 'loading' | 'playing' | 'score'
...@@ -25,16 +26,20 @@ const imageUrls = { ...@@ -25,16 +26,20 @@ const imageUrls = {
replay: cssAssetUrl('game1/replay.png'), replay: cssAssetUrl('game1/replay.png'),
} }
const wechat = $getWechat()
const currentView = ref<GameView>('loading') const currentView = ref<GameView>('loading')
const countdownInterval = ref(60) const countdownInterval = ref(60)
const isDivDescVisible = ref(false) const isDivDescVisible = ref(false)
const tick = ref(0) const tick = ref(0) //点击次数
const rank = ref(8) const rank = ref(0) //排名
const level = ref(5)
const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03] const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03]
const currentGu = ref(imageUrls.gu01) const currentGu = ref(imageUrls.gu01)
const isGuPlaying = ref(false) const isGuPlaying = ref(false)
const token = ref(wechat?.token ?? '')
const nickname = ref(wechat?.nickname ?? '')
const avatar = ref(wechat?.avatar ?? '')
const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null) const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null)
const _offSocketMessage = ref<(() => void) | undefined>()
let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined
let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined
...@@ -55,8 +60,22 @@ function stopGameCountdown() { ...@@ -55,8 +60,22 @@ function stopGameCountdown() {
} }
} }
function submitScore() { function submitScore(save: boolean = false) {
sendGameMessage('submit_score', { score: tick.value }) //最后提交保存在数据库中
if (save) {
//item_num 游戏项目(1到6)
//wechat 原始token
sendGameMessage('submit_score_save', {
score: tick.value,
wechat: wechat.token_origin,
item_num: 1,
nickname: wechat.nickname,
avatar: wechat.avatar,
rank: rank.value,
})
} else {
sendGameMessage('submit_score', { score: tick.value })
}
} }
function startGameCountdown(seconds = 60) { function startGameCountdown(seconds = 60) {
...@@ -71,7 +90,7 @@ function startGameCountdown(seconds = 60) { ...@@ -71,7 +90,7 @@ function startGameCountdown(seconds = 60) {
countdownInterval.value = 0 countdownInterval.value = 0
gameCountdownTimer = undefined gameCountdownTimer = undefined
setDivDescVisible(false) setDivDescVisible(false)
submitScore() submitScore(true)
showScoreView() showScoreView()
return return
} }
...@@ -93,46 +112,24 @@ function startGameView() { ...@@ -93,46 +112,24 @@ function startGameView() {
tick.value = 0 tick.value = 0
currentGu.value = guFrames[0] currentGu.value = guFrames[0]
currentView.value = 'playing' currentView.value = 'playing'
startGameCountdown(60) startGameCountdown()
}
async function startGameWithCountdown() {
await mobileStageRef.value?.startCountdown(3)
startGameView()
} }
function showScoreView() { function showScoreView() {
stopGameCountdown() stopGameCountdown()
setDivDescVisible(false) setDivDescVisible(false)
level.value = rank.value
currentView.value = 'score' currentView.value = 'score'
} }
function updateSubmittedRank(data: any) {
const nextRank = Number(typeof data === 'object' ? data?.rank : data)
if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank
level.value = nextRank
}
}
function replayGame() { function replayGame() {
startGameView() startGameView()
} }
function backToWaiting() { function backToWaiting() {
showLoadingView() showLoadingView()
joinSharedRoom() joinSharedRoom(nickname.value, token.value, avatar.value)
} }
const { userJoinStatus, joinSharedRoom, sendGameMessage } = useGameSocket({
gameId: 'game1',
onGameStart: startGameWithCountdown,
onScoreSubmitted: updateSubmittedRank,
onRoomClosed: showLoadingView,
})
const touchHandler = () => { const touchHandler = () => {
if (isGuPlaying.value) { if (isGuPlaying.value) {
return return
...@@ -162,26 +159,65 @@ const touchHandler = () => { ...@@ -162,26 +159,65 @@ const touchHandler = () => {
playNextFrame() playNextFrame()
} }
if (wechat) {
const { offSocketMessage } = useGameSocket({
gameId: 'game1',
auth: {
userid: wechat.token,
},
onConnect: () => {
window.setTimeout(() => {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value)
}, 200)
},
onGameStart: async () => {
await mobileStageRef.value?.startCountdown()
startGameView()
},
onScoreSubmitted: (is_save, data) => {
if (!is_save) {//非保存状态下
const nextRank = Number(typeof data === 'object' ? data?.rank : data)
if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank
}
}
},
onRescoreSubmitted: (_recount) => {
submitScore(true);
},
onRoomClosed: showLoadingView,
})
_offSocketMessage.value = offSocketMessage
}
onMounted(() => { onMounted(() => {
window.addEventListener('beforeunload', confirmRefresh) if (token.value) {
window.addEventListener('beforeunload', confirmRefresh)
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer) window.clearTimeout(guFrameTimer)
} }
stopGameCountdown() stopGameCountdown()
window.removeEventListener('beforeunload', confirmRefresh) if (token.value) {
window.removeEventListener('beforeunload', confirmRefresh)
}
}) })
</script> </script>
<template> <template>
<MobileStage ref="mobileStageRef" :background="`${imageUrls.bg} center/cover`"> <MobileStage v-if="token" ref="mobileStageRef" :background="`${imageUrls.bg} center/cover`">
<LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus" /> <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus" />
<PlayingView v-else-if="currentView === 'playing'" :image-urls="imageUrls" :tick="tick" :rank="rank" <PlayingView v-else-if="currentView === 'playing'" :image-urls="imageUrls" :tick="tick" :rank="rank"
:current-gu="currentGu" :countdown-interval="countdownInterval" :is-div-desc-visible="isDivDescVisible" :current-gu="currentGu" :countdown-interval="countdownInterval" :is-div-desc-visible="isDivDescVisible"
@touch="touchHandler" /> @touch="touchHandler" />
<ScoreView v-else :image-urls="imageUrls" :tick="tick" :level="level" @replay="replayGame" <ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" @replay="replayGame"
@back="backToWaiting" /> @back="backToWaiting" />
</MobileStage> </MobileStage>
<div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;">
请使用微信扫码进入游戏
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { $getWechat } from '@/commons/utils.ts'
import { onMounted, ref } from 'vue';
defineProps<{ defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
// countdownInterval: number
// isDivDescVisible: boolean
userJoinStatus: boolean userJoinStatus: boolean
}>() }>();
const nickname = ref('');
const avatar = ref('');
onMounted(() => {
const wechat = $getWechat();
nickname.value = wechat.nickname;
avatar.value = wechat.avatar;
});
</script> </script>
<template> <template>
<div class="h5-page game-stage"> <div class="h5-page game-stage">
<!-- <Transition name="div-desc-slide" appear>
<div v-if="isDivDescVisible" class="div-desc">
<span class="desc-clock" aria-hidden="true"></span>
<span class="desc-time">{{ countdownInterval }}</span>
</div>
</Transition> -->
<div class="game-desc">游戏规则</div> <div class="game-desc">游戏规则</div>
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title"></div> <div class="img-title"></div>
<div class="img-avatar"></div> <div class="img-avatar" :style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`"></div>
<div class="txt-nickname">张小明</div> <div class="txt-nickname">{{ nickname }}</div>
<div class="txt-loading"> <div class="txt-loading">
<label v-if="userJoinStatus">您已成功加入游戏等待主持人开始</label> <label v-if="userJoinStatus">您已成功加入游戏等待主持人开始</label>
<label v-else>正在加入游戏...</label> <label v-else>正在加入游戏...</label>
...@@ -37,64 +39,6 @@ defineProps<{ ...@@ -37,64 +39,6 @@ defineProps<{
padding: 0; padding: 0;
} }
/* .div-desc {
position: absolute;
top: 150px;
left: var(--stage-viewport-left, 0);
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
width: 180px;
height: 80px;
border-radius: 0 56px 56px 0;
background: rgba(40, 12, 8, 0.48);
color: #fff;
.desc-clock {
position: absolute;
left: 20px;
width: 38px;
height: 44px;
border-radius: 50%;
background: v-bind('imageUrls.clock') center/cover;
}
.desc-time {
position: absolute;
right: 40px;
color: white;
font-size: 35pt;
line-height: 1;
}
} */
/*
.div-desc-slide-enter-active {
transition:
transform 360ms ease-out 220ms,
opacity 360ms ease-out 220ms;
}
.div-desc-slide-leave-active {
transition:
transform 260ms ease-in,
opacity 260ms ease-in;
}
.div-desc-slide-enter-from,
.div-desc-slide-leave-to {
opacity: 0;
transform: translateX(-110%);
}
.div-desc-slide-enter-to,
.div-desc-slide-leave-from {
opacity: 1;
transform: translateX(0);
} */
.game-desc { .game-desc {
position: absolute; position: absolute;
top: 50%; top: 50%;
...@@ -150,15 +94,15 @@ defineProps<{ ...@@ -150,15 +94,15 @@ defineProps<{
position: absolute; position: absolute;
top: 480px; top: 480px;
left: 50%; left: 50%;
width: 148px; /* width: 148px;
height: 148px; height: 148px;
background: v-bind('imageUrls.avatar') center/cover; background: v-bind('avatar') center/cover; */
transform: translateX(-50%); transform: translateX(-50%);
} }
.txt-nickname { .txt-nickname {
position: absolute; position: absolute;
top: 650px; top: 640px;
left: 50%; left: 50%;
color: #E00000; color: #E00000;
font-size: 25pt; font-size: 25pt;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
defineProps<{ defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
tick: number tick: number
level: number rank: number
}>() }>()
defineEmits<{ defineEmits<{
...@@ -23,7 +23,7 @@ defineEmits<{ ...@@ -23,7 +23,7 @@ defineEmits<{
<div>击鼓次数(次)</div> <div>击鼓次数(次)</div>
<div>{{ tick }}</div> <div>{{ tick }}</div>
<div>最终排名</div> <div>最终排名</div>
<div><label>{{ level }}</label></div> <div><label>{{ rank }}</label></div>
</div> </div>
</div> </div>
<div class="btn-group"> <div class="btn-group">
......
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils' import { $read } from '@/commons/utils'
import Login from '@/pages/Login.vue'
import Game1 from '@/pages/game1/Game1.vue' import Game1 from '@/pages/game1/Game1.vue'
const getWechatId = () => { const getWechatId = () => {
return $read('wechat_id', '') return $read('token', '')
} }
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ // {
path: '/', // path: '/',
redirect: () => (getWechatId() ? '/game1' : '/login'), // redirect: () => (getWechatId() ? '/game1' : '/login'),
}, // },
{
path: '/login',
name: 'Login',
component: Login,
},
{ {
path: '/game1', path: '/game1',
name: 'Game1', name: 'Game1',
component: Game1, component: Game1,
meta: { // meta: {
requiresAuth: true, // requiresAuth: true,
}, // },
}, },
// { // {
// path: '/:pathMatch(.*)*', // path: '/:pathMatch(.*)*',
......
...@@ -6,7 +6,7 @@ import { defineConfig } from 'vite' ...@@ -6,7 +6,7 @@ import { defineConfig } 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'
const buildDomain = 'https://hddpserver.guocai365.org.cn' const buildDomain = 'https://hddp.guocai365.org.cn'
function copyImageAssets() { function copyImageAssets() {
return { return {
......
import game1MusicUrl from '@/assets/images/game1.mp3'
let backgroundMusic: HTMLAudioElement | null = null
function getBackgroundMusic() {
if (!backgroundMusic) {
backgroundMusic = new Audio(game1MusicUrl)
backgroundMusic.loop = true
backgroundMusic.preload = 'auto'
backgroundMusic.volume = 0.6
backgroundMusic.load()
}
return backgroundMusic
}
export async function playGame1Music() {
const audio = getBackgroundMusic()
try {
await audio.play()
window.removeEventListener('pointerdown', playGame1Music)
window.removeEventListener('keydown', playGame1Music)
} catch {
window.addEventListener('pointerdown', playGame1Music, { once: true })
window.addEventListener('keydown', playGame1Music, { once: true })
}
}
export function stopGame1Music() {
window.removeEventListener('pointerdown', playGame1Music)
window.removeEventListener('keydown', playGame1Music)
backgroundMusic?.pause()
}
...@@ -74,7 +74,7 @@ export function $read_socket_storge(): any { ...@@ -74,7 +74,7 @@ export function $read_socket_storge(): any {
let token = $read('token', ''); let token = $read('token', '');
const socket_path = $read('socket_path', ''); const socket_path = $read('socket_path', '');
const client_ns = $read('client_ns', '') const client_ns = $read('client_ns', '')
const domain = localStorage.getItem('domain') || '' const domain = $read('domain', '');
return { return {
passphrase, token, socket_path, client_ns, domain passphrase, token, socket_path, client_ns, domain
} }
...@@ -88,8 +88,17 @@ export function $format_str(str: string, num: number) { ...@@ -88,8 +88,17 @@ export function $format_str(str: string, num: number) {
} }
export function $is_run_local(): boolean { export function $is_run_local(): boolean {
if (/localhost|192.168/gim.test(location.href)) { if (/https?:\/\/localhost|192.168/gim.test(location.href)) {
return true; return true;
} }
return false; return false;
} }
export function $query(q: string | string[]): string | string[] {
const query = location.href.match(/\?([^#]*)/)?.[1]?.replace(/\/$/, '');
const params = new URLSearchParams(query);
if (Array.isArray(q)) {
return q.map((key) => params.get(key)).filter((v): v is string => !!v);
}
return params.get(q) || '';
}
\ No newline at end of file
import { io, type Socket } from "socket.io-client"; import { io, type Socket } from "socket.io-client";
import { decode, encode } from "@msgpack/msgpack"; import { decode, encode } from "@msgpack/msgpack";
import { $is_run_local } from "./utils";
export type SocketIOClientOptions = { export type SocketIOClientOptions = {
url?: string; url?: string;
...@@ -36,7 +37,6 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -36,7 +37,6 @@ export function initSocket(args: SocketInitArgs): Socket | null {
args.onConnect?.(socket); args.onConnect?.(socket);
return socket; return socket;
} }
socket.removeAllListeners(); socket.removeAllListeners();
socket.disconnect(); socket.disconnect();
socket = null; socket = null;
...@@ -56,7 +56,9 @@ export function initSocket(args: SocketInitArgs): Socket | null { ...@@ -56,7 +56,9 @@ export function initSocket(args: SocketInitArgs): Socket | null {
}); });
socket.onAny((evt, payload) => { socket.onAny((evt, payload) => {
const data = decode(payload); const data = decode(payload);
console.log(evt, data); if ($is_run_local()) {
console.log(evt, data);
}
messageHandlers.forEach((handler) => handler(evt, data)); messageHandlers.forEach((handler) => handler(evt, data));
}); });
......
...@@ -45,7 +45,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -45,7 +45,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
const { passphrase, token, socket_path, client_ns, domain } = $read_socket_storge() const { passphrase, token, socket_path, client_ns, domain } = $read_socket_storge()
initSocket({ initSocket({
socket_path: `${domain}${socket_path}`, socket_path: `${domain}${socket_path}`,
client_ns, client_ns:"/ws",
auth: { auth: {
userid: token, userid: token,
validuser: CryptoJS.AES.encrypt(token, passphrase).toString(), validuser: CryptoJS.AES.encrypt(token, passphrase).toString(),
...@@ -58,9 +58,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -58,9 +58,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => { offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
const { msg, state, data } = payload const { msg, state, data } = payload
if ($goto_login(evt, router, options.loginRedirectPath ?? '/game1')) { // if ($goto_login(evt, router, options.loginRedirectPath ?? '/game1')) {
return // return
} // }
if (state === 0) { if (state === 0) {
if (evt === 'game_start_result' && data === 1) { if (evt === 'game_start_result' && data === 1) {
......
<script setup lang="ts"> <script setup lang="ts">
import { $save } from '@/commons/utils.ts'; import { $save } from '@/commons/utils.ts';
import { ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import DesignStage from '@/components/DesignStage.vue' import DesignStage from '@/components/DesignStage.vue'
import { playGame1Music } from '@/commons/music'
const router = useRouter() const router = useRouter()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
...@@ -16,8 +17,8 @@ async function login() { ...@@ -16,8 +17,8 @@ async function login() {
}, },
body: JSON.stringify({ username: username.value, password: password.value }), body: JSON.stringify({ username: username.value, password: password.value }),
}) })
const data = await res.json()
const data = await res.json()
if (!res.ok || data.state !== 1) { if (!res.ok || data.state !== 1) {
alert(data.msg || 'Login failed') alert(data.msg || 'Login failed')
throw new Error(data.msg || 'Login failed') throw new Error(data.msg || 'Login failed')
...@@ -27,6 +28,7 @@ async function login() { ...@@ -27,6 +28,7 @@ async function login() {
} }
const loginHandler = async () => { const loginHandler = async () => {
playGame1Music()
const { state, msg, data } = await login() const { state, msg, data } = await login()
if (state === 0) { if (state === 0) {
...@@ -38,6 +40,10 @@ const loginHandler = async () => { ...@@ -38,6 +40,10 @@ const loginHandler = async () => {
} }
await router.replace('/game1') await router.replace('/game1')
} }
onMounted(() => {
playGame1Music()
})
</script> </script>
<template> <template>
......
...@@ -5,6 +5,7 @@ import Rank1 from './views/Rank1View.vue' ...@@ -5,6 +5,7 @@ import Rank1 from './views/Rank1View.vue'
import Rank2 from './views/Rank2View.vue' 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 { playGame1Music } from '@/commons/music'
type GameScreen = 'loading' | 'rank1' | 'rank2' type GameScreen = 'loading' | 'rank1' | 'rank2'
...@@ -108,6 +109,11 @@ const { startGame, createRoom, requestRoomState } = useAdminGameSocket({ ...@@ -108,6 +109,11 @@ const { startGame, createRoom, requestRoomState } = useAdminGameSocket({
onRoomRank: updateRankPlayers, onRoomRank: updateRankPlayers,
}) })
const startGameWithMusic = () => {
playGame1Music()
startGame()
}
const nextRound = () => { const nextRound = () => {
gotoLoading() gotoLoading()
createRoom(1) createRoom(1)
...@@ -116,8 +122,14 @@ const nextRound = () => { ...@@ -116,8 +122,14 @@ const nextRound = () => {
}, 200) }, 200)
} }
const nextRoundWithMusic = () => {
playGame1Music()
nextRound()
}
onMounted(() => { onMounted(() => {
window.addEventListener('beforeunload', confirmRefresh) window.addEventListener('beforeunload', confirmRefresh)
playGame1Music()
}) })
onUnmounted(() => { onUnmounted(() => {
...@@ -126,7 +138,7 @@ onUnmounted(() => { ...@@ -126,7 +138,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount" @start="startGame" /> <Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount" @start="startGameWithMusic" />
<Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGame" /> <Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<Rank2 v-else :rank-list="rankPlayers" @next="nextRound" /> <Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" />
</template> </template>
...@@ -57,7 +57,7 @@ const releaseStartButton = () => { ...@@ -57,7 +57,7 @@ const releaseStartButton = () => {
<div v-for="(item, index) in players" :key="item.userid || item.wechat_id || `empty-${index}`"> <div v-for="(item, index) in players" :key="item.userid || item.wechat_id || `empty-${index}`">
<div> <div>
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar"></div> <div class="img-avatar" :style="`background:url('${item.avatar}') center / cover no-repeat;`"></div>
</div> </div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div> <div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
</div> </div>
...@@ -205,7 +205,7 @@ const releaseStartButton = () => { ...@@ -205,7 +205,7 @@ const releaseStartButton = () => {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 30px; border-radius: 30px;
background: v-bind('imageUrls.avatar'); /* background: v-bind('imageUrls.avatar'); */
} }
.txt-nickname { .txt-nickname {
......
...@@ -54,6 +54,13 @@ watch( ...@@ -54,6 +54,13 @@ watch(
(rankList) => updateFinalRank(rankList), (rankList) => updateFinalRank(rankList),
{ immediate: true, deep: true }, { immediate: true, deep: true },
); );
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
return `background: url('${item.avatar}') center / cover no-repeat;`
}
return `background: v-bind('imageUrls.avatar') center / cover no-repeat;`
}
</script> </script>
<template> <template>
...@@ -66,21 +73,24 @@ watch( ...@@ -66,21 +73,24 @@ watch(
<div class="podium"> <div class="podium">
<div class="rank-no1"> <div class="rank-no1">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div class="avatar" :style="`${renderAvatar(list[0])}`">
</div>
<div class="nickname">{{ list[0]?.nickname }}</div> <div class="nickname">{{ list[0]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
</div> </div>
<div class="rank-no2"> <div class="rank-no2">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div class="avatar" :style="`${renderAvatar(list[1])}`">
</div>
<div class="nickname">{{ list[1]?.nickname }}</div> <div class="nickname">{{ list[1]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
</div> </div>
<div class="rank-no3"> <div class="rank-no3">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div class="avatar" :style="`${renderAvatar(list[2])}`">
</div>
<div class="nickname">{{ list[2]?.nickname }}</div> <div class="nickname">{{ list[2]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
...@@ -172,7 +182,7 @@ watch( ...@@ -172,7 +182,7 @@ watch(
} }
.avatar { .avatar {
background: v-bind('imageUrls.avatar') center / cover no-repeat; /* background: v-bind('imageUrls.avatar') center / cover no-repeat; */
width: 120px; width: 120px;
height: 120px; height: 120px;
border-radius: 60px; border-radius: 60px;
......
...@@ -6,7 +6,7 @@ import { defineConfig } from 'vite' ...@@ -6,7 +6,7 @@ import { defineConfig } 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'
const buildDomain = 'https://hddpserver.guocai365.org.cn' const buildDomain = 'https://hddp.guocai365.org.cn'
function copyImageAssets() { function copyImageAssets() {
return { return {
...@@ -28,7 +28,8 @@ function copyImageAssets() { ...@@ -28,7 +28,8 @@ function copyImageAssets() {
export default defineConfig(({ command }) => ({ export default defineConfig(({ command }) => ({
base: command === 'build' ? '/cc/pc/' : '/', base: command === 'build' ? '/cc/pc/' : '/',
define: { define: {
__APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''), // __APP_DOMAIN__: JSON.stringify(command === 'build' ? buildDomain : ''),
__APP_DOMAIN__: JSON.stringify(buildDomain),
}, },
plugins: [ plugins: [
vue(), vue(),
......
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