diff --git a/src/assets/images/icons/exit.svg b/src/assets/images/icons/exit.svg new file mode 100644 index 0000000..5c23a53 --- /dev/null +++ b/src/assets/images/icons/exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/icons/robot.svg b/src/assets/images/icons/robot.svg new file mode 100644 index 0000000..0f4260c --- /dev/null +++ b/src/assets/images/icons/robot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/room.css b/src/assets/styles/room.css index 6d3123b..f82b70c 100644 --- a/src/assets/styles/room.css +++ b/src/assets/styles/room.css @@ -107,6 +107,27 @@ 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 { width: 50px; height: 50px; @@ -122,6 +143,106 @@ 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 { display: inline-flex; align-items: center; @@ -133,6 +254,38 @@ 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 { width: 16px; height: 24px; @@ -145,8 +298,8 @@ .top-right-clock { position: absolute; - top: 14px; - right: 16px; + top: 30px; + right: 36px; display: inline-flex; align-items: center; gap: 12px; diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index 5e74771..db60231 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -5,6 +5,8 @@ import deskImage from '../assets/images/desk/desk_01.png' import wanIcon from '../assets/images/flowerClolor/wan.png' import tongIcon from '../assets/images/flowerClolor/tong.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 topBackImage from '../assets/images/tiles/top/tbgs_2.png' import rightBackImage from '../assets/images/tiles/right/tbgs_1.png' @@ -42,6 +44,11 @@ const { const now = ref(Date.now()) 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(() => { if (roomState.value.status === 'playing') { @@ -220,10 +227,65 @@ function actionTheme(type: string): 'gold' | 'jade' | '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(() => { clockTimer = window.setInterval(() => { now.value = Date.now() }, 1000) + window.addEventListener('click', handleGlobalClick) + window.addEventListener('keydown', handleGlobalEsc) }) onBeforeUnmount(() => { @@ -231,6 +293,16 @@ onBeforeUnmount(() => { window.clearInterval(clockTimer) 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 + } }) @@ -247,11 +319,45 @@ onBeforeUnmount(() => {
- +
{{ roomState.game?.state?.wall.length ?? 48 }}
+ 托管中
@@ -346,19 +452,6 @@ onBeforeUnmount(() => {

等待服务端下发 `my_hand`。

- -
- - - -