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