diff --git a/client/src/components/game/GamePort.styles.css b/client/src/components/game/GamePort.styles.css index d601b38..e08b815 100644 --- a/client/src/components/game/GamePort.styles.css +++ b/client/src/components/game/GamePort.styles.css @@ -82,6 +82,7 @@ align-items: center; justify-content: center; transition: none !important; + border-radius: var(--radius-lg); } /* Тёмная тема */ @@ -260,6 +261,7 @@ padding: 1rem; background: linear-gradient(135deg, var(--color-surface) 0%, rgba(16, 185, 129, 0.1) 100%); position: relative; + border-radius: var(--radius-lg); } .active-port.bonus-active { @@ -367,16 +369,7 @@ left: 100%; } -/* Кнопка переноса */ -.move-btn { - background: linear-gradient(135deg, #3b82f6, #2563eb); - color: white; -} - -.move-btn:hover { - transform: scale(1.05); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4); -} +/* Кнопка переноса - удалена, используется flip-btn */ /* Игровая статистика */ .game-stats-layout { @@ -510,7 +503,7 @@ justify-content: center; font-weight: 600; font-size: 14px; - z-index: 2; + z-index: 10; box-shadow: var(--shadow-sm); } @@ -596,8 +589,9 @@ display: flex; gap: 0.5rem; position: absolute; - top: 0; - right: 0; + top: 8px; + right: 8px; + z-index: 100; } .header-btn { @@ -615,7 +609,7 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } -.header-btn:hover { +.header-btn:not(.flip-btn):hover { transform: scale(1.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } @@ -628,50 +622,84 @@ background: linear-gradient(135deg, #ef4444, #dc2626); } -.header-btn.move-btn { - background: linear-gradient(135deg, #3b82f6, #2563eb); -} +/* Стили move-btn перенесены в flip-btn */ .header-btn.delete-btn { background: linear-gradient(135deg, #ef4444, #dc2626); } -/* Flip button - эффект переворота карты */ -.flip-btn { +/* Flip button - эффект переворота карты 3D */ +.header-btn.flip-btn { + width: 40px; + height: 40px; + padding: 0; + border: none; + cursor: pointer; + background: transparent !important; position: relative; + box-shadow: none; perspective: 1000px; - transform-style: preserve-3d; } -.flip-front, -.flip-back { +/* Блокируем все hover эффекты для flip кнопки */ +.header-btn.flip-btn:hover { + transform: none !important; + box-shadow: none !important; + background: transparent !important; +} + +.header-btn.flip-btn .flip-inner { + position: relative; + width: 100%; + height: 100%; + text-align: center; + transition: transform 1s ease-in-out; + transform-style: preserve-3d; + -webkit-transform-style: preserve-3d; +} + +/* Когда flip-inner в состоянии flipped - переворачиваем контейнер */ +.header-btn.flip-btn .flip-inner.flipped { + transform: rotateY(180deg); + -webkit-transform: rotateY(180deg); +} + +/* Стороны карты */ +.header-btn.flip-btn .flip-front, +.header-btn.flip-btn .flip-back { position: absolute; top: 0; left: 0; width: 100%; height: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; display: flex; align-items: center; justify-content: center; - backface-visibility: hidden; - transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); + border-radius: var(--radius-md); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + font-weight: 600; } -.flip-front { +/* Передняя сторона - стрелки переноса */ +.header-btn.flip-btn .flip-front { + background: linear-gradient(135deg, #3b82f6, #2563eb); + color: white; + font-size: 18px; + z-index: 2; transform: rotateY(0deg); + -webkit-transform: rotateY(0deg); } -.flip-back { - transform: rotateY(180deg); +/* Задняя сторона - красный крестик */ +.header-btn.flip-btn .flip-back { background: linear-gradient(135deg, #ef4444, #dc2626); -} - -.flip-btn.flipped .flip-front { + color: white; + font-size: 22px; + font-weight: bold; transform: rotateY(180deg); -} - -.flip-btn.flipped .flip-back { - transform: rotateY(0deg); + -webkit-transform: rotateY(180deg); } .game-title { @@ -895,9 +923,7 @@ background: rgba(255, 255, 255, 0.3); } -.move-btn:hover { - background: rgba(59, 130, 246, 0.8); -} +/* Удалено - стили move-btn больше не используются */ .cancel-btn:hover, .delete-btn:hover { @@ -937,10 +963,21 @@ } /* Анимации переноса */ -.moving-source { - opacity: 0.5; +.moving-source::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 5; + pointer-events: none; + border-radius: var(--radius-lg); } +/* Кнопки и номер порта уже имеют z-index: 10 и 100 */ + .receiving-target { position: relative; } @@ -950,14 +987,9 @@ } .move-target { - border-color: #a855f7; + border: 3px solid #a855f7; background: rgba(168, 85, 247, 0.15); - animation: pulse-purple 2s infinite; -} - -@keyframes pulse-purple { - 0%, 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.7); } - 50% { box-shadow: 0 0 0 8px rgba(168, 85, 247, 0); } + box-shadow: 0 0 20px rgba(168, 85, 247, 0.5); } /* Drag & Drop стили */ @@ -973,18 +1005,8 @@ border-width: 3px !important; background: linear-gradient(135deg, rgba(168, 85, 247, 0.2) 0%, rgba(168, 85, 247, 0.1) 100%) !important; box-shadow: 0 0 30px rgba(168, 85, 247, 0.6), inset 0 0 20px rgba(168, 85, 247, 0.2) !important; - transform: scale(1.05); + transform: scale(1.02); transition: all 0.2s ease; - animation: drag-over-pulse 1s ease-in-out infinite; -} - -@keyframes drag-over-pulse { - 0%, 100% { - box-shadow: 0 0 30px rgba(168, 85, 247, 0.6), inset 0 0 20px rgba(168, 85, 247, 0.2); - } - 50% { - box-shadow: 0 0 40px rgba(168, 85, 247, 0.8), inset 0 0 30px rgba(168, 85, 247, 0.3); - } } .game-port-container[draggable="true"] { @@ -1063,13 +1085,18 @@ .move-overlay { position: absolute; inset: 0; - background: rgba(168, 85, 247, 0.9); + background: transparent; /* Убрали темный фон */ display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; z-index: 10; + pointer-events: none; /* Пропускаем клики через overlay */ +} + +.move-overlay > * { + pointer-events: auto; /* Но контент внутри кликабелен */ } .move-text { diff --git a/client/src/components/game/GamePort.vue b/client/src/components/game/GamePort.vue index a7785fa..fd81b14 100644 --- a/client/src/components/game/GamePort.vue +++ b/client/src/components/game/GamePort.vue @@ -8,7 +8,7 @@ moveClasses, { 'paused': systemState.pause, - 'moving-source': port.isMoving, + 'moving-source': isPortMoving, 'receiving-target': port.isReceiving, 'loading': port.isLoading, 'is-dragging': isDragging, @@ -47,12 +47,13 @@ @@ -108,12 +109,13 @@ @@ -268,6 +270,12 @@ const isTestGame = computed(() => { (!port.value.gamer || !port.value.gamer.gamerId) }) +// Проверка активен ли режим переноса для этого порта +const isPortMoving = computed(() => { + return moveState.movingPlayer === props.portNumber || + moveState.movingGame === props.portNumber +}) + // Время игры const updateGameTime = () => { // Не обновляем таймер при общей паузе или паузе порта @@ -408,6 +416,12 @@ const handlePortClick = (event) => { } const handleFreePortClick = () => { + // Блокировка если активен режим переноса + if (port.value.isMoving || moveState.movingPlayer !== null || moveState.movingGame !== null) { + window.notify?.warning('Внимание', 'Завершите перенос перед началом игры') + return + } + // Проверки системы if (!canStartGame.value) { if (!systemState.smena) { @@ -739,6 +753,11 @@ onUnmounted(() => { }) + + \ No newline at end of file diff --git a/client/src/components/game/GamePortFlip.css b/client/src/components/game/GamePortFlip.css new file mode 100644 index 0000000..6657f45 --- /dev/null +++ b/client/src/components/game/GamePortFlip.css @@ -0,0 +1,72 @@ +/* Flip button 3D анимация - отдельный файл БЕЗ scoped */ +.header-btn.flip-btn { + width: 40px !important; + height: 40px !important; + padding: 0 !important; + border: none !important; + cursor: pointer !important; + background: transparent !important; + position: relative !important; + box-shadow: none !important; + perspective: 1000px !important; + transform: none !important; + filter: none !important; +} + +.header-btn.flip-btn:hover { + transform: none !important; + box-shadow: none !important; + background: transparent !important; + filter: none !important; +} + +.header-btn.flip-btn .flip-inner { + position: relative !important; + width: 100% !important; + height: 100% !important; + transition: transform 0.6s ease-in-out !important; + transform-style: preserve-3d !important; + will-change: transform !important; +} + +.header-btn.flip-btn .flip-inner.flipped { + transform: rotateY(180deg) !important; +} + +.header-btn.flip-btn .flip-front, +.header-btn.flip-btn .flip-back { + position: absolute !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + backface-visibility: hidden !important; + -webkit-backface-visibility: hidden !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + border-radius: 8px !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; + font-weight: 600 !important; +} + +.header-btn.flip-btn .flip-front { + background: linear-gradient(135deg, #3b82f6, #2563eb) !important; + color: white !important; + font-size: 18px !important; + transform: rotateY(0deg) !important; +} + +.header-btn.flip-btn .flip-back { + background: linear-gradient(135deg, #ef4444, #dc2626) !important; + color: white !important; + font-size: 22px !important; + font-weight: bold !important; + transform: rotateY(180deg) !important; +} + +/* Убираем все что может сломать 3D контекст */ +.header-controls { + transform: none !important; + filter: none !important; +} diff --git a/client/src/components/ui/QuickActionsMenu.vue b/client/src/components/ui/QuickActionsMenu.vue index 7cab7a5..3c6be98 100644 --- a/client/src/components/ui/QuickActionsMenu.vue +++ b/client/src/components/ui/QuickActionsMenu.vue @@ -91,36 +91,117 @@ const emit = defineEmits(['item-click', 'menu-toggle']) const isOpen = ref(false) const isClosing = ref(false) +// Умная система прерывания анимации +const animationState = ref('idle') // 'idle' | 'opening' | 'closing' +const animationTimeoutId = ref(null) +const animationStartTime = ref(0) + +const startOpeningAnimation = () => { + animationState.value = 'opening' + animationStartTime.value = Date.now() + isOpen.value = true + isClosing.value = false + emit('menu-toggle', true) + + // Длительность открытия: максимальная задержка элементов + длительность анимации + const openDuration = (props.menuItems.length - 1) * 80 + 600 + + if (animationTimeoutId.value) { + clearTimeout(animationTimeoutId.value) + } + + animationTimeoutId.value = setTimeout(() => { + animationState.value = 'idle' + animationTimeoutId.value = null + }, openDuration) +} + +const startClosingAnimation = () => { + animationState.value = 'closing' + animationStartTime.value = Date.now() + + requestAnimationFrame(() => { + isClosing.value = true + }) + + const closeDuration = 400 + + if (animationTimeoutId.value) { + clearTimeout(animationTimeoutId.value) + } + + animationTimeoutId.value = setTimeout(() => { + isOpen.value = false + isClosing.value = false + animationState.value = 'idle' + animationTimeoutId.value = null + emit('menu-toggle', false) + }, closeDuration) +} + const toggleMenu = () => { - if (isOpen.value) { - // Закрываем - используем requestAnimationFrame для чистого reflow - requestAnimationFrame(() => { - isClosing.value = true - }) - // Все элементы возвращаются одновременно, только длительность анимации 400ms - const maxDelay = 400 - setTimeout(() => { - isOpen.value = false + const now = Date.now() + const elapsed = now - animationStartTime.value + + // Если анимация открытия идёт + if (animationState.value === 'opening') { + const openDuration = (props.menuItems.length - 1) * 80 + 600 + const progress = Math.min(elapsed / openDuration, 1) + + if (progress < 0.3) { + // Быстрый реверс - мгновенно закрываем + clearTimeout(animationTimeoutId.value) + startClosingAnimation() + } else { + // Даём досказать открытие, потом закрываем + clearTimeout(animationTimeoutId.value) + const remaining = Math.max(0, openDuration - elapsed) + + animationTimeoutId.value = setTimeout(() => { + startClosingAnimation() + }, remaining) + } + return + } + + // Если анимация закрытия идёт + if (animationState.value === 'closing') { + const closeDuration = 400 + const progress = Math.min(elapsed / closeDuration, 1) + + if (progress < 0.3) { + // Быстрый реверс - мгновенно открываем + clearTimeout(animationTimeoutId.value) isClosing.value = false - emit('menu-toggle', false) - }, maxDelay) + startOpeningAnimation() + } else { + // Даём досказать закрытие, потом открываем + clearTimeout(animationTimeoutId.value) + const remaining = Math.max(0, closeDuration - elapsed) + + animationTimeoutId.value = setTimeout(() => { + startOpeningAnimation() + }, remaining) + } + return + } + + // Обычное переключение из idle состояния + if (isOpen.value) { + startClosingAnimation() } else { - // Открываем - isOpen.value = true - emit('menu-toggle', true) + startOpeningAnimation() } } const closeMenu = () => { - requestAnimationFrame(() => { - isClosing.value = true - }) - const maxDelay = 400 - setTimeout(() => { - isOpen.value = false - isClosing.value = false - emit('menu-toggle', false) - }, maxDelay) + // При клике на overlay всегда закрываем (не используем умную логику) + if (animationState.value === 'opening' || animationState.value === 'idle') { + if (animationTimeoutId.value) { + clearTimeout(animationTimeoutId.value) + } + startClosingAnimation() + } } const handleItemClick = (item) => { diff --git a/server/data/avt.ini b/server/data/avt.ini index 97ae109..b58daee 100644 --- a/server/data/avt.ini +++ b/server/data/avt.ini @@ -6,9 +6,9 @@ "tel": 79026527096, "group": [ 3, - 5, 4, - 6 + 6, + 5 ], "rdata": 1722848687, "color": "#4965ca", @@ -35,7 +35,7 @@ }, "info": { "setadmin": 8, - "setdata": 1759755500 + "setdata": 1759821739 }, "coord": { "x": 223, diff --git a/server/data/game.ini b/server/data/game.ini index 8f65aec..d0508e0 100644 --- a/server/data/game.ini +++ b/server/data/game.ini @@ -1 +1 @@ -{"0":{"_testing":12345,"pause":false,"gamersCounter":2,"init":null,"sharik":{"playersCounter":1,"startTime":null,"playerId":null},"smena":{"adminId":183,"stime":"2025-10-07T07:20:08.881Z","money_razm_open":5000,"comm_open":"123123","adminName":"Соломин Вадим"}},"1":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"2":{"game":0,"patr":0,"patrOk":0,"patrAdd":0,"gamer":{"gamerId":null,"games":[],"gameCounter":0},"startTime":null,"timeOut":false,"canHaveBonus":false,"isBonus":false},"3":{"game":0,"patr":0,"patrOk":0,"patrAdd":0,"gamer":{"gamerId":null,"games":[],"gameCounter":0},"startTime":null,"timeOut":false,"canHaveBonus":false,"isBonus":false},"4":{"game":0,"gamer":{"gamerId":2,"games":[],"gameCounter":0},"timeOut":false,"canHaveBonus":false},"5":{"game":0,"gamer":{"gamerId":1,"games":[],"gameCounter":0},"timeOut":false,"canHaveBonus":false},"6":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false,"canHaveBonus":false}} \ No newline at end of file +{"0":{"_testing":12345,"pause":false,"gamersCounter":3,"init":null,"sharik":{"playersCounter":1,"startTime":null,"playerId":null},"smena":{"adminId":183,"stime":"2025-10-07T07:20:08.881Z","money_razm_open":5000,"comm_open":"123123","adminName":"Соломин Вадим"}},"1":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"2":{"game":0,"patr":0,"patrOk":0,"patrAdd":0,"gamer":{"gamerId":1,"games":[],"gameCounter":0},"startTime":null,"timeOut":false,"canHaveBonus":false,"isBonus":false},"3":{"game":0,"gamer":{"gamerId":3,"games":[],"gameCounter":0},"timeOut":false,"canHaveBonus":false},"4":{"game":0,"gamer":{"gamerId":2,"games":[],"gameCounter":0},"timeOut":false,"canHaveBonus":false},"5":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false,"canHaveBonus":false},"6":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false,"canHaveBonus":false}} \ No newline at end of file diff --git a/server/data/hash.ini b/server/data/hash.ini index 2cad531..c719386 100644 --- a/server/data/hash.ini +++ b/server/data/hash.ini @@ -1 +1 @@ -{"79616613126":"E2JEmCcy5CEC1sO","79026527096":"8668fc8a3692ecf6"} \ No newline at end of file +{"79616613126":"TKi9iK1XrEsmr1J","79026527096":"8668fc8a3692ecf6"} \ No newline at end of file diff --git a/server/data/log.ini b/server/data/log.ini index c705d5c..057f3a5 100644 --- a/server/data/log.ini +++ b/server/data/log.ini @@ -1 +1 @@ -{"day":"2025-10-07T10:00:50.183Z","num":9} \ No newline at end of file +{"day":"2025-10-07T10:00:50.183Z","num":14} \ No newline at end of file diff --git a/server/data/ver.ini b/server/data/ver.ini index 822df97..8b8fbfe 100644 --- a/server/data/ver.ini +++ b/server/data/ver.ini @@ -1 +1 @@ -{"admin":44,"info":418} \ No newline at end of file +{"admin":44,"info":420} \ No newline at end of file