Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
F
fc-minigame
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
陈冲
fc-minigame
Commits
2912c668
Commit
2912c668
authored
Jun 03, 2026
by
陈冲
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
commit
parent
8a5923a5
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
153 additions
and
55 deletions
+153
-55
game_mysql.sql
backend/deploy/game_mysql.sql
+2
-2
mod.rs
backend/src/db/mod.rs
+1
-1
mysql.rs
backend/src/db/mysql.rs
+9
-2
socket_io.rs
backend/src/protocols/socket_io.rs
+16
-5
env.d.ts
frontend-h5/env.d.ts
+1
-0
useGameSocket.ts
frontend-h5/src/composables/useGameSocket.ts
+6
-1
Game1.vue
frontend-h5/src/pages/game1/Game1.vue
+25
-1
PlayingView.vue
frontend-h5/src/pages/game1/views/PlayingView.vue
+2
-0
ScoreView.vue
frontend-h5/src/pages/game1/views/ScoreView.vue
+24
-13
utils.ts
frontend-large/src/commons/utils.ts
+0
-8
useAdminGameSocket.ts
frontend-large/src/composables/useAdminGameSocket.ts
+6
-9
Game1.vue
frontend-large/src/pages/game1/Game1.vue
+46
-9
Rank1View.vue
frontend-large/src/pages/game1/views/Rank1View.vue
+15
-4
No files found.
backend/deploy/game_mysql.sql
View file @
2912c668
DROP
TABLE
IF
EXISTS
tb_game
;
CREATE
TABLE
tb_game
(
wechat_id
VARCHAR
(
50
)
,
wechat_id
text
,
nickname
VARCHAR
(
50
),
avatar
VARCHAR
(
200
)
,
avatar
text
,
game_rank
INT
NOT
NULL
,
item_num
INT
NOT
NULL
,
score
INT
NOT
NULL
DEFAULT
0
,
...
...
backend/src/db/mod.rs
View file @
2912c668
...
...
@@ -5,8 +5,8 @@ use async_trait::async_trait;
use
serde_json
::
Value
;
use
tokio
::
sync
::
OnceCell
;
pub
mod
mysql
;
pub
mod
models
;
pub
mod
mysql
;
pub
mod
pg
;
#[async_trait]
...
...
backend/src/db/mysql.rs
View file @
2912c668
...
...
@@ -3,7 +3,7 @@ use crate::db::DbDriver;
use
async_trait
::
async_trait
;
use
serde_json
::{
Value
,
json
};
use
sqlx
::
mysql
::{
MySqlArguments
,
MySqlPoolOptions
,
MySqlRow
};
use
sqlx
::{
Column
,
MySql
,
MySqlPool
,
Row
,
TypeInfo
};
use
sqlx
::{
Column
,
MySql
,
MySql
Connection
,
MySql
Pool
,
Row
,
TypeInfo
};
pub
struct
MysqlDb
{
pool
:
MySqlPool
,
...
...
@@ -12,11 +12,18 @@ pub struct MysqlDb {
impl
MysqlDb
{
pub
async
fn
new
()
->
Result
<
Self
,
String
>
{
let
pool
=
MySqlPoolOptions
::
new
()
.after_connect
(|
conn
:
&
mut
MySqlConnection
,
_
|
{
Box
::
pin
(
async
move
{
sqlx
::
query
(
"SET time_zone = '+08:00'"
)
.execute
(
conn
)
.await
?
;
Ok
(())
})
})
.max_connections
(
16
)
.connect
(
&
config
.db.uri
)
.await
.map_err
(|
err
|
err
.to_string
())
?
;
Ok
(
Self
{
pool
})
}
}
...
...
backend/src/protocols/socket_io.rs
View file @
2912c668
...
...
@@ -9,12 +9,12 @@ use serde_json::{Value, json};
use
socketioxide
::
SocketIo
;
use
socketioxide
::
extract
::{
Data
,
SocketRef
};
use
socketioxide
::
socket
::
DisconnectReason
;
use
sqlx
::
encode
::
IsNull
::
No
;
use
std
::
collections
::
HashMap
;
use
std
::
sync
::
atomic
::{
AtomicU64
,
Ordering
};
use
std
::
time
::
Duration
;
lazy_static
::
lazy_static!
{
/// 可以有多个管理员,但不要用同一个账户登录,否则后面的没效果
static
ref
CLIENT_ADMIN_MAP
:
DashMap
<
String
,
SocketRef
>
=
DashMap
::
new
();
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
());
...
...
@@ -297,7 +297,7 @@ fn spawn_game_timeout(round_id: u64, secs: u64) {
if
let
Ok
(
mut
state
)
=
ROOM_STATE
.lock
()
{
state
.status
=
RoomStatus
::
Submit
;
dbg!
(
"submit"
);
//
dbg!("submit");
drop
(
state
);
}
});
...
...
@@ -322,25 +322,30 @@ async fn handle_room_command(
)
{
let
evt
=
&
format!
(
"{cmd}_result"
);
match
cmd
{
"room_create"
=>
{
"room_create"
|
"room_back"
=>
{
//room_back返回当前游戏的Loading状态
// 只有管理员可以创建房间
if
!
is_admin
{
return
;
}
if
let
Some
(
index
)
=
data
.as_i64
()
{
if
let
Ok
(
mut
state
)
=
ROOM_STATE
.lock
()
{
if
cmd
==
"room_create"
{
if
state
.status
!=
RoomStatus
::
Closed
{
drop
(
state
);
emit_msgpack
(
&
socket
,
evt
,
&
err_resp
(
"不能重复创建房间"
,
None
));
return
;
}
}
state
.reset
();
state
.status
=
RoomStatus
::
Start
;
// 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
))));
}
}
}
}
...
...
@@ -504,7 +509,8 @@ async fn handle_room_command(
return
;
}
if
let
Ok
(
state
)
=
ROOM_STATE
.lock
()
{
if
!
matches!
(
state
.status
,
RoomStatus
::
Submit
)
{
//防止客户端提交的状态还在Running中
if
!
matches!
(
state
.status
,
RoomStatus
::
Submit
|
RoomStatus
::
Running
)
{
drop
(
state
);
return
;
}
...
...
@@ -616,6 +622,11 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
if
is_admin
{
match
CLIENT_ADMIN_MAP
.entry
(
userid
.clone
())
{
Entry
::
Occupied
(
_
)
=>
{
emit_msgpack
(
&
socket
,
"login_result"
,
&
err_resp
(
"当前管理员已登录,不能重复登录"
,
None
),
);
_
=
socket
.disconnect
();
return
;
}
...
...
frontend-h5/env.d.ts
View file @
2912c668
/// <reference types="vite/client" />
declare
module
'crypto-js'
;
declare
module
'crypto-js/sha256'
;
declare
const
__APP_DOMAIN__
:
string
;
...
...
frontend-h5/src/composables/useGameSocket.ts
View file @
2912c668
...
...
@@ -19,6 +19,7 @@ type GameSocketOptions = {
onRescoreSubmitted
?:
(
recount
:
number
)
=>
void
onRoomClosed
?:
(
data
:
any
)
=>
void
onEvent
?:
(
evt
:
string
,
payload
:
SocketPayload
)
=>
void
onRoomBack
?:
(
data
:
any
)
=>
void
}
function
getGameIndex
(
data
:
any
)
{
...
...
@@ -122,11 +123,15 @@ export function useGameSocket(options: GameSocketOptions) {
options
.
onScoreSubmitted
?.(
true
,
data
)
break
}
case
'room_close'
:
{
case
'room_close
_result
'
:
{
userJoinStatus
.
value
=
false
options
.
onRoomClosed
?.(
data
)
break
}
case
'room_back_result'
:
{
options
.
onRoomBack
?.(
data
);
break
;
}
}
options
.
onEvent
?.(
evt
,
payload
)
...
...
frontend-h5/src/pages/game1/Game1.vue
View file @
2912c668
...
...
@@ -107,6 +107,22 @@ function showLoadingView() {
setDivDescVisible
(
false
)
}
function
resetToLoadingView
()
{
stopGameCountdown
()
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
)
guFrameTimer
=
undefined
}
tick
.
value
=
0
rank
.
value
=
0
countdownInterval
.
value
=
60
currentGu
.
value
=
guFrames
[
0
]
isGuPlaying
.
value
=
false
userJoinStatus
.
value
=
false
currentView
.
value
=
'loading'
setDivDescVisible
(
false
)
}
function
startGameView
()
{
stopGameCountdown
()
tick
.
value
=
0
...
...
@@ -126,8 +142,15 @@ function replayGame() {
}
function
backToWaiting
()
{
showLoadingView
()
resetToLoadingView
()
joinSharedRoom
(
nickname
.
value
,
token
.
value
,
avatar
.
value
)
}
function
handleRoomBack
()
{
resetToLoadingView
()
window
.
setTimeout
(()
=>
{
joinSharedRoom
(
nickname
.
value
,
token
.
value
,
avatar
.
value
)
},
200
)
}
const
touchHandler
=
()
=>
{
...
...
@@ -186,6 +209,7 @@ if (wechat) {
submitScore
(
true
);
},
onRoomClosed
:
showLoadingView
,
onRoomBack
:
handleRoomBack
,
})
_offSocketMessage
.
value
=
offSocketMessage
}
...
...
frontend-h5/src/pages/game1/views/PlayingView.vue
View file @
2912c668
...
...
@@ -120,6 +120,8 @@ defineEmits<{
.play-content
{
margin-top
:
240px
;
margin-left
:
40px
;
margin-right
:
40px
;
padding
:
30px
0
;
border-radius
:
25px
;
background
:
rgba
(
40
,
12
,
8
,
0.52
);
...
...
frontend-h5/src/pages/game1/views/ScoreView.vue
View file @
2912c668
<
script
setup
lang=
"ts"
>
import
{
$getWechat
}
from
'@/commons/utils'
;
import
{
onMounted
,
ref
}
from
'vue'
;
defineProps
<
{
imageUrls
:
Record
<
string
,
string
>
tick
:
number
...
...
@@ -9,6 +12,13 @@ defineEmits<{
replay
:
[]
back
:
[]
}
>
()
const
nickname
=
ref
(
''
);
const
avatar
=
ref
(
''
);
onMounted
(()
=>
{
const
wechat
=
$getWechat
();
nickname
.
value
=
wechat
.
nickname
;
avatar
.
value
=
wechat
.
avatar
;
});
</
script
>
<
template
>
...
...
@@ -16,9 +26,10 @@ defineEmits<{
<div
class=
"img-logo"
></div>
<div
class=
"bg-bottom"
></div>
<div
class=
"img-score"
>
<div
class=
"score-avatar"
></div>
<div
class=
"score-avatar"
:style=
"`background: url($
{avatar});border-radius:66px;width:132px;height:132px;`">
</div>
<div
class=
"score-content"
>
<div>
张小明
</div>
<div>
{{
nickname
}}
</div>
<div>
您的成绩
</div>
<div>
击鼓次数(次)
</div>
<div>
{{
tick
}}
</div>
...
...
@@ -73,12 +84,12 @@ defineEmits<{
.score-avatar
{
position
:
absolute
;
top
:
-
2
0px
;
top
:
-
5
0px
;
left
:
50%
;
width
:
60px
;
height
:
60px
;
background
:
v-bind
(
'imageUrls.avatar'
)
center
/
cover
;
transform
:
translateX
(
-50%
)
scale
(
2
)
;
transform
:
translateX
(
-50%
);
}
.score-content
{
...
...
@@ -91,41 +102,41 @@ defineEmits<{
text-align
:
center
;
}
.score-content
>
div
:nth-child
(
1
)
{
.score-content
>
div
:nth-child
(
1
)
{
color
:
#E00000
;
font-size
:
32pt
;
font-weight
:
bold
;
}
.score-content
>
div
:nth-child
(
2
)
{
.score-content
>
div
:nth-child
(
2
)
{
margin-top
:
20px
;
font-size
:
32pt
;
}
.score-content
>
div
:nth-child
(
3
)
{
.score-content
>
div
:nth-child
(
3
)
{
margin-top
:
40px
;
}
.score-content
>
div
:nth-child
(
4
)
{
.score-content
>
div
:nth-child
(
4
)
{
margin-top
:
30px
;
color
:
#E00000
;
font-size
:
32pt
;
font-weight
:
bold
;
transform
:
scale
Y
(
2.4
);
/* font-weight: bold; */
transform
:
scale
(
1.5
);
}
.score-content
>
div
:nth-child
(
5
)
{
.score-content
>
div
:nth-child
(
5
)
{
margin-top
:
40px
;
}
.score-content
>
div
:nth-child
(
6
)
{
.score-content
>
div
:nth-child
(
6
)
{
margin-top
:
20px
;
color
:
#E00000
;
font-size
:
32pt
;
font-weight
:
bold
;
}
.score-content
>
div
:nth-child
(
6
)
label
{
.score-content
>
div
:nth-child
(
6
)
label
{
color
:
#E00000
;
font-size
:
42pt
;
margin
:
0
10px
;
...
...
frontend-large/src/commons/utils.ts
View file @
2912c668
...
...
@@ -53,14 +53,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto
return
defaultValue
??
null
;
}
}
export
function
$goto_login
(
evt
:
string
,
router
:
any
,
path
:
string
):
boolean
{
if
(
evt
==
"login_result"
)
{
$remove_socket_storage
();
router
.
replace
(
path
).
then
(()
=>
{
});
return
true
;
}
return
false
;
}
export
function
$remove_socket_storage
():
void
{
localStorage
.
removeItem
(
'passphrase'
);
...
...
frontend-large/src/composables/useAdminGameSocket.ts
View file @
2912c668
import
CryptoJS
from
'crypto-js'
import
{
onBeforeUnmount
,
onMounted
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
import
{
$goto_login
,
$read_socket_storge
}
from
'@/commons/utils'
import
{
$read_socket_storge
}
from
'@/commons/utils'
import
{
initSocket
,
onSocketMessage
,
sendSocketMessage
}
from
'@/commons/ws'
type
SocketPayload
=
{
...
...
@@ -22,7 +21,6 @@ type AdminGameSocketOptions = {
}
export
function
useAdminGameSocket
(
options
:
AdminGameSocketOptions
)
{
const
router
=
useRouter
()
let
offSocketMessage
:
(()
=>
void
)
|
undefined
function
requestRoomState
()
{
...
...
@@ -32,6 +30,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
function
createRoom
(
index
:
number
)
{
return
sendSocketMessage
(
'room_create'
,
index
)
}
function
backRoom
(
index
:
number
)
{
return
sendSocketMessage
(
'room_back'
,
index
)
}
function
startGame
()
{
return
sendSocketMessage
(
'game_start'
,
options
.
gameId
)
...
...
@@ -45,7 +46,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
const
{
passphrase
,
token
,
socket_path
,
client_ns
,
domain
}
=
$read_socket_storge
()
initSocket
({
socket_path
:
`
${
domain
}${
socket_path
}
`
,
client_ns
:
"/ws"
,
client_ns
:
"/ws"
,
auth
:
{
userid
:
token
,
validuser
:
CryptoJS
.
AES
.
encrypt
(
token
,
passphrase
).
toString
(),
...
...
@@ -58,16 +59,11 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
offSocketMessage
=
onSocketMessage
(
async
(
evt
,
payload
:
SocketPayload
)
=>
{
const
{
msg
,
state
,
data
}
=
payload
// if ($goto_login(evt, router, options.loginRedirectPath ?? '/game1')) {
// return
// }
if
(
state
===
0
)
{
if
(
evt
===
'game_start_result'
&&
data
===
1
)
{
options
.
onGameStartRejected
?.(
data
)
return
}
alert
(
msg
)
return
}
...
...
@@ -104,6 +100,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
return
{
requestRoomState
,
createRoom
,
backRoom
,
startGame
,
sendAdminMessage
,
}
...
...
frontend-large/src/pages/game1/Game1.vue
View file @
2912c668
...
...
@@ -59,7 +59,43 @@ const updateRankPlayers = (data: any) => {
)
}
const
gotoLoading
=
()
=>
{
const
playerToRankPlayer
=
(
player
:
any
,
index
:
number
):
Player
=>
({
active
:
player
?.
active
??
false
,
telephone
:
player
?.
telephone
??
''
,
userid
:
player
?.
userid
??
''
,
wechat_id
:
player
?.
wechat_id
||
`empty-
${
index
}
`
,
nickname
:
player
?.
nickname
||
'铏氫綅浠ュ緟'
,
avatar
:
player
?.
avatar
||
''
,
score
:
Number
(
player
?.
score
??
0
),
})
const
seedRankPlayersFromLoading
=
()
=>
{
rankPlayers
.
value
=
Array
.
from
(
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
{
const
player
=
loadingPlayers
.
value
[
index
]
if
(
!
player
?.
userid
&&
!
player
?.
wechat_id
)
{
return
emptyRankPlayer
(
index
)
}
return
playerToRankPlayer
(
player
,
index
)
},
)
}
const
resetRoundState
=
()
=>
{
playerCount
.
value
=
0
loadingPlayers
.
value
=
Array
.
from
({
length
:
PLAYER_LIMIT
},
emptyLoadingPlayer
)
rankPlayers
.
value
=
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
emptyRankPlayer
(
index
))
isPlayingStartAnimation
=
false
}
const
gotoLoading
=
(
reset
=
false
)
=>
{
if
(
reset
)
{
resetRoundState
()
}
screen
.
value
=
'loading'
}
...
...
@@ -67,6 +103,7 @@ const gotoRank1WithIntro = async () => {
if
(
isPlayingStartAnimation
)
return
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
screen
.
value
=
'rank1'
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
...
...
@@ -81,13 +118,14 @@ function handleRoomState(data: any) {
switch
(
data
?.
status
)
{
case
0
:
gotoLoading
()
gotoLoading
(
true
)
createRoom
(
1
)
break
case
1
:
gotoLoading
()
break
case
2
:
seedRankPlayersFromLoading
()
screen
.
value
=
'rank1'
break
case
3
:
...
...
@@ -96,7 +134,7 @@ function handleRoomState(data: any) {
}
}
const
{
startGame
,
createRoom
,
requestRoomState
}
=
useAdminGameSocket
({
const
{
startGame
,
createRoom
,
backRoom
}
=
useAdminGameSocket
({
gameId
:
'game1'
,
// loginRedirectPath: '/game1',
onGameStartRejected
:
gotoLoading
,
...
...
@@ -115,11 +153,9 @@ const startGameWithMusic = () => {
}
const
nextRound
=
()
=>
{
gotoLoading
()
createRoom
(
1
)
setTimeout
(()
=>
{
requestRoomState
()
},
200
)
gotoLoading
(
true
)
//重开房间
backRoom
(
1
)
}
const
nextRoundWithMusic
=
()
=>
{
...
...
@@ -138,7 +174,8 @@ onUnmounted(() => {
</
script
>
<
template
>
<Loading
v-if=
"screen === 'loading'"
:players=
"loadingPlayers"
:player-count=
"playerCount"
@
start=
"startGameWithMusic"
/>
<Loading
v-if=
"screen === 'loading'"
:players=
"loadingPlayers"
:player-count=
"playerCount"
@
start=
"startGameWithMusic"
/>
<Rank1
v-else-if=
"screen === 'rank1'"
ref=
"rank1Ref"
:rank-list=
"rankPlayers"
@
start=
"startGameWithMusic"
/>
<Rank2
v-else
:rank-list=
"rankPlayers"
@
next=
"nextRoundWithMusic"
/>
</
template
>
frontend-large/src/pages/game1/views/Rank1View.vue
View file @
2912c668
...
...
@@ -50,13 +50,13 @@ const imageUrls = {
rankNo1Icon
:
cssAssetUrl
(
'game1/no1-icon.png'
),
rankNo2Icon
:
cssAssetUrl
(
'game1/no2-icon.png'
),
rankNo3Icon
:
cssAssetUrl
(
'game1/no3-icon.png'
),
avatar
:
cssA
ssetUrl
(
'avatar.png'
),
avatar
:
a
ssetUrl
(
'avatar.png'
),
};
const
emptyPlayer
=
(
index
:
number
):
Player
=>
({
wechat_id
:
`empty-
${
index
}
`
,
nickname
:
'-'
,
avatar
:
""
,
avatar
:
''
,
score
:
0
,
});
...
...
@@ -232,6 +232,15 @@ const startIntro = async () => {
defineExpose
({
startIntro
,
});
const
avatarStyle
=
(
item
:
Player
)
=>
{
const
avatar
=
item
.
avatar
||
imageUrls
.
avatar
return
{
backgroundImage
:
`url("
${
avatar
}
")`
,
}
}
</
script
>
<
template
>
...
...
@@ -259,7 +268,7 @@ defineExpose({
<div
v-for=
"(item, index) in list"
:key=
"playerKey(item, index)"
class=
"rank-item"
:class=
"[rankEffects[playerKey(item, index)], index
<
3
?
`
rank-top-
$
{index + 1}` : '']">
<div
class=
"rank-icon"
:key=
"`rank-icon-$
{index}`">
{{
index
>
2
?
index
+
1
:
''
}}
</div>
<div
class=
"rank-avatar"
></div>
<div
class=
"rank-avatar"
:style=
"avatarStyle(item)"
></div>
<div
class=
"rank-nickname"
>
{{
item
.
nickname
}}
</div>
<div
class=
"rank-score"
>
{{
item
.
score
??
0
}}
</div>
</div>
...
...
@@ -431,7 +440,9 @@ defineExpose({
}
.rank-avatar
{
background
:
v-bind
(
'imageUrls.avatar'
)
center
/
cover
no-repeat
;
background-position
:
center
;
background-repeat
:
no-repeat
;
background-size
:
cover
;
width
:
48px
;
height
:
48px
;
border-radius
:
24px
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment