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
f807008f
Commit
f807008f
authored
Jun 12, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
style: game3 的 h5 样式马位置、道具位置、道具碰到位置、苹果蒙层宽度调整;
parent
f8b7ee46
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
544 additions
and
427 deletions
+544
-427
MobileStage.vue
frontend-h5/src/components/MobileStage.vue
+7
-3
Game3.vue
frontend-h5/src/pages/game3/Game3.vue
+491
-410
PlayingView.vue
frontend-h5/src/pages/game3/views/PlayingView.vue
+46
-14
No files found.
frontend-h5/src/components/MobileStage.vue
View file @
f807008f
...
...
@@ -193,14 +193,18 @@ defineExpose({
}
.modal-container
{
position
:
absolute
;
inset
:
0
;
/* position:fixed + 显式 100vw/100vh 确保在 iOS Safari 中全屏蒙层,
避免因父级 overflow:hidden / transform:scale 导致 inset:0 计算异常 */
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
z-index
:
10
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
rgba
(
0
,
0
,
0
,
0.6
);
/* pointer-events: none; */
}
.game-rule-fade-enter-active
,
...
...
frontend-h5/src/pages/game3/Game3.vue
View file @
f807008f
<
script
setup
lang=
"ts"
>
import
{
computed
,
onBeforeUnmount
,
onMounted
,
ref
}
from
'vue'
import
{
cssAssetUrl
}
from
'@/commons/assets.ts'
import
MobileStage
from
'@/components/MobileStage.vue'
import
{
useGameSocket
,
sendGameMessage
,
userJoinStatus
,
joinSharedRoom
}
from
'@/composables/useGameSocket'
import
LoadingView
from
'./views/LoadingView.vue'
import
PlayingView
from
'./views/PlayingView.vue'
import
{
$getWechat
,
$toast
,
$is_run_local
}
from
'@/commons/utils.ts'
import
{
playGame3Music
,
stopAllMusic
}
from
'@/commons/music'
;
type
GameView
=
'loading'
|
'playing'
import
{
computed
,
onBeforeUnmount
,
onMounted
,
ref
}
from
"vue"
;
import
{
cssAssetUrl
}
from
"@/commons/assets.ts"
;
import
MobileStage
from
"@/components/MobileStage.vue"
;
import
{
useGameSocket
,
sendGameMessage
,
userJoinStatus
,
joinSharedRoom
,
}
from
"@/composables/useGameSocket"
;
import
LoadingView
from
"./views/LoadingView.vue"
;
import
PlayingView
from
"./views/PlayingView.vue"
;
import
{
$getWechat
,
$toast
,
$is_run_local
}
from
"@/commons/utils.ts"
;
import
{
playGame3Music
,
stopAllMusic
}
from
"@/commons/music"
;
// ⚠ 发布上线时设为 true,本地测试时设为 false
const
IS_PRODUCTION
=
true
;
type
GameView
=
"loading"
|
"playing"
;
type
RankPlayer
=
{
rank
:
number
nickname
:
string
avatar
:
string
score
:
number
|
string
}
rank
:
number
;
nickname
:
string
;
avatar
:
string
;
score
:
number
|
string
;
}
;
const
imageUrls
:
Record
<
string
,
string
>
=
{
bg
:
cssAssetUrl
(
'game3/bg.webp'
),
bg3
:
cssAssetUrl
(
'game3/bg3.webp'
),
left
:
cssAssetUrl
(
'game3/icon_zuo.webp'
),
right
:
cssAssetUrl
(
'game3/icon_you.webp'
),
bg4
:
cssAssetUrl
(
'game3/pic_paodao.webp'
),
logo
:
cssAssetUrl
(
'logo.png'
),
loadingBg1
:
cssAssetUrl
(
'game6/loadingbg1.png'
),
loadingBg2
:
cssAssetUrl
(
'game6/loadingbg2.png'
),
close
:
cssAssetUrl
(
'game1/close.png'
),
clock
:
cssAssetUrl
(
'game1/clock.png'
),
bg2
:
cssAssetUrl
(
'game6/bg2.png'
),
hydt
:
cssAssetUrl
(
'game3/title3.webp'
),
rank
:
cssAssetUrl
(
'game6/rank.png'
),
moleShowSprite
:
cssAssetUrl
(
'game6/mole_show.png'
),
moleHideSprite
:
cssAssetUrl
(
'game6/mole_hide.png'
),
moleHideprite
:
cssAssetUrl
(
'game6/mole_hit.png'
),
rabbitFaintprite
:
cssAssetUrl
(
'game6/rabbit_faint.png'
),
rabbitHideprite
:
cssAssetUrl
(
'game6/rabbit_hide.png'
),
rabbitshowprite
:
cssAssetUrl
(
'game6/rabbit_show.png'
),
land
:
cssAssetUrl
(
'game6/land.png'
),
hammer
:
cssAssetUrl
(
'game6/hammer.png'
),
ranklist
:
cssAssetUrl
(
'game6/ranklist.png'
),
rule
:
cssAssetUrl
(
'game1/rule.png'
)
}
const
wechat
=
$getWechat
()
const
currentView
=
ref
<
GameView
>
(
'loading'
)
const
countdownInterval
=
ref
(
60
)
const
isDivDescVisible
=
ref
(
false
)
const
isGameOver
=
ref
(
false
)
const
score
=
ref
(
0
)
const
rank
=
ref
(
0
)
//排名
const
rankList
=
ref
<
RankPlayer
[]
>
([])
bg
:
cssAssetUrl
(
"game3/bg.webp"
),
bg3
:
cssAssetUrl
(
"game3/bg3.webp"
),
left
:
cssAssetUrl
(
"game3/icon_zuo.webp"
),
right
:
cssAssetUrl
(
"game3/icon_you.webp"
),
bg4
:
cssAssetUrl
(
"game3/pic_paodao.webp"
),
logo
:
cssAssetUrl
(
"logo.png"
),
loadingBg1
:
cssAssetUrl
(
"game6/loadingbg1.png"
),
loadingBg2
:
cssAssetUrl
(
"game6/loadingbg2.png"
),
close
:
cssAssetUrl
(
"game1/close.png"
),
clock
:
cssAssetUrl
(
"game1/clock.png"
),
bg2
:
cssAssetUrl
(
"game6/bg2.png"
),
hydt
:
cssAssetUrl
(
"game3/title3.webp"
),
rank
:
cssAssetUrl
(
"game6/rank.png"
),
moleShowSprite
:
cssAssetUrl
(
"game6/mole_show.png"
),
moleHideSprite
:
cssAssetUrl
(
"game6/mole_hide.png"
),
moleHideprite
:
cssAssetUrl
(
"game6/mole_hit.png"
),
rabbitFaintprite
:
cssAssetUrl
(
"game6/rabbit_faint.png"
),
rabbitHideprite
:
cssAssetUrl
(
"game6/rabbit_hide.png"
),
rabbitshowprite
:
cssAssetUrl
(
"game6/rabbit_show.png"
),
land
:
cssAssetUrl
(
"game6/land.png"
),
hammer
:
cssAssetUrl
(
"game6/hammer.png"
),
ranklist
:
cssAssetUrl
(
"game6/ranklist.png"
),
rule
:
cssAssetUrl
(
"game1/rule.png"
),
}
;
const
wechat
=
$getWechat
()
;
const
currentView
=
ref
<
GameView
>
(
"loading"
);
const
countdownInterval
=
ref
(
60
)
;
const
isDivDescVisible
=
ref
(
false
)
;
const
isGameOver
=
ref
(
false
)
;
const
score
=
ref
(
0
)
;
const
rank
=
ref
(
0
)
;
//排名
const
rankList
=
ref
<
RankPlayer
[]
>
([])
;
const
displayRankList
=
computed
<
RankPlayer
[]
>
(()
=>
{
const
list
=
rankList
.
value
.
slice
(
0
,
10
)
return
Array
.
from
({
length
:
10
},
(
_
,
index
)
=>
{
return
list
[
index
]
??
{
rank
:
index
+
1
,
nickname
:
'-'
,
avatar
:
''
,
score
:
'-'
,
}
})
})
const
list
=
rankList
.
value
.
slice
(
0
,
10
);
return
Array
.
from
({
length
:
10
},
(
_
,
index
)
=>
{
return
(
list
[
index
]
??
{
rank
:
index
+
1
,
nickname
:
"-"
,
avatar
:
""
,
score
:
"-"
,
}
);
});
});
const
rankScore
=
computed
<
number
|
string
>
(()
=>
{
const
currentPlayer
=
rankList
.
value
.
slice
(
0
,
10
)
.
find
(
item
=>
item
.
rank
===
rank
.
value
)
const
currentPlayer
=
rankList
.
value
.
slice
(
0
,
10
)
.
find
((
item
)
=>
item
.
rank
===
rank
.
value
);
return
currentPlayer
?.
score
??
score
.
value
})
return
currentPlayer
?.
score
??
score
.
value
;
})
;
// const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03]
// const currentGu = ref(imageUrls.gu01)
const
isGuPlaying
=
ref
(
false
)
const
token
=
ref
(
wechat
?.
token
??
''
)
const
nickname
=
ref
(
wechat
?.
nickname
??
''
)
const
avatar
=
ref
(
wechat
?.
avatar
??
''
)
const
showGameRule
=
ref
(
true
)
const
showGameRank
=
ref
(
false
)
const
isLocalMode
=
$is_run_local
()
const
mobileStageRef
=
ref
<
InstanceType
<
typeof
MobileStage
>
|
null
>
(
null
)
const
_offSocketMessage
=
ref
<
(()
=>
void
)
|
undefined
>
()
let
guFrameTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
let
gameCountdownTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
const
isGuPlaying
=
ref
(
false
)
;
const
token
=
ref
(
wechat
?.
token
??
""
);
const
nickname
=
ref
(
wechat
?.
nickname
??
""
);
const
avatar
=
ref
(
wechat
?.
avatar
??
""
);
const
showGameRule
=
ref
(
true
)
;
const
showGameRank
=
ref
(
false
)
;
const
isLocalMode
=
!
IS_PRODUCTION
&&
$is_run_local
();
const
mobileStageRef
=
ref
<
InstanceType
<
typeof
MobileStage
>
|
null
>
(
null
)
;
const
_offSocketMessage
=
ref
<
(()
=>
void
)
|
undefined
>
()
;
let
guFrameTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
;
let
gameCountdownTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
;
const
confirmRefresh
=
(
event
:
BeforeUnloadEvent
)
=>
{
event
.
preventDefault
()
event
.
returnValue
=
''
}
event
.
preventDefault
();
event
.
returnValue
=
""
;
}
;
function
setDivDescVisible
(
visible
:
boolean
)
{
isDivDescVisible
.
value
=
visible
isDivDescVisible
.
value
=
visible
;
}
function
stopGameCountdown
()
{
if
(
gameCountdownTimer
)
{
window
.
clearTimeout
(
gameCountdownTimer
)
gameCountdownTimer
=
undefined
}
if
(
gameCountdownTimer
)
{
window
.
clearTimeout
(
gameCountdownTimer
);
gameCountdownTimer
=
undefined
;
}
}
function
submitScore
(
save
:
boolean
=
false
,
currentTick
=
score
.
value
)
{
if
(
save
)
{
sendGameMessage
(
'submit_score_save'
,
{
score
:
currentTick
,
wechat
:
wechat
.
token_origin
,
item_num
:
3
,
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
})
}
else
{
sendGameMessage
(
'submit_score'
,
{
score
:
currentTick
})
if
(
!
wechat
&&
!
isLocalMode
)
return
;
if
(
save
)
{
if
(
isLocalMode
&&
!
wechat
)
{
console
.
log
(
"[game3 本地模式] 提交最终分数:"
,
currentTick
);
return
;
}
sendGameMessage
(
"submit_score_save"
,
{
score
:
currentTick
,
wechat
:
wechat
.
token_origin
,
item_num
:
3
,
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
});
}
else
{
if
(
isLocalMode
&&
!
wechat
)
{
console
.
log
(
"[game3 本地模式] 实时分数:"
,
currentTick
);
return
;
}
sendGameMessage
(
"submit_score"
,
{
score
:
currentTick
});
}
}
function
startGameCountdown
(
seconds
=
60
)
{
stopGameCountdown
()
countdownInterval
.
value
=
seconds
setDivDescVisible
(
true
)
const
tickCountdown
=
()
=>
{
countdownInterval
.
value
-=
1
if
(
countdownInterval
.
value
<=
0
)
{
countdownInterval
.
value
=
0
gameCountdownTimer
=
undefined
setDivDescVisible
(
false
)
submitScore
(
true
)
showGameOverRank
()
return
}
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
)
stopGameCountdown
();
countdownInterval
.
value
=
seconds
;
setDivDescVisible
(
true
);
const
tickCountdown
=
()
=>
{
countdownInterval
.
value
-=
1
;
if
(
countdownInterval
.
value
<=
0
)
{
countdownInterval
.
value
=
0
;
gameCountdownTimer
=
undefined
;
setDivDescVisible
(
false
);
submitScore
(
true
);
showGameOverRank
();
return
;
}
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
)
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
);
};
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
);
}
function
resetToLoadingView
()
{
stopGameCountdown
()
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
)
guFrameTimer
=
undefined
}
score
.
value
=
0
rank
.
value
=
0
rankList
.
value
=
[]
countdownInterval
.
value
=
60
isGuPlaying
.
value
=
false
isGameOver
.
value
=
false
userJoinStatus
.
value
=
false
showGameRank
.
value
=
false
currentView
.
value
=
'loading'
setDivDescVisible
(
false
)
stopGameCountdown
();
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
);
guFrameTimer
=
undefined
;
}
score
.
value
=
0
;
rank
.
value
=
0
;
rankList
.
value
=
[];
countdownInterval
.
value
=
60
;
isGuPlaying
.
value
=
false
;
isGameOver
.
value
=
false
;
userJoinStatus
.
value
=
false
;
showGameRank
.
value
=
false
;
currentView
.
value
=
"loading"
;
setDivDescVisible
(
false
);
}
function
startGameView
()
{
stopGameCountdown
()
score
.
value
=
0
rank
.
value
=
0
rankList
.
value
=
[]
isGameOver
.
value
=
false
showGameRank
.
value
=
false
currentView
.
value
=
'playing'
startGameCountdown
()
stopGameCountdown
();
score
.
value
=
0
;
rank
.
value
=
0
;
rankList
.
value
=
[];
isGameOver
.
value
=
false
;
showGameRank
.
value
=
false
;
currentView
.
value
=
"playing"
;
startGameCountdown
();
}
function
showGameOverRank
()
{
stopGameCountdown
()
setDivDescVisible
(
false
)
currentView
.
value
=
'loading'
showGameRank
.
value
=
true
userJoinStatus
.
value
=
false
;
stopGameCountdown
();
setDivDescVisible
(
false
);
currentView
.
value
=
"loading"
;
showGameRank
.
value
=
true
;
userJoinStatus
.
value
=
false
;
}
const
scoreChangeHandler
=
(
delta
:
number
)
=>
{
if
(
delta
>
0
)
{
playGame3Music
(
1
);
}
else
{
playGame3Music
(
2
);
}
let
nextTick
=
score
.
value
+
delta
if
(
nextTick
<
0
)
nextTick
=
0
score
.
value
=
nextTick
submitScore
(
false
,
nextTick
)
}
if
(
delta
>
0
)
{
playGame3Music
(
1
);
}
else
{
playGame3Music
(
2
);
}
let
nextTick
=
score
.
value
+
delta
;
if
(
nextTick
<
0
)
nextTick
=
0
;
score
.
value
=
nextTick
;
submitScore
(
false
,
nextTick
);
}
;
if
(
wechat
)
{
const
gameId
=
'game3'
;
const
{
offSocketMessage
}
=
useGameSocket
({
gameId
,
auth
:
{
userid
:
wechat
.
token
,
},
onConnect
:
()
=>
{
window
.
setTimeout
(()
=>
{
const
joined
=
joinSharedRoom
(
nickname
.
value
,
token
.
value
,
avatar
.
value
,
gameId
)
},
200
)
},
onGameStart
:
async
()
=>
{
showGameRule
.
value
=
false
;
await
mobileStageRef
.
value
?.
startCountdown
()
startGameView
()
},
onScoreSubmitted
:
(
is_save
,
data
)
=>
{
if
(
!
is_save
)
{
//非保存状态下
const
nextRank
=
Number
(
typeof
data
===
'object'
?
data
?.
rank
:
data
)
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
rank
.
value
=
nextRank
}
}
else
if
(
typeof
data
===
'object'
&&
data
)
{
if
(
Array
.
isArray
(
data
.
list
))
{
rankList
.
value
=
data
.
list
.
map
((
item
:
any
,
index
:
number
)
=>
({
rank
:
Number
(
item
?.
rank
??
index
+
1
),
nickname
:
String
(
item
?.
nickname
??
''
),
avatar
:
String
(
item
?.
avatar
??
''
),
score
:
Number
(
item
?.
score
??
0
),
}))
}
const
nextRank
=
Number
(
data
.
rank
)
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
rank
.
value
=
nextRank
}
}
},
onRescoreSubmitted
:
(
_recount
)
=>
{
submitScore
(
true
);
},
onGameRecover
:
(
data
)
=>
{
const
recoveredRank
=
Number
(
data
?.
rank
)
const
recoveredScore
=
Number
(
data
?.
score
)
const
recoveredSeconds
=
Number
(
data
?.
remainingSeconds
)
rank
.
value
=
Number
.
isFinite
(
recoveredRank
)
&&
recoveredRank
>
0
?
recoveredRank
:
0
score
.
value
=
Number
.
isFinite
(
recoveredScore
)
&&
recoveredScore
>
0
?
recoveredScore
:
0
showGameRule
.
value
=
false
showGameRank
.
value
=
false
isGameOver
.
value
=
false
currentView
.
value
=
'playing'
startGameCountdown
(
Number
.
isFinite
(
recoveredSeconds
)
&&
recoveredSeconds
>
0
?
recoveredSeconds
:
0
,
)
},
onRoomClosed
:
()
=>
{
stopGameCountdown
()
currentView
.
value
=
'loading'
setDivDescVisible
(
false
)
$toast
(
'管理员关闭了游戏房间'
)
},
onRoomBack
:
()
=>
{
},
})
_offSocketMessage
.
value
=
offSocketMessage
const
gameId
=
"game3"
;
const
{
offSocketMessage
}
=
useGameSocket
({
gameId
,
auth
:
{
userid
:
wechat
.
token
,
},
onConnect
:
()
=>
{
window
.
setTimeout
(()
=>
{
const
joined
=
joinSharedRoom
(
nickname
.
value
,
token
.
value
,
avatar
.
value
,
gameId
,
);
},
200
);
},
onGameStart
:
async
()
=>
{
showGameRule
.
value
=
false
;
await
mobileStageRef
.
value
?.
startCountdown
();
startGameView
();
},
onScoreSubmitted
:
(
is_save
,
data
)
=>
{
if
(
!
is_save
)
{
//非保存状态下
const
nextRank
=
Number
(
typeof
data
===
"object"
?
data
?.
rank
:
data
);
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
rank
.
value
=
nextRank
;
}
}
else
if
(
typeof
data
===
"object"
&&
data
)
{
if
(
Array
.
isArray
(
data
.
list
))
{
rankList
.
value
=
data
.
list
.
map
((
item
:
any
,
index
:
number
)
=>
({
rank
:
Number
(
item
?.
rank
??
index
+
1
),
nickname
:
String
(
item
?.
nickname
??
""
),
avatar
:
String
(
item
?.
avatar
??
""
),
score
:
Number
(
item
?.
score
??
0
),
}));
}
const
nextRank
=
Number
(
data
.
rank
);
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
rank
.
value
=
nextRank
;
}
}
},
onRescoreSubmitted
:
(
_recount
)
=>
{
submitScore
(
true
);
},
onGameRecover
:
(
data
)
=>
{
const
recoveredRank
=
Number
(
data
?.
rank
);
const
recoveredScore
=
Number
(
data
?.
score
);
const
recoveredSeconds
=
Number
(
data
?.
remainingSeconds
);
rank
.
value
=
Number
.
isFinite
(
recoveredRank
)
&&
recoveredRank
>
0
?
recoveredRank
:
0
;
score
.
value
=
Number
.
isFinite
(
recoveredScore
)
&&
recoveredScore
>
0
?
recoveredScore
:
0
;
showGameRule
.
value
=
false
;
showGameRank
.
value
=
false
;
isGameOver
.
value
=
false
;
currentView
.
value
=
"playing"
;
startGameCountdown
(
Number
.
isFinite
(
recoveredSeconds
)
&&
recoveredSeconds
>
0
?
recoveredSeconds
:
0
,
);
},
onRoomClosed
:
()
=>
{
stopGameCountdown
();
currentView
.
value
=
"loading"
;
setDivDescVisible
(
false
);
$toast
(
"管理员关闭了游戏房间"
);
},
onRoomBack
:
()
=>
{},
});
_offSocketMessage
.
value
=
offSocketMessage
;
}
onMounted
(()
=>
{
document
.
title
=
'互动游戏 - 马上有福'
if
(
token
.
value
)
{
window
.
addEventListener
(
'beforeunload'
,
confirmRefresh
)
}
})
document
.
title
=
"互动游戏 - 马上有福"
;
if
(
token
.
value
)
{
window
.
addEventListener
(
"beforeunload"
,
confirmRefresh
);
}
// 本地测试模式:自动模拟游戏开始(IS_PRODUCTION=false 时生效)
if
(
isLocalMode
)
{
showGameRule
.
value
=
false
;
currentView
.
value
=
"playing"
;
startGameCountdown
();
}
});
onBeforeUnmount
(()
=>
{
_offSocketMessage
.
value
?.()
stopAllMusic
()
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
)
}
stopGameCountdown
()
if
(
token
.
value
)
{
window
.
removeEventListener
(
'beforeunload'
,
confirmRefresh
)
}
})
_offSocketMessage
.
value
?.();
stopAllMusic
();
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
);
}
stopGameCountdown
();
if
(
token
.
value
)
{
window
.
removeEventListener
(
"beforeunload"
,
confirmRefresh
);
}
})
;
const
showGameRuleHandler
=
()
=>
{
showGameRule
.
value
=
!
showGameRule
.
value
;
}
showGameRule
.
value
=
!
showGameRule
.
value
;
}
;
const
renderBG
=
()
=>
{
if
(
currentView
.
value
==
'playing'
)
{
return
`
${
imageUrls
.
bg2
}
center/cover`
}
return
`
${
imageUrls
.
bg
}
center/cover`
;
}
if
(
currentView
.
value
==
"playing"
)
{
return
`
${
imageUrls
.
bg2
}
center/cover`
;
}
return
`
${
imageUrls
.
bg
}
center/cover`
;
}
;
const
rankCloseHandler
=
()
=>
{
showGameRank
.
value
=
false
isGameOver
.
value
=
false
resetToLoadingView
()
}
showGameRank
.
value
=
false
;
isGameOver
.
value
=
false
;
resetToLoadingView
();
}
;
</
script
>
<
template
>
<MobileStage
v-if=
"token"
:showGameRule=
"showGameRule"
:showGameRank=
"showGameRank"
ref=
"mobileStageRef"
:background=
"`$
{renderBG()}`">
<template
#
gameRule
>
<div
class=
"rule-container"
>
<div
class=
"rule-txt-container"
>
<div
class=
"rule-title"
>
游戏规则
</div>
<div
class=
"rule-txt"
>
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;
<br
/>
2. 限时闯关比拼得分,结束后按积分排名;
<br
/>
3. 高分用户有机会领取新疆福彩定制周边;
<br
/>
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
祝您游戏愉快!
</div>
<div
class=
"rule-close"
@
click=
"showGameRuleHandler"
></div>
</div>
<MobileStage
v-if=
"token || isLocalMode"
:showGameRule=
"showGameRule"
:showGameRank=
"showGameRank"
ref=
"mobileStageRef"
:background=
"`$
{renderBG()}`"
>
<template
#
gameRule
>
<div
class=
"rule-container"
>
<div
class=
"rule-txt-container"
>
<div
class=
"rule-title"
>
游戏规则
</div>
<div
class=
"rule-txt"
>
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2
分;
<br
/>
2. 限时闯关比拼得分,结束后按积分排名;
<br
/>
3. 高分用户有机会领取新疆福彩定制周边;
<br
/>
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
祝您游戏愉快!
</div>
<div
class=
"rule-close"
@
click=
"showGameRuleHandler"
></div>
</div>
</div>
</
template
>
<
template
#
gameRank
>
<div
class=
"ranklist-container"
>
<div
class=
"txt-title"
>
排行
</div>
<div
class=
"list-container"
>
<div
class=
"row row-1"
>
<div
class=
"col-1"
>
排名
</div>
<div
class=
"col-2"
>
用户
</div>
<div
class=
"col-3"
>
成绩
</div>
</div>
<div
class=
"row-2"
>
<div
class=
"row rank-row"
v-for=
"(item, index) in displayRankList"
:key=
"`$
{item.rank}-${index}`"
>
<div
class=
"col-1"
>
<div>
{{
item
.
rank
}}
</div>
</div>
<div
class=
"col-2"
>
<div
class=
"avatar"
:style=
"
item.avatar
?
{ backgroundImage: `url(${item.avatar})` }
: {}
"
>
</div>
<div>
{{
item
.
nickname
}}
</div>
</div>
<div
class=
"col-3"
>
{{
item
.
score
}}
</div>
</div>
</
template
>
<
template
#
gameRank
>
<div
class=
"ranklist-container"
>
<div
class=
"txt-title"
>
排行
</div>
<div
class=
"list-container"
>
<div
class=
"row row-1"
>
<div
class=
"col-1"
>
排名
</div>
<div
class=
"col-2"
>
用户
</div>
<div
class=
"col-3"
>
成绩
</div>
</div>
<div
class=
"row-2"
>
<div
class=
"row rank-row"
v-for=
"(item, index) in displayRankList"
:key=
"`$
{item.rank}-${index}`">
<div
class=
"col-1"
>
<div>
{{
item
.
rank
}}
</div>
</div>
<div
class=
"col-2"
>
<div
class=
"avatar"
:style=
"item.avatar ?
{ backgroundImage: `url(${item.avatar})` } : {}">
</div>
<div>
{{
item
.
nickname
}}
</div>
</div>
<div
class=
"col-3"
>
{{
item
.
score
}}
</div>
</div>
</div>
<div
class=
"row row-3"
>
<div
class=
"col-1"
>
{{
rank
>
0
?
rank
:
'未上榜'
}}
</div>
<div
class=
"col-2"
>
<div
class=
"avatar"
:style=
"
{ backgroundImage: `url(${avatar})` }">
</div>
<div>
{{
nickname
}}
</div>
</div>
<div
class=
"col-3"
>
{{
rankScore
}}
</div>
</div>
</div>
<div
class=
"img-close"
@
click=
"rankCloseHandler"
></div>
</div>
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<PlayingView
v-else
:image-urls=
"imageUrls"
:tick=
"score"
:rank=
"rank"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
score-change=
"scoreChangeHandler"
@
rank-close=
"rankCloseHandler"
/>
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
</div>
<div
class=
"row row-3"
>
<div
class=
"col-1"
>
{{
rank
>
0
?
rank
:
"未上榜"
}}
</div>
<div
class=
"col-2"
>
<div
class=
"avatar"
:style=
"
{ backgroundImage: `url(${avatar})` }"
>
</div>
<div>
{{
nickname
}}
</div>
</div>
<div
class=
"col-3"
>
{{
rankScore
}}
</div>
</div>
</div>
<div
class=
"img-close"
@
click=
"rankCloseHandler"
></div>
</div>
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<PlayingView
v-else
:image-urls=
"imageUrls"
:tick=
"score"
:rank=
"rank"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
score-change=
"scoreChangeHandler"
@
rank-close=
"rankCloseHandler"
/>
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" />
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</MobileStage>
<div
v-else
style=
"text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"
>
请使用微信扫码进入游戏
</div>
</MobileStage>
<div
v-else
style=
"
text-align: center;
width: 100vw;
height: 100vh;
line-height: 30;
font-size: 20px;
"
>
请使用微信扫码进入游戏
</div>
</template>
<
style
scoped
>
.rule-container
{
.rule-txt-container
{
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
:
26pt
;
line-height
:
50pt
;
letter-spacing
:
4px
;
}
.rule-txt
{
font-size
:
26pt
;
padding
:
40px
;
margin-top
:
10px
;
color
:
#AA0000
;
}
.rule-txt-container
{
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
:
26pt
;
line-height
:
50pt
;
letter-spacing
:
4px
;
}
.rule-close
{
width
:
57px
;
height
:
57px
;
background
:
v-bind
(
'imageUrls.close'
)
center
/
cover
no-repeat
;
transform
:
translateX
(
-50%
)
scale
(
1.4
);
position
:
absolute
;
bottom
:
-75px
;
left
:
50%
;
.rule-txt
{
font-size
:
26pt
;
padding
:
40px
;
margin-top
:
10px
;
color
:
#aa0000
;
}
}
.rule-close
{
width
:
57px
;
height
:
57px
;
background
:
v-bind
(
"imageUrls.close"
)
center
/
cover
no-repeat
;
transform
:
translateX
(
-50%
)
scale
(
1.4
);
position
:
absolute
;
bottom
:
-75px
;
left
:
50%
;
}
}
.ranklist-container
{
width
:
747px
;
height
:
1084px
;
background
:
v-bind
(
'imageUrls.ranklist'
);
transform
:
translate
(
-50%
,
-50%
)
scale
(
0.45
);
left
:
50%
;
top
:
50%
;
width
:
747px
;
height
:
1084px
;
background
:
v-bind
(
"imageUrls.ranklist"
);
transform
:
translate
(
-50%
,
-50%
)
scale
(
0.45
);
left
:
50%
;
top
:
50%
;
position
:
absolute
;
.txt-title
{
width
:
100%
;
text-align
:
center
;
font-size
:
50px
;
margin-top
:
35px
;
font-weight
:
bold
;
color
:
#aa0000
;
letter-spacing
:
2px
;
}
.list-container
{
width
:
calc
(
100%
-
90px
);
height
:
calc
(
100%
-
210px
);
position
:
absolute
;
.txt-title
{
width
:
100%
;
text-align
:
center
;
font-size
:
50px
;
margin-top
:
35px
;
font-weight
:
bold
;
color
:
#AA0000
;
letter-spacing
:
2px
;
left
:
40px
;
top
:
140px
;
text-align
:
center
;
height
:
73px
;
line-height
:
73px
;
font-size
:
32px
;
.row
{
display
:
grid
;
grid-template-columns
:
150px
auto
150px
;
align-items
:
center
;
}
.list-container
{
width
:
calc
(
100%
-
90px
);
height
:
calc
(
100%
-
210px
);
position
:
absolute
;
left
:
40px
;
top
:
140px
;
text-align
:
center
;
height
:
73px
;
line-height
:
73px
;
font-size
:
32px
;
.row
{
display
:
grid
;
grid-template-columns
:
150px
auto
150px
;
align-items
:
center
;
}
.row-1
{
color
:
#AA0000
;
font-weight
:
bold
;
background
:
linear-gradient
(
-84deg
,
#FFD396
0%
,
#FFF3D1
53%
,
#FFD59C
100%
);
border-radius
:
20px
20px
0px
0px
;
}
.row-1
{
color
:
#aa0000
;
font-weight
:
bold
;
background
:
linear-gradient
(
-84deg
,
#ffd396
0%
,
#fff3d1
53%
,
#ffd59c
100%
);
border-radius
:
20px
20px
0px
0px
;
}
.row-2
{
color
:
#633911
;
.row-2
{
color
:
#633911
;
.
rank-row
:
nth-child
(
odd
)
{
background
:
#F2E2AE
;
}
.
rank-row
:
nth-child
(
odd
)
{
background
:
#f2e2ae
;
}
.rank-row
:nth-child
(
even
)
{
background
:
#FCEEBF
;
}
.rank-row
:nth-child
(
even
)
{
background
:
#fceebf
;
}
.col-1
{
position
:
relative
;
}
}
.col-1
{
position
:
relative
;
}
}
.row-3
{
background
:
linear-gradient
(
0deg
,
#F4B64C
0%
,
#F7C96F
100%
);
border-radius
:
0px
0px
20px
20px
;
}
.row-3
{
background
:
linear-gradient
(
0deg
,
#f4b64c
0%
,
#f7c96f
100%
);
border-radius
:
0px
0px
20px
20px
;
}
.col-2
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
18px
;
.avatar
{
width
:
48px
;
height
:
48px
;
flex
:
0
0
auto
;
border-radius
:
50%
;
background-position
:
center
;
background-size
:
cover
;
}
}
.col-2
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
18px
;
.avatar
{
width
:
48px
;
height
:
48px
;
flex
:
0
0
auto
;
border-radius
:
50%
;
background-position
:
center
;
background-size
:
cover
;
}
}
}
}
.img-close
{
width
:
57px
;
height
:
57px
;
background
:
v-bind
(
'imageUrls.close'
)
center
/
cover
no-repeat
;
transform
:
translateX
(
-50%
)
scale
(
1.4
);
position
:
absolute
;
top
:
0
;
left
:
100%
;
width
:
57px
;
height
:
57px
;
background
:
v-bind
(
"imageUrls.close"
)
center
/
cover
no-repeat
;
transform
:
translateX
(
-50%
)
scale
(
1.4
);
position
:
absolute
;
top
:
0
;
left
:
100%
;
}
</
style
>
frontend-h5/src/pages/game3/views/PlayingView.vue
View file @
f807008f
...
...
@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11;
const
ITEM_SIZE
=
80
;
const
GAME_DURATION
=
60
;
const
LEFT_LANE_X
=
170
;
const
RIGHT_LANE_X
=
470
;
// 设计基准跑道 X 位置(750px 设计稿下的值,窄屏设备上会自动居中偏移)
const
DESIGN_LEFT_LANE_X
=
150
;
const
DESIGN_RIGHT_LANE_X
=
500
;
// 运行时根据视口计算的真实 X 位置(适配不同屏幕宽度)
const
leftLaneX
=
ref
(
DESIGN_LEFT_LANE_X
);
const
rightLaneX
=
ref
(
DESIGN_RIGHT_LANE_X
);
/** 按视口宽度计算 750px 设计区域在 content-bg 中的居中偏移 */
const
updateLanePositions
=
()
=>
{
const
vw
=
window
.
visualViewport
?.
width
??
window
.
innerWidth
;
const
vh
=
window
.
visualViewport
?.
height
??
window
.
innerHeight
;
// MobileStage contain 模式下 scale = min(scaleX, scaleY),手机通常由高度决定
const
scaleY
=
vh
/
1624
;
if
(
!
scaleY
||
scaleY
<=
0
)
return
;
const
stageViewportWidth
=
vw
/
scaleY
;
const
offset
=
Math
.
max
(
0
,
(
stageViewportWidth
-
750
)
/
2
);
leftLaneX
.
value
=
DESIGN_LEFT_LANE_X
+
offset
;
rightLaneX
.
value
=
DESIGN_RIGHT_LANE_X
+
offset
;
};
// ========== 马帧 ==========
const
HORSE_FRAME_COUNT
=
21
;
...
...
@@ -63,20 +81,20 @@ preloadFrames(bomFrames);
const
horseLane
=
ref
<
'left'
|
'right'
>
(
'left'
);
const
horseFrameIndex
=
ref
(
0
);
const
horseX
=
ref
(
LEFT_LANE_X
);
const
horseTargetX
=
ref
(
LEFT_LANE_X
);
const
horseX
=
ref
(
leftLaneX
.
value
);
const
horseTargetX
=
ref
(
leftLaneX
.
value
);
const
switchToLeft
=
()
=>
{
if
(
horseLane
.
value
!==
'left'
)
{
horseLane
.
value
=
'left'
;
horseTargetX
.
value
=
LEFT_LANE_X
;
horseTargetX
.
value
=
leftLaneX
.
value
;
}
};
const
switchToRight
=
()
=>
{
if
(
horseLane
.
value
!==
'right'
)
{
horseLane
.
value
=
'right'
;
horseTargetX
.
value
=
RIGHT_LANE_X
;
horseTargetX
.
value
=
rightLaneX
.
value
;
}
};
...
...
@@ -183,7 +201,7 @@ function generateNewItem() {
if
(
!
props
.
isDivDescVisible
)
return
;
const
seed
=
Date
.
now
()
+
itemIdCounter
;
const
kind
:
ItemKind
=
seededRandom
(
seed
)
<
0.
5
5
?
'coin'
:
'bomb'
;
const
kind
:
ItemKind
=
seededRandom
(
seed
)
<
0.
7
5
?
'coin'
:
'bomb'
;
// 概率分配车道,但避免与前一个同车道道具太近
let
lane
:
'left'
|
'right'
=
seededRandom
(
seed
+
1000
)
<
0.5
?
'left'
:
'right'
;
...
...
@@ -244,7 +262,7 @@ const checkCollisions = () => {
for
(
const
item
of
roadItems
.
value
)
{
if
(
item
.
collected
||
item
.
colliding
)
continue
;
const
itemX
=
item
.
lane
===
'left'
?
LEFT_LANE_X
:
RIGHT_LANE_X
;
const
itemX
=
item
.
lane
===
'left'
?
leftLaneX
.
value
:
rightLaneX
.
value
;
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset
const
itemCenterX
=
itemX
+
ITEM_SIZE
/
2
;
const
itemCenterY
=
item
.
y
+
ITEM_SIZE
/
2
;
...
...
@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const
frameDuration
=
1000
/
totalFrames
;
// 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点)
effectAnims
.
value
.
push
({
id
:
animId
,
kind
:
item
.
kind
,
frame
:
0
,
x
:
x
+
40
,
y
});
effectAnims
.
value
.
push
({
id
:
animId
,
kind
:
item
.
kind
,
frame
:
0
,
x
:
x
+
65
,
y
});
let
frameIdx
=
0
;
const
tick
=
()
=>
{
...
...
@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const
effectId
=
++
scoreEffectIdSeq
;
scoreEffects
.
value
.
push
({
id
:
effectId
,
x
:
x
+
ITEM_SIZE
/
2
,
x
:
x
+
ITEM_SIZE
/
2
+
25
,
y
:
y
,
score
:
delta
,
});
...
...
@@ -377,6 +395,12 @@ const stopMainLoop = () => {
// ========== 生命周期 ==========
onMounted
(()
=>
{
window
.
addEventListener
(
'keydown'
,
handleKeydown
);
window
.
addEventListener
(
'resize'
,
updateLanePositions
);
window
.
visualViewport
?.
addEventListener
(
'resize'
,
updateLanePositions
);
updateLanePositions
();
// 初始化马匹在左侧跑道(使用计算后的实际 X 位置)
horseX
.
value
=
leftLaneX
.
value
;
horseTargetX
.
value
=
leftLaneX
.
value
;
nextTick
(()
=>
{
startRoadScroll
();
startMainLoop
();
...
...
@@ -386,6 +410,8 @@ onMounted(() => {
onBeforeUnmount
(()
=>
{
window
.
removeEventListener
(
'keydown'
,
handleKeydown
);
window
.
removeEventListener
(
'resize'
,
updateLanePositions
);
window
.
visualViewport
?.
removeEventListener
(
'resize'
,
updateLanePositions
);
stopRoadScroll
();
stopMainLoop
();
stopItemGen
();
...
...
@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => {
scoreEffects
.
value
=
[];
roadOffset
.
value
=
0
;
gameElapsed
.
value
=
0
;
updateLanePositions
();
horseX
.
value
=
leftLaneX
.
value
;
horseTargetX
.
value
=
leftLaneX
.
value
;
startRoadScroll
();
startMainLoop
();
scheduleNextItem
();
...
...
@@ -582,18 +611,21 @@ watch(() => props.isDivDescVisible, (visible) => {
.road-item
{
position
:
absolute
;
width
:
80px
;
height
:
80px
;
/* width: 80px;
height: 80px; */
width
:
77px
;
height
:
96px
;
z-index
:
5
;
pointer-events
:
none
;
}
.item-lane-left
{
left
:
170px
;
/* 当 content-bg 宽于 750px 设计稿时,自动居中 750px 游戏区域 */
left
:
calc
((
var
(
--stage-viewport-width
,
750px
)
-
750px
)
/
2
+
170px
);
}
.item-lane-right
{
left
:
470px
;
left
:
calc
((
var
(
--stage-viewport-width
,
750px
)
-
750px
)
/
2
+
520px
)
;
}
.effect-anim
{
...
...
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