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
01d3642f
Commit
01d3642f
authored
Jun 05, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: game5 PC端第一个页面完成;
parent
df306b30
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
1355 additions
and
0 deletions
+1355
-0
bg.svg
frontend-large/src/assets/images/game5/bg.svg
+0
-0
left.webp
frontend-large/src/assets/images/game5/left.webp
+0
-0
qrcode.png
frontend-large/src/assets/images/game5/qrcode.png
+0
-0
right.webp
frontend-large/src/assets/images/game5/right.webp
+0
-0
title1.webp
frontend-large/src/assets/images/game5/title1.webp
+0
-0
Game5.vue
frontend-large/src/pages/game5/Game5.vue
+189
-0
LoadingView.vue
frontend-large/src/pages/game5/views/LoadingView.vue
+264
-0
Rank1View.vue
frontend-large/src/pages/game5/views/Rank1View.vue
+491
-0
Rank2View.vue
frontend-large/src/pages/game5/views/Rank2View.vue
+402
-0
index.ts
frontend-large/src/router/index.ts
+9
-0
No files found.
frontend-large/src/assets/images/game5/bg.svg
0 → 100644
View file @
01d3642f
This source diff could not be displayed because it is too large. You can
view the blob
instead.
frontend-large/src/assets/images/game5/left.webp
0 → 100644
View file @
01d3642f
File added
frontend-large/src/assets/images/game5/qrcode.png
0 → 100644
View file @
01d3642f
7.69 KB
frontend-large/src/assets/images/game5/right.webp
0 → 100644
View file @
01d3642f
File added
frontend-large/src/assets/images/game5/title1.webp
0 → 100644
View file @
01d3642f
File added
frontend-large/src/pages/game5/Game5.vue
0 → 100644
View file @
01d3642f
<
script
setup
lang=
"ts"
>
import
{
nextTick
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
import
Loading
from
'./views/LoadingView.vue'
import
Rank1
from
'./views/Rank1View.vue'
import
Rank2
from
'./views/Rank2View.vue'
import
{
useAdminGameSocket
}
from
'@/composables/useAdminGameSocket'
import
type
Player
from
'@/commons/player.ts'
import
{
playGame1Music
}
from
'@/commons/music'
import
{
$remove_socket_storage
}
from
'@/commons/utils.ts'
const
router
=
useRouter
()
type
GameScreen
=
'loading'
|
'rank1'
|
'rank2'
const
PLAYER_LIMIT
=
20
const
RANK_LIMIT
=
10
const
emptyLoadingPlayer
=
()
=>
({
score
:
'-'
,
telephone
:
''
,
userid
:
''
,
wechat_id
:
''
,
nickname
:
'虚位以待'
,
avatar
:
''
,
})
const
emptyRankPlayer
=
(
index
:
number
):
Player
=>
({
wechat_id
:
`empty-
${
index
}
`
,
nickname
:
'虚位以待'
,
avatar
:
''
,
score
:
'-'
,
})
const
screen
=
ref
<
GameScreen
>
(
'loading'
)
const
loadingPlayers
=
ref
<
any
[]
>
(
Array
.
from
({
length
:
PLAYER_LIMIT
},
emptyLoadingPlayer
))
const
playerCount
=
ref
(
0
)
const
rankPlayers
=
ref
<
Player
[]
>
(
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
emptyRankPlayer
(
index
)))
const
rank1Ref
=
ref
<
InstanceType
<
typeof
Rank1
>
|
null
>
(
null
)
let
isPlayingStartAnimation
=
false
const
confirmRefresh
=
(
event
:
BeforeUnloadEvent
)
=>
{
event
.
preventDefault
()
event
.
returnValue
=
''
}
const
renderPlayers
=
(
data
:
any
)
=>
{
const
players
=
Array
.
isArray
(
data
?.
list
)
?
data
.
list
:
[]
playerCount
.
value
=
Number
(
data
?.
count
??
players
.
length
)
loadingPlayers
.
value
=
Array
.
from
(
{
length
:
PLAYER_LIMIT
},
(
_
,
index
)
=>
players
[
index
]
??
emptyLoadingPlayer
(),
)
}
const
updateRankPlayers
=
(
data
:
any
)
=>
{
if
(
!
Array
.
isArray
(
data
))
return
rankPlayers
.
value
=
Array
.
from
(
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
data
[
index
]
??
emptyRankPlayer
(
index
),
)
}
const
playerToRankPlayer
=
(
player
:
any
,
index
:
number
):
Player
=>
({
telephone
:
player
?.
telephone
??
''
,
userid
:
player
?.
userid
??
''
,
wechat_id
:
player
?.
wechat_id
||
`empty-
${
index
}
`
,
nickname
:
player
?.
nickname
||
'虚位以待'
,
avatar
:
player
?.
avatar
||
''
,
score
:
player
?.
score
??
'-'
,
})
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'
}
const
gotoRank1WithIntro
=
async
()
=>
{
if
(
isPlayingStartAnimation
)
return
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
screen
.
value
=
'rank1'
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
screen
.
value
=
'rank2'
isPlayingStartAnimation
=
false
}
function
handleRoomState
(
data
:
any
)
{
if
(
Array
.
isArray
(
data
?.
list
))
{
renderPlayers
(
data
)
}
switch
(
data
?.
status
)
{
case
0
:
gotoLoading
(
true
)
createRoom
(
1
)
break
case
1
:
gotoLoading
()
break
case
2
:
seedRankPlayersFromLoading
()
screen
.
value
=
'rank1'
break
case
3
:
screen
.
value
=
'rank2'
break
}
}
const
{
startGame
,
createRoom
,
backRoom
}
=
useAdminGameSocket
({
gameId
:
'game1'
,
onRoomState
:
handleRoomState
,
onRoomJoin
:
(
data
)
=>
{
renderPlayers
(
data
)
gotoLoading
()
},
onGameStart
:
gotoRank1WithIntro
,
onRoomRank
:
updateRankPlayers
,
onRelogin
:
async
()
=>
{
$remove_socket_storage
();
await
router
.
replace
(
'/'
)
}
})
const
startGameWithMusic
=
()
=>
{
if
(
playerCount
.
value
==
0
)
{
alert
(
'房间中没有玩家'
)
return
;
}
playGame1Music
()
startGame
()
}
const
nextRound
=
()
=>
{
gotoLoading
(
true
)
//重开房间
backRoom
(
1
)
}
const
nextRoundWithMusic
=
()
=>
{
playGame1Music
()
nextRound
()
}
onMounted
(()
=>
{
window
.
addEventListener
(
'beforeunload'
,
confirmRefresh
)
playGame1Music
()
})
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'beforeunload'
,
confirmRefresh
)
})
</
script
>
<
template
>
<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/game5/views/LoadingView.vue
0 → 100644
View file @
01d3642f
<
script
setup
lang=
"ts"
>
import
{
ref
}
from
'vue'
import
{
$format_str
}
from
'@/commons/utils.ts'
;
import
DesignStage
from
'@/components/DesignStage.vue'
import
{
assetUrl
,
cssAssetUrl
}
from
'@/commons/assets.ts'
;
defineProps
<
{
players
:
any
[];
playerCount
:
number
;
}
>
();
const
emit
=
defineEmits
<
{
start
:
[];
}
>
();
const
imageUrls
=
{
bg
:
cssAssetUrl
(
'game5/bg.svg'
),
logo
:
cssAssetUrl
(
'game1/logo.png'
),
title1
:
cssAssetUrl
(
'game5/title1.webp'
),
// title2: cssAssetUrl('game1/title2.png'),
qrcode
:
assetUrl
(
'game5/qrcode.png'
),
left
:
cssAssetUrl
(
'game5/left.webp'
),
right
:
cssAssetUrl
(
'game5/right.webp'
),
bottomLayers
:
cssAssetUrl
(
'game1/bottom-layers.png'
),
avatarAnimation
:
cssAssetUrl
(
'avatar_animation.png'
),
avatar
:
cssAssetUrl
(
'avatar.png'
),
line
:
cssAssetUrl
(
'game1/line.png'
),
start1
:
cssAssetUrl
(
'game1/start-1.png'
),
start2
:
cssAssetUrl
(
'game1/start-2.png'
),
};
const
isStartPressed
=
ref
(
false
);
const
pressStartButton
=
()
=>
{
isStartPressed
.
value
=
true
;
emit
(
'start'
);
};
const
releaseStartButton
=
()
=>
{
isStartPressed
.
value
=
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;`
}
</
script
>
<
template
>
<DesignStage>
<div
class=
"bg"
>
<div
class=
"img-logo"
></div>
<div
class=
"img-title1"
></div>
<!--
<div
class=
"img-title2"
></div>
-->
<div
class=
"img-qrcode-container"
>
<div
class=
"img-qrcode"
><img
:src=
"imageUrls.qrcode"
/></div>
<div
class=
"txt-qrcode"
>
微信扫码参与
</div>
</div>
<div
class=
"img-left"
></div>
<div
class=
"img-right"
></div>
<div
class=
"list-container"
>
<div
class=
"txt-count"
>
当前在线人数
<span>
{{
playerCount
}}
</span></div>
<div
class=
"list-container-left"
>
<div
v-for=
"(item, index) in players"
:key=
"item.userid || item.wechat_id || `empty-$
{index}`">
<div>
<div
class=
"img-avatar-container"
>
<div
class=
"img-avatar"
:style=
"`$
{renderAvatar(item)}`">
</div>
</div>
<div
class=
"txt-nickname"
>
{{
$format_str
(
item
.
nickname
,
12
)
}}
</div>
</div>
</div>
</div>
<div
class=
"list-container-line"
></div>
<div
class=
"list-container-right"
>
<div
class=
"btn-start"
:class=
"
{ pressed: isStartPressed }"
@pointerdown="pressStartButton"
@pointerup="releaseStartButton"
@pointerleave="releaseStartButton"
@pointercancel="releaseStartButton"
>
</div>
</div>
</div>
</div>
</DesignStage>
</
template
>
<
style
scoped
>
.bg
{
width
:
1920px
;
height
:
1080px
;
background
:
v-bind
(
'imageUrls.bg'
)
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-title1
{
background
:
v-bind
(
'imageUrls.title1'
);
width
:
891px
;
height
:
309px
;
margin
:
0
auto
;
position
:
relative
;
top
:
60px
;
}
/* .img-title2 {
background: v-bind('imageUrls.title2');
width: 643px;
height: 140px;
margin: 0 auto;
position: relative;
top: 80px;
} */
.img-qrcode-container
{
width
:
144px
;
height
:
177px
;
position
:
absolute
;
z-index
:
1
;
top
:
40px
;
right
:
40px
;
.img-qrcode
{
width
:
100%
;
height
:
144px
;
border-radius
:
10px
;
img
{
width
:
147px
;
height
:
147px
;
}
}
.txt-qrcode
{
font-size
:
24px
;
color
:
white
;
}
}
.img-left
{
background
:
v-bind
(
'imageUrls.left'
)
no-repeat
;
width
:
531px
;
height
:
418px
;
position
:
absolute
;
top
:
193px
;
left
:
47px
;
}
.img-right
{
background
:
v-bind
(
'imageUrls.right'
)
no-repeat
;
width
:
423px
;
height
:
419px
;
position
:
absolute
;
top
:
193px
;
right
:
50px
;
}
.list-container
{
background
:
v-bind
(
'imageUrls.bottomLayers'
);
width
:
1800px
;
height
:
397px
;
position
:
absolute
;
left
:
60px
;
bottom
:
60px
;
.txt-count
{
font-size
:
36px
;
margin-left
:
30px
;
margin-top
:
20px
;
color
:
#AA0000
;
font-weight
:
bold
;
>span
{
margin-left
:
20px
;
font-size
:
48px
;
position
:
relative
;
top
:
5px
;
}
}
.list-container-left
{
float
:
left
;
display
:
grid
;
grid-template-columns
:
repeat
(
10
,
1
fr
);
row-gap
:
20px
;
width
:
73%
;
margin-top
:
30px
;
height
:
250px
;
align-content
:
center
;
.img-avatar-container
{
position
:
relative
;
margin
:
0
auto
;
width
:
72px
;
height
:
72px
;
border-radius
:
36px
;
}
.img-avatar-container
::before
{
content
:
""
;
position
:
absolute
;
inset
:
0
;
border-radius
:
inherit
;
background
:
v-bind
(
'imageUrls.avatarAnimation'
);
animation
:
avatar-rotate
10s
linear
infinite
;
}
.img-avatar
{
position
:
relative
;
top
:
6px
;
z-index
:
1
;
margin
:
0
auto
;
width
:
60px
;
height
:
60px
;
border-radius
:
30px
;
/* background: v-bind('imageUrls.avatar'); */
}
.txt-nickname
{
color
:
#666
;
font-size
:
20px
;
text-align
:
center
;
}
}
.list-container-line
{
float
:
left
;
width
:
1px
;
height
:
281px
;
margin-left
:
50px
;
background
:
v-bind
(
'imageUrls.line'
)
center
no-repeat
;
}
.list-container-right
{
float
:
left
;
.btn-start
{
width
:
316px
;
height
:
105px
;
margin-left
:
60px
;
margin-top
:
80px
;
background
:
v-bind
(
'imageUrls.start1'
);
cursor
:
pointer
;
}
.btn-start.pressed
{
background
:
v-bind
(
'imageUrls.start2'
);
}
}
}
</
style
>
frontend-large/src/pages/game5/views/Rank1View.vue
0 → 100644
View file @
01d3642f
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
}
from
'vue'
import
DesignStage
from
'../../../components/DesignStage.vue'
import
type
Player
from
'../../../commons/player.ts'
import
{
$is_run_local
}
from
'@/commons/utils.ts'
;
import
{
assetUrl
,
cssAssetUrl
}
from
'@/commons/assets.ts'
;
const
props
=
defineProps
<
{
rankList
:
Player
[];
}
>
();
const
emit
=
defineEmits
<
{
start
:
[];
}
>
();
const
RANK_LIMIT
=
10
;
const
START_COUNTDOWN_SECONDS
=
3
;
const
GU_FRAME_DURATION
=
300
;
const
GU_LOOP_DELAY
=
1000
;
const
designStage
=
ref
<
InstanceType
<
typeof
DesignStage
>
|
null
>
(
null
);
const
rankEffects
=
ref
<
Record
<
string
,
"up"
|
"down"
|
"new"
>>
({});
const
guFrameIndex
=
ref
(
0
);
const
guBurstVisible
=
ref
(
false
);
const
guBurstKey
=
ref
(
0
);
let
guFramesReady
:
Promise
<
void
>
|
undefined
;
let
guTimer
:
ReturnType
<
typeof
setTimeout
>
|
undefined
;
let
guBurstTimer
:
ReturnType
<
typeof
setTimeout
>
|
undefined
;
const
loadGuFrames
=
(
dir
:
string
)
=>
Array
.
from
(
{
length
:
10
},
(
_
,
index
)
=>
assetUrl
(
`
${
dir
}
/
${
String
(
index
+
1
).
padStart
(
2
,
'0'
)}
.png`
),
);
const
guFrames
=
[
loadGuFrames
(
'game1/gu'
),
loadGuFrames
(
'game1/gu2'
),
];
const
imageUrls
=
{
bg
:
cssAssetUrl
(
'game1/bg.png'
),
logo
:
cssAssetUrl
(
'game1/logo.png'
),
title
:
cssAssetUrl
(
'game1/title3.png'
),
fireworks
:
cssAssetUrl
(
'game1/fireworks.png'
),
titleRank
:
cssAssetUrl
(
'game1/title-rank.png'
),
rankNo4
:
cssAssetUrl
(
'game1/no4.png'
),
rankNo1
:
cssAssetUrl
(
'game1/no1.png'
),
rankNo2
:
cssAssetUrl
(
'game1/no2.png'
),
rankNo3
:
cssAssetUrl
(
'game1/no3.png'
),
rankIcon
:
cssAssetUrl
(
'game1/no-icon.png'
),
rankNo1Icon
:
cssAssetUrl
(
'game1/no1-icon.png'
),
rankNo2Icon
:
cssAssetUrl
(
'game1/no2-icon.png'
),
rankNo3Icon
:
cssAssetUrl
(
'game1/no3-icon.png'
),
avatar
:
assetUrl
(
'avatar.png'
),
};
const
emptyPlayer
=
(
index
:
number
):
Player
=>
({
wechat_id
:
`empty-
${
index
}
`
,
nickname
:
'-'
,
avatar
:
''
,
score
:
0
,
});
const
list
=
ref
<
Player
[]
>
(
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
emptyPlayer
(
index
)));
const
playerKey
=
(
player
:
Player
,
index
=
0
)
=>
player
.
userid
||
player
.
wechat_id
||
`empty-
${
index
}
`
;
const
mockRankStep
=
ref
(
0
);
const
makeMockPlayer
=
(
id
:
number
,
score
:
number
):
Player
=>
({
wechat_id
:
`mock-
${
id
}
`
,
nickname
:
`Player
${
id
}
`
,
avatar
:
''
,
score
,
});
const
mockRankRounds
:
Player
[][]
=
[
[
makeMockPlayer
(
1
,
980
),
makeMockPlayer
(
2
,
900
),
makeMockPlayer
(
3
,
860
),
makeMockPlayer
(
4
,
820
),
makeMockPlayer
(
5
,
780
),
makeMockPlayer
(
6
,
720
),
makeMockPlayer
(
7
,
680
),
makeMockPlayer
(
8
,
640
),
makeMockPlayer
(
9
,
600
),
makeMockPlayer
(
10
,
560
),
],
[
makeMockPlayer
(
4
,
1040
),
makeMockPlayer
(
1
,
1010
),
makeMockPlayer
(
7
,
960
),
makeMockPlayer
(
2
,
940
),
makeMockPlayer
(
11
,
880
),
makeMockPlayer
(
3
,
850
),
makeMockPlayer
(
5
,
830
),
makeMockPlayer
(
8
,
790
),
makeMockPlayer
(
6
,
760
),
makeMockPlayer
(
10
,
700
),
],
[
makeMockPlayer
(
7
,
1180
),
makeMockPlayer
(
11
,
1090
),
makeMockPlayer
(
4
,
1060
),
makeMockPlayer
(
12
,
1000
),
makeMockPlayer
(
1
,
980
),
makeMockPlayer
(
8
,
930
),
makeMockPlayer
(
2
,
890
),
makeMockPlayer
(
5
,
850
),
makeMockPlayer
(
13
,
810
),
makeMockPlayer
(
3
,
780
),
],
];
const
updateRankList
=
(
rankList
:
Player
[])
=>
{
const
oldRanks
=
new
Map
(
list
.
value
.
map
((
player
,
index
)
=>
[
playerKey
(
player
,
index
),
index
]));
const
next
=
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
rankList
[
index
]
??
emptyPlayer
(
index
));
const
effects
:
Record
<
string
,
"up"
|
"down"
|
"new"
>
=
{};
next
.
forEach
((
player
,
index
)
=>
{
const
key
=
playerKey
(
player
,
index
);
if
(
!
key
||
key
.
startsWith
(
"empty-"
))
return
;
const
oldIndex
=
oldRanks
.
get
(
key
);
if
(
oldIndex
===
undefined
)
effects
[
key
]
=
"new"
;
else
if
(
oldIndex
>
index
)
effects
[
key
]
=
"up"
;
else
if
(
oldIndex
<
index
)
effects
[
key
]
=
"down"
;
});
list
.
value
=
next
;
rankEffects
.
value
=
{};
requestAnimationFrame
(()
=>
{
rankEffects
.
value
=
effects
;
});
};
watch
(
()
=>
props
.
rankList
,
(
rankList
)
=>
updateRankList
(
rankList
),
{
immediate
:
true
,
deep
:
true
},
);
const
playMockRankAnimation
=
()
=>
{
if
(
$is_run_local
())
{
updateRankList
(
mockRankRounds
[
mockRankStep
.
value
]
??
mockRankRounds
[
0
]
??
[]);
mockRankStep
.
value
=
(
mockRankStep
.
value
+
1
)
%
mockRankRounds
.
length
;
}
};
const
startGame
=
()
=>
{
if
(
$is_run_local
())
{
emit
(
'start'
);
}
};
const
stopPlayerGu
=
()
=>
{
if
(
guTimer
)
{
clearTimeout
(
guTimer
);
guTimer
=
undefined
;
}
if
(
guBurstTimer
)
{
clearTimeout
(
guBurstTimer
);
guBurstTimer
=
undefined
;
}
guBurstVisible
.
value
=
false
;
guFrameIndex
.
value
=
0
;
}
const
preloadGuFrames
=
()
=>
{
if
(
!
guFramesReady
)
{
guFramesReady
=
Promise
.
all
(
guFrames
.
flat
().
map
((
src
)
=>
{
const
img
=
new
Image
();
img
.
src
=
src
;
return
img
.
decode
?
img
.
decode
().
catch
(()
=>
undefined
)
:
new
Promise
<
void
>
((
resolve
)
=>
{
img
.
onload
=
()
=>
resolve
();
img
.
onerror
=
()
=>
resolve
();
});
})).
then
(()
=>
undefined
);
}
return
guFramesReady
;
}
const
playGuLoopDelayAnimation
=
()
=>
{
if
(
guBurstTimer
)
{
clearTimeout
(
guBurstTimer
);
guBurstTimer
=
undefined
;
}
guBurstVisible
.
value
=
false
;
requestAnimationFrame
(()
=>
{
guBurstKey
.
value
+=
1
;
guBurstVisible
.
value
=
true
;
guBurstTimer
=
setTimeout
(()
=>
{
guBurstVisible
.
value
=
false
;
guBurstTimer
=
undefined
;
},
GU_LOOP_DELAY
);
});
}
const
playerGu
=
async
()
=>
{
stopPlayerGu
();
const
frameCount
=
Math
.
min
(...
guFrames
.
map
((
frames
)
=>
frames
.
length
));
if
(
frameCount
<=
0
)
return
;
await
preloadGuFrames
();
let
frameIndex
=
0
;
const
tick
=
()
=>
{
guFrameIndex
.
value
=
frameIndex
;
if
(
frameIndex
>=
frameCount
-
1
)
{
frameIndex
=
0
;
playGuLoopDelayAnimation
();
guTimer
=
setTimeout
(
tick
,
GU_LOOP_DELAY
);
return
;
}
frameIndex
+=
1
;
guTimer
=
setTimeout
(
tick
,
GU_FRAME_DURATION
);
}
tick
();
}
const
startIntro
=
async
()
=>
{
playerGu
();
await
designStage
.
value
?.
startCountdown
(
START_COUNTDOWN_SECONDS
,
60
);
stopPlayerGu
();
};
defineExpose
({
startIntro
,
});
const
avatarStyle
=
(
item
:
Player
)
=>
{
const
avatar
=
item
.
avatar
||
imageUrls
.
avatar
return
{
backgroundImage
:
`url("
${
avatar
}
")`
,
}
}
</
script
>
<
template
>
<DesignStage
ref=
"designStage"
>
<div
class=
"bg"
>
<div
class=
"img-logo"
></div>
<div
class=
"img-title"
></div>
<div
class=
"img-gu-group"
>
<div
class=
"img-gu"
>
<div
v-for=
"(frame, index) in guFrames[1]"
:key=
"`left-frame-$
{frame}`" class="gu-frame"
:class="{ active: index === guFrameIndex }" :style="{ backgroundImage: `url(${frame})` }">
</div>
<div
:key=
"`left-$
{guBurstKey}`" class="gu-burst" :class="{ active: guBurstVisible }">
</div>
</div>
<div
class=
"img-gu"
>
<div
v-for=
"(frame, index) in guFrames[0]"
:key=
"`right-frame-$
{frame}`" class="gu-frame"
:class="{ active: index === guFrameIndex }" :style="{ backgroundImage: `url(${frame})` }">
</div>
<div
:key=
"`right-$
{guBurstKey}`" class="gu-burst" :class="{ active: guBurstVisible }">
</div>
</div>
</div>
<div
class=
"rank-container"
>
<div
class=
"rank-txt"
@
click=
"startGame()"
@
contextmenu
.
prevent=
"playMockRankAnimation()"
></div>
<TransitionGroup
name=
"rank"
tag=
"div"
class=
"rank-list"
>
<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"
:style=
"avatarStyle(item)"
></div>
<div
class=
"rank-nickname"
>
{{
item
.
nickname
}}
</div>
<div
class=
"rank-score"
>
{{
item
.
score
??
0
}}
</div>
</div>
</TransitionGroup>
</div>
</div>
</DesignStage>
</
template
>
<
style
scoped
>
.bg
{
width
:
1920px
;
height
:
1080px
;
background
:
v-bind
(
'imageUrls.bg'
)
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-title
{
background
:
v-bind
(
'imageUrls.title'
);
width
:
967px
;
height
:
122px
;
margin
:
0
auto
;
margin-top
:
37px
;
}
.img-gu-group
{
.img-gu
{
width
:
400px
;
height
:
400px
;
transform
:
scale
(
1.6
);
position
:
absolute
;
top
:
400px
;
}
.img-gu
:nth-child
(
1
)
{
left
:
200px
;
}
.img-gu
:nth-child
(
2
)
{
right
:
200px
;
}
.img-gu
{
.gu-frame
{
position
:
absolute
;
inset
:
0
;
opacity
:
0
;
background-position
:
center
;
background-repeat
:
no-repeat
;
background-size
:
contain
;
pointer-events
:
none
;
will-change
:
opacity
;
}
.gu-frame.active
{
opacity
:
1
;
}
.gu-burst
{
width
:
200px
;
height
:
200px
;
position
:
absolute
;
right
:
80px
;
top
:
30px
;
opacity
:
0
;
transform
:
scale
(
0.6
);
transform-origin
:
center
center
;
background
:
v-bind
(
'imageUrls.fireworks'
)
center
/
cover
no-repeat
;
pointer-events
:
none
;
&.active
{
animation
:
gu-burst-expand
1s
ease-out
both
;
}
}
}
}
.rank-container
{
width
:
503px
;
height
:
810px
;
background
:
#fff
;
opacity
:
0.9
;
border-radius
:
30px
;
position
:
absolute
;
left
:
50%
;
top
:
210px
;
transform
:
translateX
(
-50%
);
z-index
:
1
;
overflow
:
hidden
;
.rank-txt
{
background
:
v-bind
(
'imageUrls.titleRank'
);
width
:
304px
;
height
:
40px
;
margin
:
10px
auto
auto
auto
;
}
.rank-list
{
position
:
relative
;
}
.rank-item
{
background
:
v-bind
(
'imageUrls.rankNo4'
);
width
:
468px
;
height
:
68px
;
margin
:
6px
auto
0
auto
;
display
:
grid
;
grid-template-columns
:
95px
68px
1
fr
100px
;
align-items
:
center
;
color
:
#E00000
;
font-weight
:
bold
;
&.rank-top-1
{
background
:
v-bind
(
'imageUrls.rankNo1'
);
}
&
.rank-top-2
{
background
:
v-bind
(
'imageUrls.rankNo2'
);
}
&
.rank-top-3
{
background
:
v-bind
(
'imageUrls.rankNo3'
);
}
.rank-icon
{
background
:
v-bind
(
'imageUrls.rankIcon'
)
center
/
cover
no-repeat
;
width
:
40px
;
height
:
40px
;
margin
:
auto
auto
auto
20px
;
text-align
:
center
;
color
:
white
;
font-weight
:
bold
;
font-size
:
22px
;
line-height
:
40px
;
}
&
.rank-top-1
.rank-icon
{
background
:
v-bind
(
'imageUrls.rankNo1Icon'
)
center
/
cover
no-repeat
;
width
:
60px
;
height
:
38px
;
margin
:
auto
auto
auto
10px
;
}
&
.rank-top-2
.rank-icon
{
background
:
v-bind
(
'imageUrls.rankNo2Icon'
)
center
/
cover
no-repeat
;
width
:
60px
;
height
:
38px
;
margin
:
auto
auto
auto
10px
;
}
&
.rank-top-3
.rank-icon
{
background
:
v-bind
(
'imageUrls.rankNo3Icon'
)
center
/
cover
no-repeat
;
width
:
60px
;
height
:
38px
;
margin
:
auto
auto
auto
10px
;
}
.rank-avatar
{
background-position
:
center
;
background-repeat
:
no-repeat
;
background-size
:
cover
;
width
:
48px
;
height
:
48px
;
border-radius
:
24px
;
margin-left
:
10px
;
}
.rank-nickname
{
text-indent
:
10px
;
}
.rank-score
{
text-align
:
right
;
margin-right
:
30px
;
}
}
}
.rank-move
{
transition
:
transform
0.45s
ease
;
}
.rank-enter-active
,
.rank-leave-active
{
transition
:
opacity
0.25s
ease
,
transform
0.25s
ease
;
}
.rank-leave-active
{
position
:
absolute
;
left
:
17px
;
}
.rank-enter-from
,
.rank-leave-to
{
opacity
:
0
;
transform
:
translateY
(
90px
);
}
.rank-item.up
,
.rank-item.new
{
animation
:
rank-up-flash
0.8s
ease
;
}
.rank-item.down
{
animation
:
rank-down-flash
0.8s
ease
;
}
</
style
>
frontend-large/src/pages/game5/views/Rank2View.vue
0 → 100644
View file @
01d3642f
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
}
from
'vue'
import
DesignStage
from
'../../../components/DesignStage.vue'
import
type
Player
from
'../../../commons/player.ts'
import
{
cssAssetUrl
}
from
'@/commons/assets.ts'
;
const
props
=
defineProps
<
{
rankList
:
Player
[];
}
>
();
const
emit
=
defineEmits
<
{
next
:
[];
}
>
();
const
FINAL_RANK_LIMIT
=
10
;
const
imageUrls
=
{
bg
:
cssAssetUrl
(
'game1/bg.png'
),
logo
:
cssAssetUrl
(
'game1/logo.png'
),
title
:
cssAssetUrl
(
'game1/title3.png'
),
title2
:
cssAssetUrl
(
'game1/title4.png'
),
podium
:
cssAssetUrl
(
'game1/podium.png'
),
fireworks
:
cssAssetUrl
(
'game1/fireworks.png'
),
avatar
:
cssAssetUrl
(
'avatar.png'
),
guan1
:
cssAssetUrl
(
'game1/guan1.png'
),
pai1
:
cssAssetUrl
(
'game1/pai1.png'
),
guan2
:
cssAssetUrl
(
'game1/guan2.png'
),
pai2
:
cssAssetUrl
(
'game1/pai2.png'
),
guan3
:
cssAssetUrl
(
'game1/guan3.png'
),
pai3
:
cssAssetUrl
(
'game1/pai3.png'
),
rankIcon
:
cssAssetUrl
(
'game1/no-icon.png'
),
avatarAnimation
:
cssAssetUrl
(
'avatar_animation.png'
),
next
:
cssAssetUrl
(
'game1/next-1.png'
),
};
const
emptyPlayer
=
(
index
:
number
):
Player
=>
({
active
:
false
,
wechat_id
:
`empty-
${
index
}
`
,
nickname
:
'虚位以待'
,
avatar
:
''
,
score
:
0
,
});
const
list
=
ref
<
Player
[]
>
(
Array
.
from
({
length
:
FINAL_RANK_LIMIT
},
(
_
,
index
)
=>
emptyPlayer
(
index
)));
const
updateFinalRank
=
(
rankList
:
Player
[])
=>
{
list
.
value
=
Array
.
from
(
{
length
:
FINAL_RANK_LIMIT
},
(
_
,
index
)
=>
rankList
[
index
]
??
emptyPlayer
(
index
),
);
};
watch
(
()
=>
props
.
rankList
,
(
rankList
)
=>
updateFinalRank
(
rankList
),
{
immediate
:
true
,
deep
:
true
},
);
const
renderAvatar
=
(
item
:
any
|
null
)
=>
{
if
(
item
&&
item
.
avatar
)
{
return
`background: url('
${
item
.
avatar
}
') center / cover no-repeat;`
}
return
`background: v-bind('imageUrls.avatar') center / cover no-repeat;`
}
</
script
>
<
template
>
<DesignStage>
<div
class=
"bg"
>
<div
class=
"img-logo"
></div>
<div
class=
"img-title"
></div>
<div
class=
"img-title2"
></div>
<div
class=
"podium"
>
<div
class=
"rank-no1"
>
<div
class=
"gu-burst"
></div>
<div
class=
"avatar"
:style=
"`$
{renderAvatar(list[0])}`">
</div>
<div
class=
"nickname"
>
{{
list
[
0
]?.
nickname
}}
</div>
<div
class=
"guan"
></div>
<div
class=
"pai"
></div>
</div>
<div
class=
"rank-no2"
>
<div
class=
"gu-burst"
></div>
<div
class=
"avatar"
:style=
"`$
{renderAvatar(list[1])}`">
</div>
<div
class=
"nickname"
>
{{
list
[
1
]?.
nickname
}}
</div>
<div
class=
"guan"
></div>
<div
class=
"pai"
></div>
</div>
<div
class=
"rank-no3"
>
<div
class=
"gu-burst"
></div>
<div
class=
"avatar"
:style=
"`$
{renderAvatar(list[2])}`">
</div>
<div
class=
"nickname"
>
{{
list
[
2
]?.
nickname
}}
</div>
<div
class=
"guan"
></div>
<div
class=
"pai"
></div>
</div>
</div>
<div
class=
"rank-container"
>
<div
v-for=
"(item, index) in list.slice(3)"
:key=
"item.userid || item.wechat_id || `rank-$
{index}`">
<div
class=
"no-icon"
>
{{
index
+
4
}}
</div>
<div>
<div
class=
"img-avatar-container"
>
<div
class=
"img-avatar"
></div>
</div>
<div
class=
"txt-nickname"
>
{{
item
.
nickname
}}
</div>
</div>
</div>
</div>
<div
class=
"btn-next"
@
click=
"emit('next')"
></div>
</div>
</DesignStage>
</
template
>
<
style
scoped
>
.bg
{
width
:
1920px
;
height
:
1080px
;
background
:
v-bind
(
'imageUrls.bg'
)
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-title
{
background
:
v-bind
(
'imageUrls.title'
);
width
:
967px
;
height
:
122px
;
margin
:
0
auto
;
margin-top
:
37px
;
}
.img-title2
{
background
:
v-bind
(
'imageUrls.title2'
)
no-repeat
center
;
width
:
457px
;
height
:
57px
;
margin
:
0
auto
;
margin-top
:
37px
;
}
.podium
{
background
:
v-bind
(
'imageUrls.podium'
)
no-repeat
center
;
width
:
1347px
;
height
:
269px
;
position
:
absolute
;
top
:
427px
;
left
:
287px
;
.gu-burst
{
width
:
200px
;
height
:
200px
;
position
:
absolute
;
z-index
:
1
;
border-radius
:
50%
;
opacity
:
1
;
transform
:
scale
(
0.6
);
transform-origin
:
center
center
;
background
:
v-bind
(
'imageUrls.fireworks'
)
center
/
cover
no-repeat
;
pointer-events
:
none
;
animation
:
gu-burst-expand
1.2s
ease-out
infinite
;
}
.rank-no1
{
position
:
relative
;
.gu-burst
{
left
:
580px
;
top
:
-200px
;
}
.avatar
{
/* background: v-bind('imageUrls.avatar') center / cover no-repeat; */
width
:
120px
;
height
:
120px
;
border-radius
:
60px
;
position
:
absolute
;
left
:
610px
;
top
:
-136px
;
}
.nickname
{
width
:
167px
;
height
:
46px
;
background
:
#FFCF44
;
color
:
white
;
text-align
:
center
;
line-height
:
46px
;
position
:
absolute
;
left
:
590px
;
top
:
-25px
;
z-index
:
1
;
border-radius
:
23px
;
}
.guan
{
background
:
v-bind
(
'imageUrls.guan1'
);
width
:
64px
;
height
:
64px
;
position
:
absolute
;
left
:
580px
;
top
:
-153px
}
.pai
{
background
:
v-bind
(
'imageUrls.pai1'
);
width
:
212px
;
height
:
200px
;
position
:
absolute
;
left
:
570px
;
top
:
40px
;
}
}
.rank-no2
{
position
:
relative
;
.gu-burst
{
left
:
140px
;
top
:
-170px
;
}
.avatar
{
background
:
v-bind
(
'imageUrls.avatar'
)
center
/
cover
no-repeat
;
width
:
120px
;
height
:
120px
;
border-radius
:
60px
;
position
:
absolute
;
left
:
180px
;
top
:
-110px
;
}
.nickname
{
width
:
167px
;
height
:
46px
;
background
:
#CA9600
;
color
:
white
;
text-align
:
center
;
line-height
:
46px
;
position
:
absolute
;
left
:
157px
;
top
:
2px
;
z-index
:
1
;
border-radius
:
23px
;
}
.guan
{
background
:
v-bind
(
'imageUrls.guan2'
)
no-repeat
center
;
width
:
96px
;
height
:
96px
;
position
:
absolute
;
left
:
134px
;
top
:
-144px
;
}
.pai
{
background
:
v-bind
(
'imageUrls.pai2'
)
no-repeat
center
;
width
:
212px
;
height
:
200px
;
position
:
absolute
;
left
:
140px
;
top
:
50px
;
}
}
.rank-no3
{
position
:
relative
;
.gu-burst
{
left
:
1020px
;
top
:
-170px
;
}
.avatar
{
background
:
v-bind
(
'imageUrls.avatar'
)
center
/
cover
no-repeat
;
width
:
120px
;
height
:
120px
;
border-radius
:
60px
;
position
:
absolute
;
left
:
1046px
;
top
:
-110px
;
}
.nickname
{
width
:
167px
;
height
:
46px
;
background
:
#E2E2E2
;
color
:
#AFAFAF
;
text-align
:
center
;
line-height
:
46px
;
position
:
absolute
;
left
:
1028px
;
top
:
2px
;
z-index
:
1
;
border-radius
:
23px
;
}
.guan
{
background
:
v-bind
(
'imageUrls.guan3'
)
no-repeat
center
;
width
:
96px
;
height
:
96px
;
position
:
absolute
;
left
:
1003px
;
top
:
-146px
;
}
.pai
{
background
:
v-bind
(
'imageUrls.pai3'
)
no-repeat
center
;
width
:
212px
;
height
:
200px
;
position
:
absolute
;
right
:
130px
;
top
:
50px
;
}
}
}
.rank-container
{
display
:
flex
;
align-items
:
center
;
width
:
1270px
;
height
:
115px
;
position
:
absolute
;
bottom
:
243px
;
left
:
325px
;
>div
{
flex
:
1
;
display
:
grid
;
width
:
100px
;
height
:
100%
;
.no-icon
{
background
:
v-bind
(
'imageUrls.rankIcon'
)
no-repeat
center
;
width
:
40px
;
height
:
40px
;
position
:
absolute
;
text-align
:
center
;
line-height
:
40px
;
color
:
white
;
font-weight
:
bold
;
}
.img-avatar-container
{
position
:
relative
;
margin
:
0
auto
;
margin-top
:
10px
;
width
:
72px
;
height
:
72px
;
border-radius
:
36px
;
}
.img-avatar-container
::before
{
content
:
""
;
position
:
absolute
;
inset
:
0
;
border-radius
:
inherit
;
background
:
v-bind
(
'imageUrls.avatarAnimation'
);
animation
:
avatar-rotate
10s
linear
infinite
;
}
.img-avatar
{
position
:
relative
;
top
:
6px
;
z-index
:
1
;
margin
:
0
auto
;
width
:
60px
;
height
:
60px
;
border-radius
:
30px
;
background
:
v-bind
(
'imageUrls.avatar'
);
}
.txt-nickname
{
color
:
white
;
font-size
:
20px
;
text-align
:
center
;
}
}
}
.btn-next
{
position
:
absolute
;
bottom
:
100px
;
left
:
820px
;
width
:
316px
;
height
:
105px
;
cursor
:
pointer
;
background
:
v-bind
(
'imageUrls.next'
)
center
/
cover
no-repeat
;
}
</
style
>
frontend-large/src/router/index.ts
View file @
01d3642f
...
@@ -4,6 +4,7 @@ import Main from '@/pages/Main.vue'
...
@@ -4,6 +4,7 @@ import Main from '@/pages/Main.vue'
import
Login
from
'@/pages/Login.vue'
import
Login
from
'@/pages/Login.vue'
import
Game1
from
'@/pages/game1/Game.vue'
import
Game1
from
'@/pages/game1/Game.vue'
import
Game4
from
'@/pages/game4/Game4.vue'
import
Game4
from
'@/pages/game4/Game4.vue'
import
Game5
from
'@/pages/game5/Game5.vue'
import
Game6
from
'@/pages/game6/Game.vue'
import
Game6
from
'@/pages/game6/Game.vue'
const
router
=
createRouter
({
const
router
=
createRouter
({
...
@@ -40,6 +41,14 @@ const router = createRouter({
...
@@ -40,6 +41,14 @@ const router = createRouter({
},
},
},
},
{
{
path
:
'/game5'
,
name
:
'Game5'
,
component
:
Game5
,
meta
:
{
requiresAuth
:
true
,
},
},
{
path
:
'/game6'
,
path
:
'/game6'
,
name
:
'Game6'
,
name
:
'Game6'
,
component
:
Game6
,
component
:
Game6
,
...
...
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