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())?;
......
use crate::commons::utils::{self, CONFIG as config};
use crate::db::models::game::Game;
use crate::db::models::log::Log;
use crate::db::models::manager::*;
use bytes::Bytes;
use dashmap::DashMap;
......@@ -13,9 +14,15 @@ use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Default)]
struct AdminSession {
current: Option<(String, SocketRef)>,
version: u64,
}
lazy_static::lazy_static! {
/// 当前唯一在线管理员:(token, socket)。
static ref CLIENT_ADMIN_SESSION: std::sync::Mutex<Option<(String, SocketRef)>> = std::sync::Mutex::new(None);
static ref CLIENT_ADMIN_SESSION: std::sync::Mutex<AdminSession> = std::sync::Mutex::new(AdminSession::default());
static ref CLIENT_PLAYER_MAP: DashMap<String, (i32,SocketRef)> = DashMap::new(); // 1: 正在游戏中
static ref ROOM_STATE: std::sync::Mutex<RoomState> = std::sync::Mutex::new(RoomState::default());
}
......@@ -29,6 +36,8 @@ const ROOM_SCORE_RANK_LIMIT: usize = 10;
const GAME_START_COUNTDOWN_SECS: u64 = 3;
const GAME_RESULT_WAIT_SECS: u64 = 2;
const GAME_RUNNING_SECS: u64 = 60;
//管理员退出5分钟后,关闭所有玩家的连接
const ADMIN_ABSENT_DEACTIVATE_PLAYER_SECS: u64 = 5 * 60;
fn get_game_tilte(key: &str) -> String {
let mut GAME_MAP = HashMap::new();
......@@ -70,7 +79,7 @@ struct RankingPlayer {
enum RoomStatus {
#[default]
Closed = 0, //关闭
Start, //开始排队
Loading, //开始排队
Running, //进行
Submit, //提交结果
Ended, //结束
......@@ -199,7 +208,7 @@ impl RoomState {
// fn snapshot(&self) -> Value {
// json!({
// "status": self.status.as_str(),
// "Start": self.status == RoomStatus::Start,
// "Loading": self.status == RoomStatus::Loading,
// "running": self.status == RoomStatus::Running,
// "ended": self.status == RoomStatus::Ended,
// "room_admin_count": get_admin_count(),
......@@ -220,7 +229,7 @@ impl RoomStatus {
fn as_str(self) -> &'static str {
match self {
RoomStatus::Closed => "closed",
RoomStatus::Start => "start",
RoomStatus::Loading => "loading",
RoomStatus::Running => "running",
RoomStatus::Ended => "ended",
RoomStatus::Submit => "submit",
......@@ -230,13 +239,16 @@ impl RoomStatus {
// 获取玩家数量,不统计管理员
pub fn get_player_count() -> usize {
CLIENT_PLAYER_MAP.len()
CLIENT_PLAYER_MAP
.iter()
.filter(|player| player.value().0 > 0)
.count()
}
// 获取管理员数量
pub fn get_admin_count() -> usize {
CLIENT_ADMIN_SESSION
.lock()
.map(|session| usize::from(session.is_some()))
.map(|session| usize::from(session.current.is_some()))
.unwrap_or(0)
}
......@@ -305,7 +317,7 @@ fn broadcast_msgpack_all_admin(event: &str, payload: &Value) {
let socket = CLIENT_ADMIN_SESSION
.lock()
.ok()
.and_then(|session| session.as_ref().map(|(_, socket)| socket.clone()));
.and_then(|session| session.current.as_ref().map(|(_, socket)| socket.clone()));
if let Some(socket) = socket {
_ = socket.emit(event, &payload);
}
......@@ -315,30 +327,57 @@ fn replace_current_admin(userid: String, socket: SocketRef) -> Result<Option<Soc
CLIENT_ADMIN_SESSION
.lock()
.map(|mut session| {
let old_socket = session.take().map(|(_, socket)| socket);
*session = Some((userid, socket));
let old_socket = session.current.take().map(|(_, socket)| socket);
session.current = Some((userid, socket));
session.version = session.version.wrapping_add(1);
old_socket
})
.map_err(|_| ())
}
fn clear_current_admin_if_match(userid: &str, socket: &SocketRef) -> bool {
fn clear_current_admin_if_match(userid: &str, socket: &SocketRef) -> Option<u64> {
CLIENT_ADMIN_SESSION
.lock()
.map(|mut session| {
let should_clear = session
.current
.as_ref()
.map(|(current_userid, current_socket)| {
current_userid == userid && current_socket.id == socket.id
})
.unwrap_or(false);
if should_clear {
*session = None;
session.current = None;
session.version = session.version.wrapping_add(1);
Some(session.version)
} else {
None
}
should_clear
})
.ok()
.flatten()
}
fn admin_session_still_empty(version: u64) -> bool {
CLIENT_ADMIN_SESSION
.lock()
.map(|session| session.current.is_none() && session.version == version)
.unwrap_or(false)
}
fn spawn_deactivate_players_if_admin_absent(version: u64) {
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(ADMIN_ABSENT_DEACTIVATE_PLAYER_SECS)).await;
if !admin_session_still_empty(version) {
return;
}
for mut player in CLIENT_PLAYER_MAP.iter_mut() {
player.value_mut().0 = 0;
}
});
}
/// 发送给所有管理员和玩家
fn broadcast_msgpack_all(event: &str, payload: &Value) {
broadcast_msgpack_all_admin(event, payload);
......@@ -375,6 +414,13 @@ fn emit_msgpack_to_user(userid: &str, event: &str, payload: &Value) {
}
}
fn is_current_player_socket(userid: &str, socket: &SocketRef) -> bool {
CLIENT_PLAYER_MAP
.get(userid)
.map(|player| player.1.id == socket.id)
.unwrap_or(false)
}
fn push_room_score_ranking() -> bool {
let (ranking, player_ranks) = if let Ok(state) = ROOM_STATE.lock() {
let full_ranking = state.full_score_ranking();
......@@ -534,16 +580,12 @@ async fn handle_room_command(
}
state.reset();
state.current_game_id = Some(game_id);
state.status = RoomStatus::Start;
state.status = RoomStatus::Loading;
// let snapshot = state.snapshot();
drop(state);
emit_msgpack(&socket, evt, &ok_resp(Some(json!(index))));
if cmd == "room_back" {
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(index))));
} else if cmd == "room_create" {
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(index))));
}
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(index))));
}
}
}
......@@ -574,58 +616,53 @@ async fn handle_room_command(
.to_string();
if let Ok(mut state) = ROOM_STATE.lock() {
// dbg!(
// &state.current_game_id.clone(),
// &state.status.clone().as_str()
// );
let Some(current_game_id) = state.current_game_id.clone() else {
drop(state);
return;
};
// 当前h5进入的不是大屏开启的游戏房间
if current_game_id != gameId {
if matches!(state.status, RoomStatus::Closed) {
drop(state);
emit_msgpack(
&socket,
evt,
&err_resp(
&format!("当前游戏 - {} 房间还未开启", get_game_tilte(&gameId)),
Some(json!(2)),
None,
),
);
return;
}
//游戏不是等待跟正在运行
if !matches!(state.status, RoomStatus::Start | RoomStatus::Running) {
drop(state);
emit_msgpack(
&socket,
evt,
&err_resp(
&format!(
"当前游戏 - {} 已经开始了,请在下一场游戏开始前进入",
get_game_tilte(&gameId)
} else if matches!(state.status, RoomStatus::Running) {
// 当前h5进入的不是大屏开启的游戏房间
if gameId != state.current_game_id.clone().unwrap_or_default() {
drop(state);
emit_msgpack(
&socket,
evt,
&err_resp(
&format!("当前游戏 - {} 房间还未开启", get_game_tilte(&gameId)),
None,
),
Some(json!(2)),
),
);
return;
}
let already_joined = state.players.contains_key(userid);
//玩家游戏过程中掉线重连
if already_joined {
if let Some(player) = state.players.get_mut(userid) {
player.online = true;
player.nickname = nickname.to_string();
player.avatar = avatar.to_string();
);
return;
}
let reconnect_payload = state.reconnect_payload(userid);
drop(state);
//如果是正在游戏中
let already_joined = state.players.contains_key(userid);
//玩家游戏过程中掉线重连
if already_joined {
if let Some(player) = state.players.get_mut(userid) {
player.online = true;
player.nickname = nickname.to_string();
player.avatar = avatar.to_string();
}
let reconnect_payload = state.reconnect_payload(userid);
drop(state);
if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(userid) {
player.0 = 1;
}
emit_msgpack(&socket, "room_recover_result", &ok_resp(reconnect_payload));
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, "room_recover_result", &ok_resp(reconnect_payload));
return;
}
let entry_order = state.next_entry_order;
......@@ -654,7 +691,7 @@ async fn handle_room_command(
if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(userid) {
player.0 = 1;
}
emit_msgpack(&socket, evt, &ok_resp(Some(json!(current_game_id))));
emit_msgpack(&socket, evt, &ok_resp(Some(json!(gameId))));
// 给所有管理员广播房间状态更新
broadcast_msgpack_all_admin(
evt,
......@@ -673,7 +710,7 @@ async fn handle_room_command(
emit_msgpack(&socket, evt, &err_resp("房间中没有玩家", Some(json!(1))));
return;
}
if state.status != RoomStatus::Start {
if state.status != RoomStatus::Loading {
drop(state);
emit_msgpack(&socket, evt, &err_resp("房间没有创建", Some(json!(2))));
return;
......@@ -708,7 +745,6 @@ async fn handle_room_command(
if !state.players.contains_key(userid) {
drop(state);
emit_msgpack(&socket, evt, &err_resp("您不在房间中", None));
_ = socket.clone().disconnect();
return;
} else if !matches!(state.status, RoomStatus::Running) {
drop(state);
......@@ -789,6 +825,23 @@ async fn handle_room_command(
} else {
(score, rank, Vec::new())
};
let action = async |msg: &str| {
let log = Log {
id: 0,
msg: msg.to_string(),
params: json!({
"wechat_id": &wechat.to_string(),
"nickname": &nickname.to_string(),
"avatar": &avatar.to_string(),
"item_num": item_num as i32,
"game_rank": rank as i32,
"score": score as i32,
}),
create_date: None,
};
return log.insert().await;
};
let game = Game {
wechat_id: wechat.to_string(),
nickname: nickname.to_string(),
......@@ -798,7 +851,20 @@ async fn handle_room_command(
score: score as i32,
create_date: None,
};
if game.insert().await {
//还需要提交给后台
// match utils::post(wechat, item_num).await {
// Ok(res) => {
// if let Some(msg) = res {
// // action(&msg).await;
// }
// }
// Err(err) => {
// dbg!("err", &err);
// }
// };
emit_msgpack(
&socket,
evt,
......@@ -936,19 +1002,24 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
}
} else {
if get_admin_count() == 0 {
match CLIENT_PLAYER_MAP.entry(userid.clone()) {
Entry::Occupied(mut entry) => {
entry.insert((0, socket.clone()));
}
Entry::Vacant(entry) => {
entry.insert((0, socket.clone()));
}
}
emit_msgpack(
&socket,
"connect_result",
&err_resp("游戏管理员暂未开启该房间", None),
);
_ = socket.disconnect();
return;
}
match CLIENT_PLAYER_MAP.entry(userid.clone()) {
Entry::Occupied(mut entry) => {
// 同一玩家重连时用新 socket 接管,避免网络抖动后无法回到本局。
let old_socket = entry.get().1.clone();
_ = old_socket.disconnect();
// 同一玩家重连时用新 socket 接管,旧 socket 后续消息会被忽略。
entry.insert((0, socket.clone()));
}
Entry::Vacant(entry) => {
......@@ -958,6 +1029,7 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
}
// 鉴权通过后,在这里挂玩家消息事件处理器。
let message_userid = userid.clone();
let message_is_admin = is_admin;
socket.on("message", move |socket: SocketRef, data: Data<Bytes>| {
let message_userid = message_userid.clone();
async move {
......@@ -971,7 +1043,10 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
if let (Some(cmd), Some(data)) =
(data.get("cmd").and_then(|v| v.as_str()), data.get("data"))
{
handle_room_command(&socket, &message_userid, is_admin, cmd, &data).await;
if !message_is_admin && !is_current_player_socket(&message_userid, &socket) {
return;
}
handle_room_command(&socket, &message_userid, message_is_admin, cmd, &data).await;
}
}
});
......@@ -980,15 +1055,18 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
move |socket: SocketRef, _reason: DisconnectReason| async move {
if !is_create_user {
if is_admin {
if !clear_current_admin_if_match(&userid, &socket) {
let Some(admin_session_version) =
clear_current_admin_if_match(&userid, &socket)
else {
return;
}
};
// 前端页面刷新时房间状态丢失体验会不好
if get_admin_count() == 0 {
if let Ok(mut state) = ROOM_STATE.lock() {
state.reset(); // 如果管理员都退出了,就把房间状态重置。
drop(state);
}
spawn_deactivate_players_if_admin_absent(admin_session_version);
}
} else {
let should_remove = CLIENT_PLAYER_MAP
......@@ -1000,7 +1078,7 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
}
CLIENT_PLAYER_MAP.remove(&userid);
if let Ok(mut state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Start {
if state.status != RoomStatus::Loading {
if let Some(player) = state.players.get_mut(&userid) {
player.online = false;
}
......@@ -1079,7 +1157,7 @@ pub async fn login(req: &mut Request, res: &mut Response) {
let game_id = state.current_game_id.clone().unwrap_or("".to_string());
if matches!(
state.status,
RoomStatus::Start | RoomStatus::Running | RoomStatus::Submit
RoomStatus::Loading | RoomStatus::Running | RoomStatus::Submit
) {
drop(state);
res.render(Json(err_resp(
......
......@@ -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