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
c1dbe1ba
Commit
c1dbe1ba
authored
Jun 06, 2026
by
陈冲
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' of
http://git.ruanyiit.com/chenchong/fc-minigame
parents
f4049020
7c0c20c7
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
209 additions
and
50 deletions
+209
-50
Game4.vue
frontend-h5/src/pages/game4/Game4.vue
+8
-6
PlayingView.vue
frontend-h5/src/pages/game4/views/PlayingView.vue
+162
-6
Game5.vue
frontend-h5/src/pages/game5/Game5.vue
+13
-32
PlayingView.vue
frontend-h5/src/pages/game5/views/PlayingView.vue
+0
-0
LoadingView.vue
frontend-large/src/pages/game4/views/LoadingView.vue
+8
-2
Game5.vue
frontend-large/src/pages/game5/Game5.vue
+14
-4
Rank1View.vue
frontend-large/src/pages/game5/views/Rank1View.vue
+4
-0
No files found.
frontend-h5/src/pages/game4/Game4.vue
View file @
c1dbe1ba
...
...
@@ -166,7 +166,7 @@ function handleRoomBack() {
}
const
touchHandler
=
()
=>
{
tick
.
value
+=
1
tick
.
value
+=
5
submitScore
()
}
...
...
@@ -232,11 +232,12 @@ onBeforeUnmount(() => {
<div
class=
"rule-title"
>
游戏规则
</div>
<div
class=
"rule-txt"
>
<!-- 微信用户通过扫描大屏幕二维码参与,可容纳多人同时进行,摇晃手机控制赛马奔跑,摇晃越快赛马跑得越快,游戏结束按分值高低先后排名,有机会获得新疆福彩定制周边礼品一份。 -->
1. 主持人开赛即可摇手机,单次摇晃得 5 分;
2. 得分越高大屏幕上面的马跑得越快;
3. 限时比拼摇速,结束后按积分排名;
4. 高分用户有机会领取新疆福彩定制周边;
5. 禁止外挂作弊,违规账号取消成绩与领奖资格;
1. 主持人开赛即可摇手机,单次摇晃得 5 分;
<br
/>
2. 得分越高大屏幕上面的马跑得越快;
<br
/>
3. 限时比拼摇速,结束后按积分排名;
<br
/>
4. 高分用户有机会领取新疆福彩定制周边;
<br
/>
5. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
祝您游戏愉快!
</div>
</div>
<div
class=
"rule-close"
>
...
...
@@ -246,6 +247,7 @@ onBeforeUnmount(() => {
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
<PlayingView
v-else-if=
"currentView === 'playing'"
:image-urls=
"imageUrls"
:tick=
"tick"
:rank=
"rank"
:currentHorse=
"currentHorse"
:currentYaoyiyao=
"currentYaoyiyao"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
touch=
"touchHandler"
/>
...
...
frontend-h5/src/pages/game4/views/PlayingView.vue
View file @
c1dbe1ba
<
script
setup
lang=
"ts"
>
import
{
onBeforeUnmount
,
onMounted
}
from
'vue'
import
{
onBeforeUnmount
,
onMounted
,
ref
}
from
'vue'
import
shakeSound
from
'@/assets/images/game4/yaoyiyao.mp3'
const
props
=
defineProps
<
{
...
...
@@ -16,6 +16,10 @@ const emit = defineEmits<{
touch
:
[]
}
>
()
// ========== iOS 设备检测 ==========
const
isIOS
=
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
===
'function'
const
showIOSHint
=
ref
(
isIOS
)
// ========== 摇一摇 ==========
const
shakeCooldown
=
300
let
lastShakeTime
=
0
...
...
@@ -33,6 +37,37 @@ let wasShaking = false
let
audioContext
:
AudioContext
|
null
=
null
let
shakeAudioBuffer
:
AudioBuffer
|
null
=
null
// iOS 13+/iPadOS 需要主动请求 DeviceMotion 权限
let
motionPermissionGranted
=
false
// 摇一摇图片抖动动画状态
const
isYaoyiyaoShaking
=
ref
(
false
)
let
yaoyiyaoShakeTimer
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
requestMotionPermission
=
async
():
Promise
<
boolean
>
=>
{
// 检查是否需要权限请求(iOS 12.2+)
if
(
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
===
'function'
)
{
try
{
const
state
=
await
(
DeviceMotionEvent
as
any
).
requestPermission
()
if
(
state
===
'granted'
)
{
console
.
log
(
'[摇一摇] iOS DeviceMotion 权限已授予'
)
motionPermissionGranted
=
true
// 权限拿到后再注册监听
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
return
true
}
console
.
warn
(
'[摇一摇] iOS DeviceMotion 权限被拒绝:'
,
state
)
return
false
}
catch
(
err
)
{
console
.
error
(
'[摇一摇] iOS 权限请求失败:'
,
err
)
return
false
}
}
// Android / 旧版 iOS:无需额外权限
motionPermissionGranted
=
true
return
true
}
const
initAudio
=
async
()
=>
{
try
{
audioContext
=
new
(
window
.
AudioContext
||
(
window
as
any
).
webkitAudioContext
)()
...
...
@@ -82,6 +117,13 @@ const handleDeviceMotion = (ev: DeviceMotionEvent) => {
if
(
now
-
lastShakeTime
<
shakeCooldown
)
return
lastShakeTime
=
now
// 摇一摇图片抖动动画
isYaoyiyaoShaking
.
value
=
true
if
(
yaoyiyaoShakeTimer
)
clearTimeout
(
yaoyiyaoShakeTimer
)
yaoyiyaoShakeTimer
=
setTimeout
(()
=>
{
isYaoyiyaoShaking
.
value
=
false
},
320
)
playShakeSound
()
// 摇晃时打印日志:输出将要通过 websocket 提交的数据
...
...
@@ -105,8 +147,6 @@ const handleDeviceMotion = (ev: DeviceMotionEvent) => {
}
}
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
// ========== 生命周期 ==========
onMounted
(
async
()
=>
{
...
...
@@ -115,6 +155,15 @@ onMounted(async () => {
}
catch
(
error
)
{
console
.
error
(
'初始化失败:'
,
error
)
}
// 非 iOS 设备直接注册监听;iOS 需等用户点击后通过 requestMotionPermission 注册
if
(
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
!==
'function'
)
{
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
motionPermissionGranted
=
true
console
.
log
(
'[摇一摇] DeviceMotion 监听已启动(Android/旧版iOS)'
)
}
else
{
console
.
log
(
'[摇一摇] 等待用户点击以请求 iOS DeviceMotion 权限…'
)
}
})
onBeforeUnmount
(()
=>
{
...
...
@@ -122,7 +171,20 @@ onBeforeUnmount(() => {
if
(
audioContext
)
{
audioContext
.
close
()
}
if
(
yaoyiyaoShakeTimer
)
{
clearTimeout
(
yaoyiyaoShakeTimer
)
}
})
// 点击马触发:iOS 先请求权限 + 触发射分
const
onTapHorse
=
async
()
=>
{
if
(
!
motionPermissionGranted
)
{
await
requestMotionPermission
()
// 权限请求完成(无论结果)隐藏 iOS 提示
showIOSHint
.
value
=
false
}
emit
(
'touch'
)
}
</
script
>
<
template
>
...
...
@@ -141,9 +203,19 @@ onBeforeUnmount(() => {
<div>
{{
tick
}}
</div>
<div>
当前排名:
第
<label>
{{
rank
}}
</label>
名
</div>
</div>
<!-- iOS 权限提示蒙层 -->
<Transition
name=
"ios-hint-fade"
>
<div
v-if=
"showIOSHint"
class=
"ios-hint-overlay"
@
click=
"onTapHorse"
>
<div
class=
"ios-hint-content"
>
<div
class=
"ios-hint-title"
>
👇点击下方赛马
</div>
<div
class=
"ios-hint-desc"
>
启用摇一摇功能开始游戏
</div>
<div
class=
"ios-hint-arrow"
>
⬇
</div>
</div>
</div>
</Transition>
<!--
<div
class=
"bg-bottom"
></div>
-->
<div
class=
"bg-yaoyiyao"
:style=
"
{ background: `${currentYaoyiyao} center/cover` }">
</div>
<div
class=
"play-horse"
:
style=
"
{ background: `${currentHorse} center/cover` }" @click="$emit('touch')
">
</div>
<div
class=
"bg-yaoyiyao"
:
class=
"
{ 'is-shaking': isYaoyiyaoShaking }" :
style="{ background: `${currentYaoyiyao} center/cover` }">
</div>
<div
class=
"play-horse"
:
class=
"
{ 'horse-clickable': showIOSHint }" :style="{ background: `${currentHorse} center/cover` }" @click="onTapHorse
">
</div>
</div>
</
template
>
...
...
@@ -297,14 +369,98 @@ onBeforeUnmount(() => {
width
:
514.51px
;
height
:
229px
;
transform
:
translateX
(
-50%
);
transition
:
transform
0.08s
ease-out
;
}
/* 摇一摇图片抖动动画 */
.bg-yaoyiyao.is-shaking
{
animation
:
yaoyiyao-shake
0.32s
ease-in-out
;
}
@keyframes
yaoyiyao-shake
{
0
%
{
transform
:
translateX
(
-50%
)
rotate
(
0deg
);
}
15
%
{
transform
:
translateX
(
-50%
)
rotate
(
-8deg
);
}
30
%
{
transform
:
translateX
(
-50%
)
rotate
(
8deg
);
}
45
%
{
transform
:
translateX
(
-50%
)
rotate
(
-6deg
);
}
60
%
{
transform
:
translateX
(
-50%
)
rotate
(
6deg
);
}
75
%
{
transform
:
translateX
(
-50%
)
rotate
(
-3deg
);
}
90
%
{
transform
:
translateX
(
-50%
)
rotate
(
3deg
);
}
100
%
{
transform
:
translateX
(
-50%
)
rotate
(
0deg
);
}
}
.play-horse
{
position
:
absolute
;
bottom
:
164px
;
left
:
50%
;
width
:
597px
;
height
:
679px
;
/* transform: translateX(-50%) scale(2.4); */
transform
:
translateX
(
-50%
);
}
/* 马匹点击引导动画 - 脉冲呼吸效果 */
.horse-clickable
{
animation
:
horse-pulse
1.2s
ease-in-out
infinite
;
}
@keyframes
horse-pulse
{
0
%,
100
%
{
filter
:
brightness
(
1
)
drop-shadow
(
0
0
8px
rgba
(
255
,
215
,
0
,
0
));
}
50
%
{
filter
:
brightness
(
1.12
)
drop-shadow
(
0
0
24px
rgba
(
255
,
215
,
0
,
0.6
));
}
}
/* ===== iOS 权限提示蒙层 ===== */
.ios-hint-overlay
{
position
:
absolute
;
top
:
0
;
bottom
:
0
;
left
:
var
(
--stage-viewport-left
,
0
);
width
:
var
(
--stage-viewport-width
,
750px
);
z-index
:
10
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
0
,
0
,
0
,
0.55
);
cursor
:
pointer
;
-webkit-tap-highlight-color
:
transparent
;
}
.ios-hint-content
{
text-align
:
center
;
color
:
#fff
;
animation
:
ios-hint-bounce
1.6s
ease-in-out
infinite
;
}
.ios-hint-title
{
font-size
:
36pt
;
font-weight
:
bold
;
margin-bottom
:
16px
;
text-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.5
);
}
.ios-hint-desc
{
font-size
:
22pt
;
opacity
:
0.85
;
margin-bottom
:
20px
;
text-shadow
:
0
1px
4px
rgba
(
0
,
0
,
0
,
0.5
);
}
.ios-hint-arrow
{
font-size
:
40pt
;
animation
:
ios-arrow-bounce
0.8s
ease-in-out
infinite
alternate
;
}
@keyframes
ios-hint-bounce
{
0
%,
100
%
{
transform
:
translateY
(
0
);
}
50
%
{
transform
:
translateY
(
-12px
);
}
}
@keyframes
ios-arrow-bounce
{
from
{
transform
:
translateY
(
0
);
opacity
:
1
;
}
to
{
transform
:
translateY
(
16px
);
opacity
:
0.3
;
}
}
.ios-hint-fade-enter-active
{
transition
:
opacity
0.4s
ease
;
}
.ios-hint-fade-leave-active
{
transition
:
opacity
0.3s
ease
;
}
.ios-hint-fade-enter-from
,
.ios-hint-fade-leave-to
{
opacity
:
0
;
}
</
style
>
frontend-h5/src/pages/game5/Game5.vue
View file @
c1dbe1ba
...
...
@@ -105,16 +105,20 @@ function submitScore(save: boolean = false) {
if
(
save
)
{
//item_num 游戏项目(1到6)
//wechat 原始token
sendGameMessage
(
'submit_score_save'
,
{
const
data
=
{
score
:
tick
.
value
,
wechat
:
wechat
.
token_origin
,
item_num
:
1
,
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
})
}
console
.
log
(
'[WebSocket] send submit_score_save:'
,
JSON
.
stringify
(
data
,
null
,
2
))
sendGameMessage
(
'submit_score_save'
,
data
)
}
else
{
sendGameMessage
(
'submit_score'
,
{
score
:
tick
.
value
})
const
data
=
{
score
:
tick
.
value
}
console
.
log
(
'[WebSocket] send submit_score:'
,
JSON
.
stringify
(
data
))
sendGameMessage
(
'submit_score'
,
data
)
}
}
...
...
@@ -191,34 +195,11 @@ function handleRoomBack() {
},
200
)
}
const
touchHandler
=
()
=>
{
if
(
isGuPlaying
.
value
)
{
return
}
tick
.
value
+=
5
const
hitHandler
=
(
score
:
number
)
=>
{
tick
.
value
+=
score
console
.
log
(
'[WebSocket] 圈中马!得分:'
,
score
,
'当前总分:'
,
tick
.
value
)
submitScore
()
playGame1Music
();
isGuPlaying
.
value
=
true
let
frameIndex
=
0
const
playNextFrame
=
()
=>
{
currentGu
.
value
=
guFrames
[
frameIndex
]
??
imageUrls
.
gu01
frameIndex
+=
1
if
(
frameIndex
>=
guFrames
.
length
)
{
guFrameTimer
=
window
.
setTimeout
(()
=>
{
currentGu
.
value
=
guFrames
[
0
]
isGuPlaying
.
value
=
false
guFrameTimer
=
undefined
},
200
)
return
}
guFrameTimer
=
window
.
setTimeout
(
playNextFrame
,
200
)
}
playNextFrame
()
playGame1Music
()
}
if
(
wechat
)
{
...
...
@@ -350,10 +331,10 @@ const rankCloseHandler = () => {
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
<PlayingView
v-else-if=
"currentView === 'playing'"
:image-urls=
"imageUrls"
:tick=
"tick"
:rank=
"rank"
:current-gu=
"currentGu"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
touch=
"touchHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
@
hit=
"hitHandler"
/>
<!-- <ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" /> -->
</MobileStage>
<div
v-else
style=
"text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"
>
...
...
frontend-h5/src/pages/game5/views/PlayingView.vue
View file @
c1dbe1ba
This diff is collapsed.
Click to expand it.
frontend-large/src/pages/game4/views/LoadingView.vue
View file @
c1dbe1ba
...
...
@@ -32,6 +32,13 @@ const imageUrls = {
const
isStartPressed
=
ref
(
false
);
const
renderAvatar
=
(
item
:
any
|
null
)
=>
{
if
(
item
&&
item
.
avatar
)
{
return
`background: url("
${
String
(
item
.
avatar
).
replace
(
/"/g
,
'
\\
"'
)}
") center / cover no-repeat;`
}
return
`background:
${
imageUrls
.
avatar
}
center / cover no-repeat;`
}
const
pressStartButton
=
()
=>
{
isStartPressed
.
value
=
true
;
emit
(
"start"
);
...
...
@@ -66,7 +73,7 @@ const releaseStartButton = () => {
>
<div>
<div
class=
"img-avatar-container"
>
<div
class=
"img-avatar"
:style=
"`
background:url('$
{item.avatar}') center / cover no-repeat;
`">
</div>
<div
class=
"img-avatar"
:style=
"`
$
{renderAvatar(item)}
`">
</div>
</div>
<div
class=
"txt-nickname"
>
{{
$format_str
(
item
.
nickname
,
12
)
}}
...
...
@@ -235,7 +242,6 @@ const releaseStartButton = () => {
width
:
60px
;
height
:
60px
;
border-radius
:
30px
;
background
:
v-bind
(
"imageUrls.avatar"
);
}
.txt-nickname
{
...
...
frontend-large/src/pages/game5/Game5.vue
View file @
c1dbe1ba
...
...
@@ -46,7 +46,9 @@ const confirmRefresh = (event: BeforeUnloadEvent) => {
const
renderPlayers
=
(
data
:
any
)
=>
{
const
players
=
Array
.
isArray
(
data
?.
list
)
?
data
.
list
:
[]
playerCount
.
value
=
Number
(
data
?.
count
??
players
.
length
)
const
count
=
Number
(
data
?.
count
??
players
.
length
)
console
.
log
(
'[WebSocket] 玩家列表更新 - 在线人数:'
,
count
,
'玩家数:'
,
players
.
length
,
'数据:'
,
JSON
.
stringify
(
players
.
map
((
p
:
any
)
=>
({
nickname
:
p
.
nickname
,
userid
:
p
.
userid
??
p
.
wechat_id
}))))
playerCount
.
value
=
count
loadingPlayers
.
value
=
Array
.
from
(
{
length
:
PLAYER_LIMIT
},
(
_
,
index
)
=>
players
[
index
]
??
emptyLoadingPlayer
(),
...
...
@@ -114,6 +116,7 @@ const gotoRank1WithIntro = async () => {
}
function
handleRoomState
(
data
:
any
)
{
console
.
log
(
'[WebSocket] 房间状态更新 - status:'
,
data
?.
status
,
'数据:'
,
JSON
.
stringify
(
data
))
if
(
Array
.
isArray
(
data
?.
list
))
{
renderPlayers
(
data
)
}
...
...
@@ -137,14 +140,21 @@ function handleRoomState(data: any) {
}
const
{
startGame
,
createRoom
,
backRoom
}
=
useAdminGameSocket
({
gameId
:
'game
1
'
,
gameId
:
'game
5
'
,
onRoomState
:
handleRoomState
,
onRoomJoin
:
(
data
)
=>
{
console
.
log
(
'[WebSocket] 玩家加入房间 - 数据:'
,
JSON
.
stringify
(
data
))
renderPlayers
(
data
)
gotoLoading
()
},
onGameStart
:
gotoRank1WithIntro
,
onRoomRank
:
updateRankPlayers
,
onGameStart
:
()
=>
{
console
.
log
(
'[WebSocket] 游戏开始!'
)
gotoRank1WithIntro
()
},
onRoomRank
:
(
data
)
=>
{
console
.
log
(
'[WebSocket] 排名更新 - 数据:'
,
JSON
.
stringify
(
data
))
updateRankPlayers
(
data
)
},
onRelogin
:
async
()
=>
{
$remove_socket_storage
();
await
router
.
replace
(
'/'
)
...
...
frontend-large/src/pages/game5/views/Rank1View.vue
View file @
c1dbe1ba
...
...
@@ -107,6 +107,7 @@ watch(
()
=>
props
.
rankList
,
(
rankList
)
=>
{
if
(
rankList
&&
rankList
.
length
>
0
)
{
console
.
log
(
'[Rank1View] 排名列表更新 - 数据:'
,
JSON
.
stringify
(
rankList
.
map
(
p
=>
({
nickname
:
p
.
nickname
,
score
:
p
.
score
}))))
updateRankList
(
rankList
);
}
},
...
...
@@ -121,11 +122,13 @@ const startIntro = async () => {
horses
.
value
=
[];
horseIdCounter
=
0
;
console
.
log
(
'[Rank1View] 游戏流程启动 - 开始 3s 倒计时 + 60s 游戏'
)
// 启动完整倒计时:3s 开场 + 60s 游戏
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
console
.
log
(
'[Rank1View] 倒计时结束,马匹开始奔跑'
)
// Phase 2: 倒计时结束 → 游戏正式开始,背景动画+马匹启动
gameStarted
.
value
=
true
;
...
...
@@ -139,6 +142,7 @@ const startIntro = async () => {
};
const
stopGame
=
()
=>
{
console
.
log
(
'[Rank1View] 游戏结束 - 60s 倒计时到'
)
isGameRunning
.
value
=
false
;
showResult
.
value
=
true
;
stopHorseLoop
();
...
...
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