fix(player): 修复顶部语言/余额/头像下拉 hover 间隙导致菜单提前关闭
新增 useHoverDropdown,为下拉菜单增加延迟关闭与透明桥接层,鼠标可从触发按钮顺利移入下拉卡片。
This commit is contained in:
@@ -4,11 +4,12 @@ import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import { useHoverDropdown } from '../composables/useHoverDropdown';
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
const router = useRouter();
|
||||
const open = ref(false);
|
||||
const { open, openHover, scheduleClose, closeNow } = useHoverDropdown();
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
const wallet = computed(() => profileRaw.value?.wallet ?? null);
|
||||
@@ -44,7 +45,12 @@ function toggle() {
|
||||
}
|
||||
|
||||
function close() {
|
||||
open.value = false;
|
||||
closeNow();
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
openHover();
|
||||
void refreshProfile();
|
||||
}
|
||||
|
||||
function goRecharge() {
|
||||
@@ -53,7 +59,7 @@ function goRecharge() {
|
||||
}
|
||||
|
||||
function onOutsideClick(e: Event) {
|
||||
if (!root.value?.contains(e.target as Node)) open.value = false;
|
||||
if (!root.value?.contains(e.target as Node)) closeNow();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -71,8 +77,8 @@ onUnmounted(() => {
|
||||
<div
|
||||
ref="root"
|
||||
class="cash-chip-wrap"
|
||||
@mouseenter="open = true; refreshProfile()"
|
||||
@mouseleave="open = false"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="scheduleClose"
|
||||
>
|
||||
<button type="button" class="cash-chip" @click="toggle">
|
||||
<span class="chip-body">
|
||||
@@ -114,6 +120,16 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
--dropdown-gap: 8px;
|
||||
}
|
||||
|
||||
.cash-chip-wrap::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--dropdown-gap);
|
||||
}
|
||||
|
||||
.direct-recharge-btn {
|
||||
@@ -193,7 +209,7 @@ onUnmounted(() => {
|
||||
|
||||
.cash-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
top: calc(100% + var(--dropdown-gap));
|
||||
right: 0;
|
||||
width: min(260px, 78vw);
|
||||
padding: 12px 14px;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import LocaleFlag from './LocaleFlag.vue';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import { useHoverDropdown } from '../composables/useHoverDropdown';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -14,7 +15,7 @@ withDefaults(
|
||||
const { locale } = useI18n();
|
||||
const { locales, setLocale } = useAppLocale();
|
||||
|
||||
const open = ref(false);
|
||||
const { open, openHover, scheduleClose, closeNow } = useHoverDropdown();
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
const currentLabel = computed(
|
||||
@@ -22,7 +23,7 @@ const currentLabel = computed(
|
||||
);
|
||||
|
||||
async function pick(code: string) {
|
||||
open.value = false;
|
||||
closeNow();
|
||||
await setLocale(code);
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ function toggle() {
|
||||
}
|
||||
|
||||
function onOutsideClick(e: Event) {
|
||||
if (!root.value?.contains(e.target as Node)) open.value = false;
|
||||
if (!root.value?.contains(e.target as Node)) closeNow();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -49,8 +50,8 @@ onUnmounted(() => {
|
||||
ref="root"
|
||||
class="locale-switch"
|
||||
:class="{ compact, open }"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
@mouseenter="openHover"
|
||||
@mouseleave="scheduleClose"
|
||||
>
|
||||
<button type="button" class="locale-trigger" :aria-expanded="open" aria-haspopup="listbox" @click.stop="toggle">
|
||||
<LocaleFlag :locale="locale" :size="compact ? 16 : 18" />
|
||||
@@ -77,6 +78,16 @@ onUnmounted(() => {
|
||||
z-index: 120;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
--dropdown-gap: 4px;
|
||||
}
|
||||
|
||||
.locale-switch::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--dropdown-gap);
|
||||
}
|
||||
|
||||
.locale-trigger {
|
||||
@@ -105,7 +116,7 @@ onUnmounted(() => {
|
||||
|
||||
.locale-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
top: calc(100% + var(--dropdown-gap));
|
||||
right: 0;
|
||||
z-index: 130;
|
||||
min-width: 100%;
|
||||
|
||||
@@ -5,12 +5,13 @@ import { useI18n } from 'vue-i18n';
|
||||
import { playerAvatarUrl, randomAvatarKey } from '@thebet365/shared';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import { useHoverDropdown } from '../composables/useHoverDropdown';
|
||||
|
||||
const { t } = useI18n();
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
const { avatarUrl } = usePlayerProfile();
|
||||
const open = ref(false);
|
||||
const { open, openHover, scheduleClose, closeNow } = useHoverDropdown();
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
const displayAvatarUrl = computed(() => {
|
||||
@@ -24,11 +25,11 @@ function toggle() {
|
||||
}
|
||||
|
||||
function close() {
|
||||
open.value = false;
|
||||
closeNow();
|
||||
}
|
||||
|
||||
function onOutsideClick(e: Event) {
|
||||
if (!root.value?.contains(e.target as Node)) open.value = false;
|
||||
if (!root.value?.contains(e.target as Node)) closeNow();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -61,8 +62,8 @@ function logout() {
|
||||
<div
|
||||
ref="root"
|
||||
class="avatar-wrap"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
@mouseenter="openHover"
|
||||
@mouseleave="scheduleClose"
|
||||
>
|
||||
<button type="button" class="avatar-btn" :aria-expanded="open" @click="toggle">
|
||||
<img v-if="displayAvatarUrl" :src="displayAvatarUrl" alt="" class="avatar-img" />
|
||||
@@ -81,6 +82,16 @@ function logout() {
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
z-index: 130;
|
||||
--dropdown-gap: 8px;
|
||||
}
|
||||
|
||||
.avatar-wrap::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--dropdown-gap);
|
||||
}
|
||||
|
||||
.avatar-btn {
|
||||
@@ -114,7 +125,7 @@ function logout() {
|
||||
|
||||
.avatar-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
top: calc(100% + var(--dropdown-gap));
|
||||
right: 0;
|
||||
min-width: 168px;
|
||||
padding: 8px 0;
|
||||
|
||||
42
apps/player/src/composables/useHoverDropdown.ts
Normal file
42
apps/player/src/composables/useHoverDropdown.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ref, onUnmounted } from 'vue';
|
||||
|
||||
export function useHoverDropdown(delay = 200) {
|
||||
const open = ref(false);
|
||||
let closeTimer: number | undefined;
|
||||
|
||||
function cancelClose() {
|
||||
if (closeTimer !== undefined) {
|
||||
window.clearTimeout(closeTimer);
|
||||
closeTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function openHover() {
|
||||
cancelClose();
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function scheduleClose() {
|
||||
cancelClose();
|
||||
closeTimer = window.setTimeout(() => {
|
||||
open.value = false;
|
||||
closeTimer = undefined;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function closeNow() {
|
||||
cancelClose();
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelClose();
|
||||
});
|
||||
|
||||
return {
|
||||
open,
|
||||
openHover,
|
||||
scheduleClose,
|
||||
closeNow,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user