Commit 40a1a7b0 authored by 陈冲's avatar 陈冲

feat: 功能修复

parent a8efe97a
......@@ -379,6 +379,16 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
......@@ -1044,6 +1054,7 @@ dependencies = [
"lazy_static",
"md-5",
"rand 0.10.0",
"reqwest",
"rmp-serde",
"rust-embed",
"salvo",
......@@ -1222,9 +1233,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
......@@ -2187,9 +2200,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
version = "0.13.2"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64",
"bytes",
......@@ -2212,6 +2225,8 @@ dependencies = [
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
......@@ -2376,7 +2391,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
......@@ -2607,7 +2622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
......@@ -3133,6 +3148,27 @@ dependencies = [
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
......@@ -3874,6 +3910,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
......
......@@ -25,6 +25,7 @@ lazy_static = "1.5.0"
aes = "0.8.4"
cbc = { version = "0.1.2", features = ["alloc"] }
md5 = { version = "0.10.6", package = "md-5" }
reqwest = { version = "0.13.4", features = ["json"] }
[[bin]]
name = "hddpServer"
......
......@@ -15,7 +15,8 @@ pub struct Config {
#[serde(skip_serializing_if = "Option::is_none")]
pub aes_passphrase: Option<String>,
// 数据库配置:所有 master/worker 节点都可以直连同一台 DB。
pub db: DbConfig,
pub db: Option<DbConfig>,
// pub post: String,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
......@@ -34,7 +35,8 @@ impl Default for Config {
client_ns: "/ws".to_string(),
socket_path: "/socket.io".to_string(),
aes_passphrase: None,
db: DbConfig::default(),
db: None, //DbConfig::default(),
// post: "".to_string(),
}
}
}
......
......@@ -7,6 +7,10 @@ use base64::{Engine as _, engine::general_purpose};
use cbc::Decryptor;
use md5::{Digest, Md5};
use rand::Rng;
use reqwest::Client;
use std::option::Option;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::LazyLock;
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load().unwrap());
......@@ -141,3 +145,52 @@ pub fn add_minute(ts: u64, val: u64) -> u64 {
pub fn add_hour(ts: u64, val: u64) -> u64 {
add_minute(ts, val * 60)
}
// ///返回为空时表示提交异常
// pub async fn post(token: &str, index: i64) -> Result<Option<String>, Box<dyn std::error::Error>> {
// let url = CONFIG.post.clone();
// #[derive(Debug, Serialize)]
// struct RequestBody {
// activity_id: i32,
// game_code: String,
// page: i32,
// pageSize: i32,
// }
// let mut map = HashMap::new();
// map.insert(1, "JGQF");
// map.insert(2, "JBJF");
// map.insert(3, "MSYF");
// map.insert(4, "CMYF");
// map.insert(5, "QCNF");
// map.insert(6, "FYDT");
// let code = map.get(&index).and_then(|v| Some("")).unwrap();
// let body = RequestBody {
// activity_id: 1,
// game_code: code.to_string(), //大屏游戏代码 (马上有福:MSYF, 击鼓齐福:JGQF, 策马迎福:CMYF, 圈彩纳福:QCNF, 福运当头:FYDT, 聚宝接福:JBJF)
// page: 1,
// pageSize: 20,
// };
// let client = Client::new();
// let resp = client
// .post(url)
// .bearer_auth(token)
// .json(&body)
// .send()
// .await?;
// let status = resp.status();
// if !status.is_success() {
// let text = resp.text().await?;
// println!("请求失败: {}, body: {}", status, text);
// return Ok(None);
// }
// let result = resp.text().await?;
// println!("响应: {:?}", result);
// Ok(Some(result))
// }
......@@ -18,17 +18,21 @@ trait DbDriver: Send + Sync {
static DB_CONN: OnceCell<Box<dyn DbDriver>> = OnceCell::const_new();
pub async fn init_db() -> Result<(), String> {
let conn: Box<dyn DbDriver> = match config.db.kind.as_str() {
"postgresql" => Box::new(PgDb::new().await?),
"mysql" => Box::new(MysqlDb::new().await?),
other => {
return Err(format!("unsupported db kind: {other}"));
}
};
if let Some(db) = &config.db {
let conn: Box<dyn DbDriver> = match db.kind.as_str() {
"postgresql" => Box::new(PgDb::new().await?),
"mysql" => Box::new(MysqlDb::new().await?),
other => {
return Err(format!("unsupported db kind: {other}"));
}
};
DB_CONN
.set(conn)
.map_err(|_| "db already initialized".to_string())
DB_CONN
.set(conn)
.map_err(|_| "db already initialized".to_string())
} else {
Ok(())
}
}
pub async fn db_query(sql: &str, params: Vec<Value>) -> Result<Vec<Value>, String> {
......
use crate::{commons::utils::CONFIG as config, db::models::log::Log};
use chrono::{DateTime, NaiveDate, Utc};
use serde_json::json;
use crate::db::models::log::Log;
pub struct Game {
pub wechat_id: String,
pub nickname: String,
......@@ -19,6 +18,10 @@ pub struct Game {
impl Game {
pub async fn insert(&self) -> bool {
let Some(_) = &config.db else {
println!("没有配置数据库, return true;");
return true;
};
let res = crate::db::db_query(
r#"insert into tb_game(wechat_id,nickname,avatar,item_num,game_rank,score,create_date) values(?,?,?,?,?,?,now());"#,
vec![
......
use serde_json::{Value, json};
use crate::commons::utils::CONFIG as config;
use crate::db::models::*;
use serde_json::{Value, json};
#[derive(Debug)]
pub struct Manager {
......@@ -18,6 +18,15 @@ pub enum ManagerLogin {
impl Manager {
pub async fn Login(m: ManagerLogin) -> Result<Self, String> {
let Some(_) = &config.db else {
println!("没有配置数据库, return Self;");
return Ok(Manager {
id: 1,
username: "admin".to_string(),
password: "123456".to_string(),
token: "88236aeb-8d3f-6d56-fdf5-3adca3961df1".to_string(),
});
};
match m {
ManagerLogin::user(username, password) => {
let rows = crate::db::db_query(
......
......@@ -21,7 +21,7 @@ impl MysqlDb {
})
})
.max_connections(16)
.connect(&config.db.uri)
.connect(&config.db.clone().unwrap().uri)
.await
.map_err(|err| err.to_string())?;
Ok(Self { pool })
......
......@@ -13,7 +13,7 @@ impl PgDb {
pub async fn new() -> Result<Self, String> {
let pool = PgPoolOptions::new()
.max_connections(16)
.connect(&config.db.uri)
.connect(&config.db.clone().unwrap().uri)
.await
.map_err(|err| err.to_string())?;
......
This diff is collapsed.
......@@ -63,6 +63,233 @@ export function $format_str(str: string, num: number) {
return str.substring(0, num) + '...';
}
let toastElement: HTMLDivElement | undefined;
let toastTimer: ReturnType<typeof window.setTimeout> | undefined;
export function $toast(message?: string, duration = 2000) {
if (!message) {
return;
}
if (!toastElement) {
toastElement = document.createElement('div');
toastElement.style.cssText = [
'position: fixed',
'left: 50%',
'top: 45%',
'z-index: 99999',
'max-width: 560px',
'padding: 18px 28px',
'border-radius: 18px',
'background: rgba(0, 0, 0, 0.72)',
'color: #fff',
'font-size: 22px',
'line-height: 1.4',
'text-align: center',
'transform: translate(-50%, -40%)',
'opacity: 0',
'pointer-events: none',
'transition: opacity 0.2s ease, transform 0.2s ease',
].join(';');
document.body.appendChild(toastElement);
}
if (toastTimer) {
window.clearTimeout(toastTimer);
toastTimer = undefined;
}
toastElement.textContent = message;
toastElement.style.opacity = '1';
toastElement.style.transform = 'translate(-50%, -50%)';
toastTimer = window.setTimeout(() => {
if (!toastElement) {
return;
}
toastElement.style.opacity = '0';
toastElement.style.transform = 'translate(-50%, -40%)';
toastTimer = undefined;
}, duration);
}
type ConfirmOptions = {
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
}
let confirmMaskElement: HTMLDivElement | undefined;
let confirmBoxElement: HTMLDivElement | undefined;
let confirmTitleElement: HTMLDivElement | undefined;
let confirmMessageElement: HTMLDivElement | undefined;
let confirmCancelElement: HTMLButtonElement | undefined;
let confirmOkElement: HTMLButtonElement | undefined;
let confirmResolve: ((value: boolean) => void) | undefined;
let confirmHideTimer: ReturnType<typeof window.setTimeout> | undefined;
function closeConfirm(result: boolean) {
if (!confirmMaskElement || !confirmBoxElement) {
confirmResolve?.(result);
confirmResolve = undefined;
return;
}
if (confirmHideTimer) {
window.clearTimeout(confirmHideTimer);
confirmHideTimer = undefined;
}
confirmMaskElement.style.opacity = '0';
confirmBoxElement.style.transform = 'scale(0.96)';
confirmHideTimer = window.setTimeout(() => {
if (confirmMaskElement) {
confirmMaskElement.style.display = 'none';
}
confirmHideTimer = undefined;
}, 200);
confirmResolve?.(result);
confirmResolve = undefined;
}
function ensureConfirmElement() {
if (confirmMaskElement) {
return;
}
confirmMaskElement = document.createElement('div');
confirmMaskElement.style.cssText = [
'position: fixed',
'inset: 0',
'z-index: 99998',
'display: none',
'align-items: center',
'justify-content: center',
'background: rgba(0, 0, 0, 0.45)',
'opacity: 0',
'transition: opacity 0.2s ease',
].join(';');
confirmBoxElement = document.createElement('div');
confirmBoxElement.style.cssText = [
'width: min(560px, calc(100vw - 80px))',
'box-sizing: border-box',
'padding: 34px 32px 28px',
'border-radius: 22px',
'background: #fff',
'color: #333',
'font-size: 24px',
'line-height: 1.45',
'text-align: center',
'box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22)',
'transform: scale(0.96)',
'transition: transform 0.2s ease',
].join(';');
confirmTitleElement = document.createElement('div');
confirmTitleElement.style.cssText = [
'margin-bottom: 16px',
'font-size: 30px',
'font-weight: 700',
'color: #222',
].join(';');
confirmMessageElement = document.createElement('div');
confirmMessageElement.style.cssText = [
'min-height: 34px',
'word-break: break-word',
].join(';');
const buttonWrap = document.createElement('div');
buttonWrap.style.cssText = [
'display: flex',
'gap: 18px',
'margin-top: 30px',
].join(';');
confirmCancelElement = document.createElement('button');
confirmCancelElement.type = 'button';
confirmCancelElement.style.cssText = [
'flex: 1',
'height: 58px',
'border: 1px solid #ddd',
'border-radius: 29px',
'background: #fff',
'color: #666',
'font-size: 24px',
].join(';');
confirmCancelElement.onclick = () => closeConfirm(false);
confirmOkElement = document.createElement('button');
confirmOkElement.type = 'button';
confirmOkElement.style.cssText = [
'flex: 1',
'height: 58px',
'border: 0',
'border-radius: 29px',
'background: #AA0000',
'color: #fff',
'font-size: 24px',
].join(';');
confirmOkElement.onclick = () => closeConfirm(true);
confirmMaskElement.onclick = (event) => {
if (event.target === confirmMaskElement) {
closeConfirm(false);
}
};
buttonWrap.append(confirmCancelElement, confirmOkElement);
confirmBoxElement.append(confirmTitleElement, confirmMessageElement, buttonWrap);
confirmMaskElement.appendChild(confirmBoxElement);
document.body.appendChild(confirmMaskElement);
}
export function $confirm(message?: string, title?: string): Promise<boolean>;
export function $confirm(options?: ConfirmOptions): Promise<boolean>;
export function $confirm(messageOrOptions?: string | ConfirmOptions, title?: string): Promise<boolean> {
const options: ConfirmOptions = typeof messageOrOptions === 'object'
? messageOrOptions
: { message: messageOrOptions, title };
if (!options.message) {
return Promise.resolve(false);
}
ensureConfirmElement();
if (confirmResolve) {
confirmResolve(false);
confirmResolve = undefined;
}
if (confirmHideTimer) {
window.clearTimeout(confirmHideTimer);
confirmHideTimer = undefined;
}
confirmTitleElement!.textContent = options.title ?? '提示';
confirmMessageElement!.textContent = options.message;
confirmCancelElement!.textContent = options.cancelText ?? '取消';
confirmOkElement!.textContent = options.confirmText ?? '确定';
confirmMaskElement!.style.display = 'flex';
window.requestAnimationFrame(() => {
if (!confirmMaskElement || !confirmBoxElement) {
return;
}
confirmMaskElement.style.opacity = '1';
confirmBoxElement.style.transform = 'scale(1)';
});
return new Promise<boolean>((resolve) => {
confirmResolve = resolve;
});
}
export function $is_run_local(): boolean {
if (/https?:\/\/localhost|192.168/gim.test(location.href)) {
return true;
......@@ -102,4 +329,39 @@ export function $getWechat(): any | null {
}
return null;
}
export async function postJson(token: string, index: number) {
const url = "https://api.guocai365.org.cn/adminapi/offline_clearance/score/list";
let arr = ['', 'JGQF', 'JBJF', 'MSYF', 'CMYF', 'QCNF', 'FYDT'];
try {
const resp = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
activity_id: 1,
game_code: arr[index],
page: 1,
pageSize: 20,
}),
});
const text = await resp.text();
if (!resp.ok) {
console.error("请求失败:", resp.status, text);
return;
}
// 如果返回的是 JSON
const data = JSON.parse(text);
console.log("响应:", data);
} catch (err) {
console.error("请求异常:", err);
}
}
\ No newline at end of file
......@@ -32,15 +32,15 @@ export function initSocket(args: SocketInitArgs): Socket | null {
onSocketMessage(args.onMessage);
}
if (socket) {
if (socket.connected) {
args.onConnect?.(socket);
return socket;
}
socket.removeAllListeners();
socket.disconnect();
socket = null;
}
// if (socket) {
// if (socket.connected) {
// args.onConnect?.(socket);
// return socket;
// }
// socket.removeAllListeners();
// socket.disconnect();
// socket = null;
// }
if ($is_run_local()) {
console.log('socket.io init:', {
......@@ -68,7 +68,7 @@ export function initSocket(args: SocketInitArgs): Socket | null {
socket.onAny((evt, payload) => {
const data = decode(payload);
if ($is_run_local()) {
console.log("recv<============", evt, data);
console.log("============>recv", evt, data);
}
messageHandlers.forEach((handler) => handler(evt, data));
});
......@@ -95,7 +95,7 @@ export function sendSocketMessage(cmd: string, data: any = {}): boolean {
return false;
}
if ($is_run_local()) {
console.log('================>send', cmd, data);
console.log('send<================', cmd, data);
}
socket.send(encode({ cmd, data }));
return true;
......@@ -114,7 +114,7 @@ export function emitSocket(eventName: string, data?: any): boolean {
return true;
}
export function closeSocket(): void {
socket?.disconnect();
socket = null;
}
// export function closeSocket(): void {
// socket?.disconnect();
// socket = null;
// }
......@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import type { Socket } from 'socket.io-client'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
import { $read } from '@/commons/utils'
import { $read, $toast, postJson } from '@/commons/utils'
type SocketPayload = {
msg?: string
......@@ -76,11 +76,16 @@ export function useGameSocket(options: GameSocketOptions) {
if (state === 0) {
if (evt === 'room_join_result') {
switch (data) {
case 1: userJoinStatus.value = true; break;
case 2:
case 1: userJoinStatus.value = true; return;
case 2: {
// $toast(msg)
alert(msg)
return;
}
case 3:
alert(msg);
break;
// alert(msg);
$toast(msg)
return;
}
} else if (evt == 'submit_score_save') {
//提交分数的情况下有问题的情况下,只重试3次
......@@ -102,7 +107,8 @@ export function useGameSocket(options: GameSocketOptions) {
}
if (state === 0) {
alert(msg)
// alert(msg)
$toast(msg)
return
}
......@@ -113,7 +119,9 @@ export function useGameSocket(options: GameSocketOptions) {
}
case 'room_create_result': {
if (`game${data}` == options.gameId) {
location.reload();
// location.reload();
options.onConnect?.();
// debugger
return;
}
const nextPath = getGameHomePath(data)
......@@ -125,20 +133,14 @@ export function useGameSocket(options: GameSocketOptions) {
}
case 'room_join_result': {
if (data != options.gameId) {
debugger
return;
}
$toast('您已进入本游戏房间')
userJoinStatus.value = true
break
}
case 'room_recover_result': {
/*{
"gameId": "game6",
"rank": 1,
"reconnect": true,
"remainingSeconds": 56,
"score": 0,
"status": "running"
}*/
//游戏进行中玩家掉线重连时
if (data.gameId != options.gameId) {
return;
......@@ -160,6 +162,7 @@ export function useGameSocket(options: GameSocketOptions) {
break
}
case 'submit_score_save_result': {
// postJson()
options.onScoreSubmitted?.(true, data)
break
}
......
......@@ -6,7 +6,7 @@ import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '
import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
import { $getWechat } from '@/commons/utils.ts'
import { $getWechat, $toast } from '@/commons/utils.ts'
import { playGame1Music } from '@/commons/music'
type GameView = 'loading' | 'playing' | 'score'
......@@ -212,7 +212,8 @@ if (wechat) {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
alert('管理员关闭了该房间')
// alert('管理员关闭了该房间')
$toast('管理员关闭了该房间')
},
onRoomBack: () => {
resetToLoadingView()
......@@ -282,14 +283,15 @@ const showGameRuleHandler = () => {
background: v-bind('imageUrls.rule') center / cover no-repeat;
width: 692px;
height: 797px;
margin-top: -20%;
transform: scale(0.5);
.rule-title {
text-align: center;
color: white;
font-weight: bold;
font-size: 22pt;
margin-top: 10px;
font-size: 26pt;
line-height: 50pt;
letter-spacing: 4px;
}
......
......@@ -40,17 +40,18 @@ onMounted(() => {
.game-desc {
position: absolute;
top: 50%;
top: 40%;
left: var(--stage-viewport-left, 0);
display: flex;
width: 86px;
height: 256px;
width: 58px;
height: 158px;
padding-top: 10px;
align-items: center;
justify-content: center;
border-radius: 0 12px 12px 0;
background: rgba(40, 12, 8, 0.52);
color: white;
font-size: 32pt;
color: rgba(255, 255, 255, 0.9);
font-size: 25px;
letter-spacing: 10px;
line-height: 1.25;
text-align: center;
......
......@@ -6,7 +6,7 @@ import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '
import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
import { $getWechat } from '@/commons/utils.ts'
import { $getWechat, $toast } from '@/commons/utils.ts'
const gameId = 'game4';
type GameView = 'loading' | 'playing' | 'score'
......@@ -123,7 +123,8 @@ function showLoadingView() {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
alert('管理员关闭了该房间')
// alert('管理员关闭了该房间')
$toast('管理员关闭了该房间')
}
function resetToLoadingView() {
......@@ -266,14 +267,15 @@ onBeforeUnmount(() => {
background: v-bind('imageUrls.rule') center / cover no-repeat;
width: 692px;
height: 797px;
margin-top: -20%;
transform: scale(0.5);
.rule-title {
text-align: center;
color: white;
font-weight: bold;
font-size: 22pt;
margin-top: 10px;
font-size: 26pt;
line-height: 50pt;
letter-spacing: 4px;
}
......
......@@ -6,7 +6,7 @@ import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '
import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue'
import ScoreView from './views/ScoreView.vue'
import { $getWechat } from '@/commons/utils.ts'
import { $getWechat, $toast } from '@/commons/utils.ts'
import { playGame1Music } from '@/commons/music'
const gameId = 'game5';
......@@ -150,7 +150,8 @@ function showLoadingView() {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
alert('管理员关闭了该房间')
// alert('管理员关闭了该房间')
$toast('管理员关闭了该房间')
}
function resetToLoadingView() {
......@@ -349,14 +350,15 @@ const rankCloseHandler = () => {
background: v-bind('imageUrls.rule') center / cover no-repeat;
width: 692px;
height: 797px;
margin-top: -20%;
transform: scale(0.5);
.rule-title {
text-align: center;
color: white;
font-weight: bold;
font-size: 22pt;
margin-top: 10px;
font-size: 26pt;
line-height: 50pt;
letter-spacing: 4px;
}
......
......@@ -42,17 +42,18 @@ onMounted(() => {
.game-desc {
position: absolute;
top: 50%;
top: 40%;
left: var(--stage-viewport-left, 0);
display: flex;
width: 86px;
height: 256px;
width: 58px;
height: 158px;
padding-top: 10px;
align-items: center;
justify-content: center;
border-radius: 0 12px 12px 0;
background: rgba(40, 12, 8, 0.52);
color: white;
font-size: 32pt;
color: rgba(255, 255, 255, 0.9);
font-size: 25px;
letter-spacing: 10px;
line-height: 1.25;
text-align: center;
......@@ -93,7 +94,7 @@ onMounted(() => {
.img-avatar {
position: absolute;
bottom: 11%;
bottom: 11%;
left: 50%;
/* width: 148px;
height: 148px;
......@@ -114,6 +115,7 @@ onMounted(() => {
text-align: center;
background: v-bind('imageUrls.contdown') center/cover;
}
.txt-nickname {
position: absolute;
bottom: 10%;
......@@ -146,15 +148,15 @@ onMounted(() => {
color: white;
position: absolute;
z-index: -1;
top:786px;
top: 786px;
left: 50%;
transform: translate(-50%);
line-height: 118px;
text-align: center;
font-weight: 500;
font-size: 46px;
color: #FFFFFF;
letter-spacing:5px;
font-weight: 500;
font-size: 46px;
color: #FFFFFF;
letter-spacing: 5px;
background: v-bind('imageUrls.startBg') center/cover;
}
......
......@@ -5,7 +5,7 @@ import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket'
import LoadingView from './views/LoadingView.vue'
import PlayingView from './views/PlayingView.vue'
import { $getWechat } from '@/commons/utils.ts'
import { $getWechat, $toast } from '@/commons/utils.ts'
import { playGame6Music, stopGame6Music } from '@/commons/music';
type GameView = 'loading' | 'playing'
......@@ -244,7 +244,7 @@ if (wechat) {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
alert('管理员关闭了该房间')
$toast('管理员关闭了该房间')
},
onRoomBack: () => {
resetToLoadingView()
......@@ -304,7 +304,7 @@ const rankCloseHandler = () => {
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br />
祝您游戏愉快!
</div>
<div class="rule-close" @click="showGameRuleHandler"></div>
<div class="rule-close" @click="showGameRuleHandler"></div>
</div>
</div>
</template>
......@@ -358,14 +358,15 @@ const rankCloseHandler = () => {
background: v-bind('imageUrls.rule') center / cover no-repeat;
width: 692px;
height: 797px;
margin-top: -20%;
transform: scale(0.5);
.rule-title {
text-align: center;
color: white;
font-weight: bold;
font-size: 22pt;
margin-top: 10px;
font-size: 26pt;
line-height: 50pt;
letter-spacing: 4px;
}
......
......@@ -77,18 +77,18 @@ onMounted(() => {
.game-desc {
position: absolute;
z-index: 9999999999;
top: 50%;
top: 40%;
left: var(--stage-viewport-left, 0);
display: flex;
width: 86px;
height: 256px;
width: 58px;
height: 158px;
padding-top: 10px;
align-items: center;
justify-content: center;
border-radius: 0 12px 12px 0;
background: rgba(40, 12, 8, 0.52);
color: white;
font-size: 32pt;
color: rgba(255, 255, 255, 0.9);
font-size: 25px;
letter-spacing: 10px;
line-height: 1.25;
text-align: center;
......
......@@ -23,17 +23,28 @@ export function stopAllMusic() {
backgroundMusic = null;
}
function playAudioTimes(audio: HTMLAudioElement, times: number) {
let count = 1
audio.currentTime = 0
audio.play()
audio.onended = () => {
if (count >= times) {
audio.onended = null
return
}
count++
audio.currentTime = 0
audio.play()
}
}
function getBackgroundMusic(music: string, force: boolean = true) {
if (force) { //
if (backgroundMusic) {
stopApplauseMusic();
stopGame1Music();
// stopGame2Music();
// stopGame3Music();
stopGame4Music();
stopGame5Music();
stopGame6Music();
backgroundMusic = null;
stopAllMusic();
}
}
if (!backgroundMusic) {
......@@ -47,13 +58,12 @@ function getBackgroundMusic(music: string, force: boolean = true) {
return backgroundMusic
}
//播放庆祝音乐
export async function playApplauseMusic() {
const audio = getBackgroundMusic(applause)
try {
await audio.play()
window.removeEventListener('pointerdown', playApplauseMusic)
window.removeEventListener('keydown', playApplauseMusic)
playAudioTimes(audio, 2); //播放两次停止
} catch {
window.addEventListener('pointerdown', playApplauseMusic, { once: false })
window.addEventListener('keydown', playApplauseMusic, { once: false })
......
......@@ -93,4 +93,233 @@ export function $query(q: string | string[]): string | string[] {
return q.map((key) => params.get(key)).filter((v): v is string => !!v);
}
return params.get(q) || '';
}
let toastElement: HTMLDivElement | undefined;
let toastTimer: ReturnType<typeof window.setTimeout> | undefined;
export function $toast(message?: string, duration = 2000) {
if (!message) {
return;
}
if (!toastElement) {
toastElement = document.createElement('div');
toastElement.style.cssText = [
'position: fixed',
'left: 50%',
'top: 45%',
'z-index: 99999',
'max-width: 560px',
'padding: 18px 28px',
'border-radius: 18px',
'background: rgba(0, 0, 0, 0.72)',
'color: #fff',
'font-size: 22px',
'line-height: 1.4',
'text-align: center',
'transform: translate(-50%, -40%)',
'opacity: 0',
'pointer-events: none',
'transition: opacity 0.2s ease, transform 0.2s ease',
].join(';');
document.body.appendChild(toastElement);
}
if (toastTimer) {
window.clearTimeout(toastTimer);
toastTimer = undefined;
}
toastElement.textContent = message;
toastElement.style.opacity = '1';
toastElement.style.transform = 'translate(-50%, -50%)';
toastTimer = window.setTimeout(() => {
if (!toastElement) {
return;
}
toastElement.style.opacity = '0';
toastElement.style.transform = 'translate(-50%, -40%)';
toastTimer = undefined;
}, duration);
}
type ConfirmOptions = {
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
}
let confirmMaskElement: HTMLDivElement | undefined;
let confirmBoxElement: HTMLDivElement | undefined;
let confirmTitleElement: HTMLDivElement | undefined;
let confirmMessageElement: HTMLDivElement | undefined;
let confirmCancelElement: HTMLButtonElement | undefined;
let confirmOkElement: HTMLButtonElement | undefined;
let confirmResolve: ((value: boolean) => void) | undefined;
let confirmHideTimer: ReturnType<typeof window.setTimeout> | undefined;
function closeConfirm(result: boolean) {
if (!confirmMaskElement || !confirmBoxElement) {
confirmResolve?.(result);
confirmResolve = undefined;
return;
}
if (confirmHideTimer) {
window.clearTimeout(confirmHideTimer);
confirmHideTimer = undefined;
}
confirmMaskElement.style.opacity = '0';
confirmBoxElement.style.transform = 'scale(0.96)';
confirmHideTimer = window.setTimeout(() => {
if (confirmMaskElement) {
confirmMaskElement.style.display = 'none';
}
confirmHideTimer = undefined;
}, 200);
confirmResolve?.(result);
confirmResolve = undefined;
}
function ensureConfirmElement() {
if (confirmMaskElement) {
return;
}
confirmMaskElement = document.createElement('div');
confirmMaskElement.style.cssText = [
'position: fixed',
'inset: 0',
'z-index: 99998',
'display: none',
'align-items: center',
'justify-content: center',
'background: rgba(0, 0, 0, 0.45)',
'opacity: 0',
'transition: opacity 0.2s ease',
].join(';');
confirmBoxElement = document.createElement('div');
confirmBoxElement.style.cssText = [
'width: min(560px, calc(100vw - 80px))',
'box-sizing: border-box',
'padding: 34px 32px 28px',
'border-radius: 22px',
'background: #fff',
'color: #333',
'font-size: 24px',
'line-height: 1.45',
'text-align: center',
'box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22)',
'transform: scale(0.96)',
'transition: transform 0.2s ease',
].join(';');
confirmTitleElement = document.createElement('div');
confirmTitleElement.style.cssText = [
'margin-bottom: 16px',
'font-size: 30px',
'font-weight: 700',
'color: #222',
].join(';');
confirmMessageElement = document.createElement('div');
confirmMessageElement.style.cssText = [
'min-height: 34px',
'word-break: break-word',
].join(';');
const buttonWrap = document.createElement('div');
buttonWrap.style.cssText = [
'display: flex',
'gap: 18px',
'margin-top: 30px',
].join(';');
confirmCancelElement = document.createElement('button');
confirmCancelElement.type = 'button';
confirmCancelElement.style.cssText = [
'flex: 1',
'height: 58px',
'border: 1px solid #ddd',
'border-radius: 29px',
'background: #fff',
'color: #666',
'font-size: 24px',
].join(';');
confirmCancelElement.onclick = () => closeConfirm(false);
confirmOkElement = document.createElement('button');
confirmOkElement.type = 'button';
confirmOkElement.style.cssText = [
'flex: 1',
'height: 58px',
'border: 0',
'border-radius: 29px',
'background: #AA0000',
'color: #fff',
'font-size: 24px',
].join(';');
confirmOkElement.onclick = () => closeConfirm(true);
confirmMaskElement.onclick = (event) => {
if (event.target === confirmMaskElement) {
closeConfirm(false);
}
};
buttonWrap.append(confirmCancelElement, confirmOkElement);
confirmBoxElement.append(confirmTitleElement, confirmMessageElement, buttonWrap);
confirmMaskElement.appendChild(confirmBoxElement);
document.body.appendChild(confirmMaskElement);
}
export function $confirm(message?: string, title?: string): Promise<boolean>;
export function $confirm(options?: ConfirmOptions): Promise<boolean>;
export function $confirm(messageOrOptions?: string | ConfirmOptions, title?: string): Promise<boolean> {
const options: ConfirmOptions = typeof messageOrOptions === 'object'
? messageOrOptions
: { message: messageOrOptions, title };
if (!options.message) {
return Promise.resolve(false);
}
ensureConfirmElement();
if (confirmResolve) {
confirmResolve(false);
confirmResolve = undefined;
}
if (confirmHideTimer) {
window.clearTimeout(confirmHideTimer);
confirmHideTimer = undefined;
}
confirmTitleElement!.textContent = options.title ?? '提示';
confirmMessageElement!.textContent = options.message;
confirmCancelElement!.textContent = options.cancelText ?? '取消';
confirmOkElement!.textContent = options.confirmText ?? '确定';
confirmMaskElement!.style.display = 'flex';
window.requestAnimationFrame(() => {
if (!confirmMaskElement || !confirmBoxElement) {
return;
}
confirmMaskElement.style.opacity = '1';
confirmBoxElement.style.transform = 'scale(1)';
});
return new Promise<boolean>((resolve) => {
confirmResolve = resolve;
});
}
\ No newline at end of file
......@@ -57,7 +57,7 @@ export function initSocket(args: SocketInitArgs): Socket | null {
socket.onAny((evt, payload) => {
const data = decode(payload);
if ($is_run_local()) {
console.log("recv<============", evt, data);
console.log("============>recv", evt, data);
}
messageHandlers.forEach((handler) => handler(evt, data));
});
......@@ -78,7 +78,7 @@ export function sendSocketMessage(cmd: string, data: any = {}): boolean {
return false;
}
if ($is_run_local()) {
console.log('================>send', cmd, data);
console.log('send<================', cmd, data);
}
socket.send(encode({ cmd, data }));
return true;
......
import CryptoJS from 'crypto-js'
import { onBeforeUnmount, onMounted } from 'vue'
import { $read_socket_storge, $remove_socket_storage } from '@/commons/utils'
import { $read_socket_storge, $remove_socket_storage, $toast } from '@/commons/utils'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
import router from '@/router'
......@@ -64,21 +64,18 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
const { msg, state, data } = payload
if (state === 0) {
// if (evt === 'game_start_result' && data === 1) {
// return
// }
if (evt == 'login_result') {
alert(msg)
// alert(msg)
// options.onRelogin?.()
socket?.close();
$toast(msg);
$remove_socket_storage();
await router.replace(`/login`);
location.href = location.href;
return;
}
else if (evt == 'new_admin_result') {
alert(msg);
socket?.close();
// alert(msg);
$toast(msg);
$remove_socket_storage();
await router.replace(`/login`);
location.href = location.href;
......
<script setup lang="ts">
import { cssAssetUrl } from '@/commons/assets';
import { $save } from '@/commons/utils.ts';
import { $save, $toast } from '@/commons/utils.ts';
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
......@@ -9,9 +9,9 @@ const username = ref('')
const password = ref('')
const imageUrls = {
bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'),
bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'),
}
async function login() {
......@@ -25,10 +25,11 @@ async function login() {
})
const data = await res.json()
// if (!res.ok || data.state !== 1) {
// alert(data.msg || 'Login failed')
// throw new Error(data.msg || 'Login failed')
// }
if (!res.ok || data.state !== 1) {
$toast(data.msg)
//alert(data.msg || 'Login failed')
// throw new Error(data.msg || 'Login failed')
}
return data
}
......@@ -36,13 +37,15 @@ const loginHandler = async () => {
const { state, msg, data } = await login()
if (state === 0) {
alert(msg)
// alert(msg)
$toast(msg)
return
}
for (let i in data) {
$save(i, data[i]);
}
alert('登录成功')
// alert('登录成功')
$toast('登录成功')
await router.replace(`/main`);
}
......@@ -77,34 +80,33 @@ onMounted(() => {
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg1') center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg1') center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
.img-logo {
background: v-bind('imageUrls.logo');
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-logo {
background: v-bind('imageUrls.logo');
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title1 {
background: v-bind('imageUrls.title1') center / cover no-repeat;
width: 1024px;
height: 143px;
margin: 0 auto;
position: relative;
top: 50px;
}
.img-title1 {
background: v-bind('imageUrls.title1') center / cover no-repeat;
width: 1024px;
height: 143px;
margin: 0 auto;
position: relative;
top: 50px;
}
}
.login-page {
......
......@@ -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 { $remove_socket_storage } from '@/commons/utils.ts'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts'
const router = useRouter()
......@@ -155,7 +155,8 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
const startGameWithMusic = () => {
if (playerCount.value == 0) {
alert('房间中没有玩家')
// alert('房间中没有玩家')
$toast('房间中没有玩家');
return;
}
startGame()
......@@ -171,10 +172,16 @@ const nextRoundWithMusic = () => {
nextRound()
}
const backHandler = () => {
if (confirm('是否关闭当前房间回到首页')) {
const backHandler = async () => {
const ok = await $confirm({
title: '提示',
message: '是否关闭当前房间回到首页',
confirmText: '确定',
cancelText: '取消',
})
if (ok) {
closeRoom();
stopGame1Music()
stopAllMusic()
router.replace('/main');
}
}
......@@ -191,9 +198,9 @@ onUnmounted(() => {
watch(screen, (value) => {
// console.log(value);
if(value == 'rank2'){
if (value == 'rank2') {
playApplauseMusic();
}else if(value == 'loading'){
} else if (value == 'loading') {
playGame1Music();
}
})
......
......@@ -152,11 +152,10 @@ const renderAvatar = (item: any | null) => {
.podium {
background: v-bind('imageUrls.podium') no-repeat center;
width: 1347px;
height: 269px;
width: 1920px;
height: 740px;
position: absolute;
top: 427px;
left: 287px;
bottom: 0;
.gu-burst {
width: 200px;
......@@ -174,6 +173,8 @@ const renderAvatar = (item: any | null) => {
.rank-no1 {
position: relative;
left: 308px;
top: 150px;
.gu-burst {
left: 580px;
......@@ -207,7 +208,8 @@ const renderAvatar = (item: any | null) => {
.rank-no2 {
position: relative;
left: 308px;
top: 150px;
.gu-burst {
left: 140px;
top: -170px;
......@@ -232,7 +234,7 @@ const renderAvatar = (item: any | null) => {
text-align: center;
line-height: 46px;
position: absolute;
left: 157px;
left: 165px;
top: 2px;
z-index: 1;
border-radius: 23px;
......@@ -241,7 +243,8 @@ const renderAvatar = (item: any | null) => {
.rank-no3 {
position: relative;
left: 308px;
top: 150px;
.gu-burst {
left: 1020px;
top: -170px;
......@@ -280,8 +283,8 @@ const renderAvatar = (item: any | null) => {
height: 238px;
background: v-bind('imageUrls.vip');
position: absolute;
left: 171px;
top: -200px;
left: 480px;
top: -50px;
}
.rank-container {
......@@ -324,19 +327,21 @@ const renderAvatar = (item: any | null) => {
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation');
transform-origin: center center;
background: v-bind('imageUrls.avatarAnimation') center / cover no-repeat;
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
position: relative;
top: 6px;
position: absolute;
left: 50%;
top: 50%;
z-index: 1;
margin: 0 auto;
width: 60px;
height: 60px;
border-radius: 30px;
background: v-bind('imageUrls.avatar');
transform: translate(-50%, -50%);
background: v-bind('imageUrls.avatar') center / cover no-repeat;
}
.txt-nickname {
......
......@@ -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 { $remove_socket_storage } from "@/commons/utils.ts";
import { $confirm, $remove_socket_storage, $toast } from "@/commons/utils.ts";
const router = useRouter()
......@@ -194,7 +194,8 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
const startGameWithMusic = () => {
if (playerCount.value == 0) {
alert('房间中没有玩家')
// alert('房间中没有玩家')
$toast('房间中没有玩家');
return;
}
startGame()
......@@ -210,10 +211,16 @@ const nextRoundWithMusic = () => {
nextRound()
}
const backHandler = () => {
if (confirm('是否关闭当前房间回到首页')) {
const backHandler = async() => {
const ok = await $confirm({
title: '提示',
message: '是否关闭当前房间回到首页',
confirmText: '确定',
cancelText: '取消',
})
if (ok) {
closeRoom();
stopGame4Music()
stopAllMusic()
router.replace('/main');
}
}
......
......@@ -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 { $remove_socket_storage } from '@/commons/utils.ts'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts'
const router = useRouter()
......@@ -163,7 +163,8 @@ const { startGame, createRoom, closeRoom, backRoom } = useAdminGameSocket({
const startGameWithMusic = () => {
if (playerCount.value == 0) {
alert('房间中没有玩家')
// alert('房间中没有玩家')
$toast('房间中没有玩家');
return;
}
playGame5Music()
......@@ -191,10 +192,16 @@ onUnmounted(() => {
window.removeEventListener('beforeunload', confirmRefresh)
})
const backHandler = () => {
if (confirm('是否关闭当前房间回到首页')) {
const backHandler = async() => {
const ok = await $confirm({
title: '提示',
message: '是否关闭当前房间回到首页',
confirmText: '确定',
cancelText: '取消',
})
if (ok) {
closeRoom();
stopGame5Music()
stopAllMusic()
router.replace('/main');
}
}
......
......@@ -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 { $remove_socket_storage } from '@/commons/utils.ts'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts'
const router = useRouter()
......@@ -153,7 +153,8 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
const startGameWithMusic = () => {
if (playerCount.value == 0) {
alert('房间中没有玩家')
// alert('房间中没有玩家')
$toast('房间中没有玩家');
return;
}
startGame()
......@@ -169,10 +170,16 @@ const nextRoundWithMusic = () => {
nextRound()
}
const backHandler = () => {
if (confirm('是否关闭当前房间回到首页')) {
const backHandler = async() => {
const ok = await $confirm({
title: '提示',
message: '是否关闭当前房间回到首页',
confirmText: '确定',
cancelText: '取消',
})
if (ok) {
closeRoom();
stopGame6Music()
stopAllMusic()
router.replace('/main');
}
}
......
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