feat(game): 添加游戏房间菜单和托管功能
- 引入机器人和退出图标资源 - 实现游戏房间顶部菜单触发器和弹出菜单 - 添加托管模式切换功能 - 实现退出房间功能 - 添加全局点击和ESC键关闭菜单事件监听 - 优化菜单动画效果和交互反馈 - 移除侧边按钮区域的聊天、赞赏和开局按钮 - 调整时钟位置以适应新菜单布局
This commit is contained in:
1
src/assets/images/icons/exit.svg
Normal file
1
src/assets/images/icons/exit.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774343595232" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5478" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M951.737186 488.212224 802.424532 301.56936c-7.222495-9.027607-18.034748-14.011108-29.157064-14.011108-4.131087 0-8.300037 0.688685-12.349259 2.106987-14.957667 5.246491-24.970718 19.371186-24.970718 35.223223l0 111.98756-298.631448 0c-41.232077 0-74.656327 33.42425-74.656327 74.656327 0 41.2331 33.42425 74.656327 74.656327 74.656327l298.631448 0 0 111.98756c0 15.852036 10.013051 29.977755 24.970718 35.223223 4.049223 1.424442 8.218172 2.108011 12.349259 2.108011 11.123338 0 21.934568-4.978385 29.157064-14.013155l149.311631-186.643887C962.64563 521.221012 962.64563 501.848803 951.737186 488.212224L951.737186 488.212224zM586.628698 810.162774 362.66074 810.162774l-74.656327 0 0-0.011256c-0.199545 0-0.393973 0.011256-0.587378 0.011256-40.906665 0-74.076112-33.42425-74.076112-74.656327l0-74.656327 0-298.631448 0-74.656327 0.011256 0c0-0.199545-0.011256-0.393973-0.011256-0.587378 0-40.906665 33.429367-74.076112 74.66349-74.076112l74.656327 0 223.967958 0c41.2331 0 74.66349-33.422204 74.66349-74.656327 0-41.232077-33.429367-74.656327-74.66349-74.656327L213.340923 63.586201c-82.459037 0-149.311631 66.853617-149.311631 149.311631l0 597.262896c0 82.4662 66.853617 149.311631 149.311631 149.311631l373.286752 0c41.2331 0 74.66349-33.422204 74.66349-74.656327C661.291165 843.586001 627.861798 810.162774 586.628698 810.162774L586.628698 810.162774zM586.628698 810.162774" fill="#272636" p-id="5479"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/assets/images/icons/robot.svg
Normal file
1
src/assets/images/icons/robot.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774343512314" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3627" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 85.333333a85.333333 85.333333 0 0 1 85.333333 85.333334c0 31.573333-17.066667 59.306667-42.666666 73.813333V298.666667h42.666666a298.666667 298.666667 0 0 1 298.666667 298.666666h42.666667a42.666667 42.666667 0 0 1 42.666666 42.666667v128a42.666667 42.666667 0 0 1-42.666666 42.666667h-42.666667v42.666666a85.333333 85.333333 0 0 1-85.333333 85.333334H213.333333a85.333333 85.333333 0 0 1-85.333333-85.333334v-42.666666H85.333333a42.666667 42.666667 0 0 1-42.666666-42.666667v-128a42.666667 42.666667 0 0 1 42.666666-42.666667h42.666667a298.666667 298.666667 0 0 1 298.666667-298.666666h42.666666V244.48c-25.6-14.506667-42.666667-42.24-42.666666-73.813333a85.333333 85.333333 0 0 1 85.333333-85.333334M320 554.666667A106.666667 106.666667 0 0 0 213.333333 661.333333 106.666667 106.666667 0 0 0 320 768a106.666667 106.666667 0 0 0 106.666667-106.666667A106.666667 106.666667 0 0 0 320 554.666667m384 0a106.666667 106.666667 0 0 0-106.666667 106.666666 106.666667 106.666667 0 0 0 106.666667 106.666667 106.666667 106.666667 0 0 0 106.666667-106.666667 106.666667 106.666667 0 0 0-106.666667-106.666666z" fill="" p-id="3628"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -107,6 +107,27 @@
|
|||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-trigger-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-trigger {
|
||||||
|
transition: transform 120ms ease-out, box-shadow 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-trigger.is-feedback {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-trigger-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-trigger.is-feedback .menu-trigger-icon {
|
||||||
|
animation: menu-toggle-spin 170ms cubic-bezier(0.2, 0.75, 0.35, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.metal-circle {
|
.metal-circle {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@@ -122,6 +143,106 @@
|
|||||||
0 8px 16px rgba(0, 0, 0, 0.22);
|
0 8px 16px rgba(0, 0, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-popover {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 58px;
|
||||||
|
min-width: 124px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
background: rgba(18, 20, 27, 0.94);
|
||||||
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
transform-origin: left top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-pop-enter-active,
|
||||||
|
.menu-pop-leave-active {
|
||||||
|
transition: transform 170ms cubic-bezier(0.2, 0.78, 0.3, 1), opacity 170ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-pop-enter-from,
|
||||||
|
.menu-pop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-pop-enter-to,
|
||||||
|
.menu-pop-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #eaf3ff;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: background-color 130ms ease-out, transform 130ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-icon {
|
||||||
|
width: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.9;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-icon img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-danger {
|
||||||
|
color: #ffd0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-delay-1,
|
||||||
|
.menu-item-delay-2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
animation: menu-item-reveal 180ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-delay-1 {
|
||||||
|
animation-delay: 60ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-delay-2 {
|
||||||
|
animation-delay: 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
.left-counter {
|
.left-counter {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -133,6 +254,38 @@
|
|||||||
background: rgba(13, 15, 20, 0.78);
|
background: rgba(13, 15, 20, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trust-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(132, 235, 166, 0.3);
|
||||||
|
color: #d4ffe0;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgba(23, 109, 52, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes menu-toggle-spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(22deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes menu-item-reveal {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.counter-light {
|
.counter-light {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -145,8 +298,8 @@
|
|||||||
|
|
||||||
.top-right-clock {
|
.top-right-clock {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 30px;
|
||||||
right: 16px;
|
right: 36px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import deskImage from '../assets/images/desk/desk_01.png'
|
|||||||
import wanIcon from '../assets/images/flowerClolor/wan.png'
|
import wanIcon from '../assets/images/flowerClolor/wan.png'
|
||||||
import tongIcon from '../assets/images/flowerClolor/tong.png'
|
import tongIcon from '../assets/images/flowerClolor/tong.png'
|
||||||
import tiaoIcon from '../assets/images/flowerClolor/tiao.png'
|
import tiaoIcon from '../assets/images/flowerClolor/tiao.png'
|
||||||
|
import robotIcon from '../assets/images/icons/robot.svg'
|
||||||
|
import exitIcon from '../assets/images/icons/exit.svg'
|
||||||
import '../assets/styles/room.css'
|
import '../assets/styles/room.css'
|
||||||
import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
||||||
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
||||||
@@ -42,6 +44,11 @@ const {
|
|||||||
|
|
||||||
const now = ref(Date.now())
|
const now = ref(Date.now())
|
||||||
let clockTimer: number | null = null
|
let clockTimer: number | null = null
|
||||||
|
const menuOpen = ref(false)
|
||||||
|
const isTrustMode = ref(false)
|
||||||
|
const menuTriggerActive = ref(false)
|
||||||
|
let menuTriggerTimer: number | null = null
|
||||||
|
let menuOpenTimer: number | null = null
|
||||||
|
|
||||||
const roomStatusText = computed(() => {
|
const roomStatusText = computed(() => {
|
||||||
if (roomState.value.status === 'playing') {
|
if (roomState.value.status === 'playing') {
|
||||||
@@ -220,10 +227,65 @@ function actionTheme(type: string): 'gold' | 'jade' | 'blue' {
|
|||||||
return 'blue'
|
return 'blue'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMenu(): void {
|
||||||
|
menuTriggerActive.value = true
|
||||||
|
if (menuTriggerTimer !== null) {
|
||||||
|
window.clearTimeout(menuTriggerTimer)
|
||||||
|
}
|
||||||
|
menuTriggerTimer = window.setTimeout(() => {
|
||||||
|
menuTriggerActive.value = false
|
||||||
|
menuTriggerTimer = null
|
||||||
|
}, 180)
|
||||||
|
|
||||||
|
if (menuOpen.value) {
|
||||||
|
menuOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuOpenTimer !== null) {
|
||||||
|
window.clearTimeout(menuOpenTimer)
|
||||||
|
}
|
||||||
|
menuOpenTimer = window.setTimeout(() => {
|
||||||
|
menuOpen.value = true
|
||||||
|
menuOpenTimer = null
|
||||||
|
}, 85)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTrustMode(): void {
|
||||||
|
isTrustMode.value = !isTrustMode.value
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLeaveRoom(): void {
|
||||||
|
menuOpen.value = false
|
||||||
|
backHall()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGlobalClick(event: MouseEvent): void {
|
||||||
|
const target = event.target as HTMLElement | null
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.closest('.menu-trigger-wrap')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGlobalEsc(event: KeyboardEvent): void {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
clockTimer = window.setInterval(() => {
|
clockTimer = window.setInterval(() => {
|
||||||
now.value = Date.now()
|
now.value = Date.now()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
window.addEventListener('click', handleGlobalClick)
|
||||||
|
window.addEventListener('keydown', handleGlobalEsc)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -231,6 +293,16 @@ onBeforeUnmount(() => {
|
|||||||
window.clearInterval(clockTimer)
|
window.clearInterval(clockTimer)
|
||||||
clockTimer = null
|
clockTimer = null
|
||||||
}
|
}
|
||||||
|
window.removeEventListener('click', handleGlobalClick)
|
||||||
|
window.removeEventListener('keydown', handleGlobalEsc)
|
||||||
|
if (menuTriggerTimer !== null) {
|
||||||
|
window.clearTimeout(menuTriggerTimer)
|
||||||
|
menuTriggerTimer = null
|
||||||
|
}
|
||||||
|
if (menuOpenTimer !== null) {
|
||||||
|
window.clearTimeout(menuOpenTimer)
|
||||||
|
menuOpenTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -247,11 +319,45 @@ onBeforeUnmount(() => {
|
|||||||
<div class="inner-outline diamond"></div>
|
<div class="inner-outline diamond"></div>
|
||||||
|
|
||||||
<div class="top-left-tools">
|
<div class="top-left-tools">
|
||||||
<button class="metal-circle" type="button" :disabled="leaveRoomPending" @click="backHall">☰</button>
|
<div class="menu-trigger-wrap">
|
||||||
|
<button
|
||||||
|
class="metal-circle menu-trigger"
|
||||||
|
:class="{ 'is-feedback': menuTriggerActive }"
|
||||||
|
type="button"
|
||||||
|
:disabled="leaveRoomPending"
|
||||||
|
@click.stop="toggleMenu"
|
||||||
|
>
|
||||||
|
<span class="menu-trigger-icon">☰</span>
|
||||||
|
</button>
|
||||||
|
<transition name="menu-pop">
|
||||||
|
<div v-if="menuOpen" class="menu-popover" @click.stop>
|
||||||
|
<div class="menu-list">
|
||||||
|
<button class="menu-item menu-item-delay-1" type="button" @click="toggleTrustMode">
|
||||||
|
<span class="menu-item-icon">
|
||||||
|
<img :src="robotIcon" alt="" />
|
||||||
|
</span>
|
||||||
|
<span>{{ isTrustMode ? '取消托管' : '托管' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="menu-item menu-item-danger menu-item-delay-2"
|
||||||
|
type="button"
|
||||||
|
:disabled="leaveRoomPending"
|
||||||
|
@click="handleLeaveRoom"
|
||||||
|
>
|
||||||
|
<span class="menu-item-icon">
|
||||||
|
<img :src="exitIcon" alt="" />
|
||||||
|
</span>
|
||||||
|
<span>{{ leaveRoomPending ? '退出中...' : '退出' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
<div class="left-counter">
|
<div class="left-counter">
|
||||||
<span class="counter-light"></span>
|
<span class="counter-light"></span>
|
||||||
<strong>{{ roomState.game?.state?.wall.length ?? 48 }}</strong>
|
<strong>{{ roomState.game?.state?.wall.length ?? 48 }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="isTrustMode" class="trust-chip">托管中</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="top-right-clock">
|
<div class="top-right-clock">
|
||||||
@@ -346,19 +452,6 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<p v-else class="empty-hand">等待服务端下发 `my_hand`。</p>
|
<p v-else class="empty-hand">等待服务端下发 `my_hand`。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-side-buttons">
|
|
||||||
<button class="side-round" type="button" @click="connectWs">聊</button>
|
|
||||||
<button class="side-round" type="button">赏</button>
|
|
||||||
<button
|
|
||||||
class="side-round gold"
|
|
||||||
type="button"
|
|
||||||
:disabled="!canStartGame || startGamePending"
|
|
||||||
@click="sendStartGame"
|
|
||||||
>
|
|
||||||
{{ startGamePending ? '开局中' : '开局' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user