diff --git a/client/src/components/ui/QuickActionsMenu.vue b/client/src/components/ui/QuickActionsMenu.vue index 86d7e14..7cab7a5 100644 --- a/client/src/components/ui/QuickActionsMenu.vue +++ b/client/src/components/ui/QuickActionsMenu.vue @@ -1,5 +1,21 @@ @@ -70,15 +89,38 @@ const props = defineProps({ const emit = defineEmits(['item-click', 'menu-toggle']) const isOpen = ref(false) +const isClosing = ref(false) const toggleMenu = () => { - isOpen.value = !isOpen.value - emit('menu-toggle', isOpen.value) + if (isOpen.value) { + // Закрываем - используем requestAnimationFrame для чистого reflow + requestAnimationFrame(() => { + isClosing.value = true + }) + // Все элементы возвращаются одновременно, только длительность анимации 400ms + const maxDelay = 400 + setTimeout(() => { + isOpen.value = false + isClosing.value = false + emit('menu-toggle', false) + }, maxDelay) + } else { + // Открываем + isOpen.value = true + emit('menu-toggle', true) + } } const closeMenu = () => { - isOpen.value = false - emit('menu-toggle', false) + requestAnimationFrame(() => { + isClosing.value = true + }) + const maxDelay = 400 + setTimeout(() => { + isOpen.value = false + isClosing.value = false + emit('menu-toggle', false) + }, maxDelay) } const handleItemClick = (item) => { @@ -91,23 +133,30 @@ const handleItemClick = (item) => { // Вычисляем позицию для каждого элемента по полукругу const getItemStyle = (index) => { const total = props.menuItems.length - const radius = 150 // Радиус от центра + const radius = 220 // Увеличен радиус с 150 до 220 - // Угол распределяется по полукругу (180 градусов) + // Угол распределяется по полукругу (180 градусов) СВЕРХУ + // Начинаем с -180° (слева) и заканчиваем 0° (справа) const angleStep = 180 / (total + 1) - const angle = (angleStep * (index + 1)) - 90 // -90 чтобы начать с левого края + const angle = -180 + (angleStep * (index + 1)) // От -180° до 0° const radian = (angle * Math.PI) / 180 const x = Math.cos(radian) * radius - const y = Math.sin(radian) * radius + const y = Math.sin(radian) * radius // Будет отрицательным (вверх) // Задержка для каскадной анимации - const delay = index * 50 // миллисекунды + const delay = index * 80 // миллисекунды + + // Нормализуем угол для передачи в CSS (для эффекта растягивания) + const normalizedAngle = angle + 180 // От 0° до 180° return { '--x': `${x}px`, '--y': `${y}px`, - '--delay': `${delay}ms` + '--delay': `${delay}ms`, + '--index': index, + '--total': total, + '--angle': `${normalizedAngle}deg` // Направление для растягивания кнопки } } @@ -132,6 +181,13 @@ onUnmounted(() => { position: relative; } +/* Контейнер с эффектом gooey (липкий пузырь) */ +.gooey-container { + position: relative; + filter: url(#gooey-filter); + overflow: visible; +} + /* Полукруг с временем (главная кнопка) */ .time-semicircle { width: 200px; @@ -142,6 +198,7 @@ onUnmounted(() => { align-items: center; justify-content: center; position: relative; + z-index: 110; /* Кнопка выше элементов меню */ box-shadow: 0 -4px 20px rgba(102, 126, 234, 0.3); cursor: pointer; transition: all var(--transition-fast); @@ -149,15 +206,30 @@ onUnmounted(() => { padding: 0; } +/* Невидимая копия кнопки для эффекта gooey */ +.time-semicircle-ghost { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 200px; + height: 100px; + background: linear-gradient(145deg, var(--color-primary), var(--color-primary-hover)); + border-radius: 200px 200px 0 0; + pointer-events: none; + z-index: 95; +} + +/* Убрана анимация при наведении */ .time-semicircle:hover { - transform: translateY(-4px); - box-shadow: 0 -6px 24px rgba(102, 126, 234, 0.4); + /* transform: translateY(-4px); */ + /* box-shadow: 0 -6px 24px rgba(102, 126, 234, 0.4); */ } .time-semicircle--active { background: linear-gradient(145deg, var(--color-primary-hover), var(--color-primary)); - transform: translateY(-4px) scale(1.05); box-shadow: 0 -8px 32px rgba(102, 126, 234, 0.5); + /* Убрана анимация растягивания */ } .current-time { @@ -177,22 +249,24 @@ onUnmounted(() => { left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - z-index: 999; + background: transparent; /* Убираем затемнение */ + z-index: 105; /* Ниже кнопки времени (110) */ display: flex; align-items: flex-end; justify-content: center; animation: fadeIn 0.3s ease-out; + pointer-events: auto; /* Разрешаем клики для закрытия */ } -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } +/* Анимация overlay */ +.overlay-fade-enter-active, +.overlay-fade-leave-active { + transition: opacity 0.3s ease; +} + +.overlay-fade-enter-from, +.overlay-fade-leave-to { + opacity: 0; } /* Контейнер радиального меню */ @@ -203,13 +277,18 @@ onUnmounted(() => { display: flex; align-items: flex-end; justify-content: center; - margin-bottom: 100px; /* Отступ для футера */ + margin-bottom: 0; /* Убираем отступ - элементы должны начинаться с линии низа кнопки */ + pointer-events: none; /* Разрешаем клики проходить сквозь контейнер */ +} + +.radial-menu > * { + pointer-events: auto; /* Но сами пункты меню кликабельны */ } /* Элементы радиального меню */ .radial-item { position: absolute; - bottom: 0; + bottom: -40px; /* Смещаем вниз на половину высоты элемента (80px / 2), чтобы центр был на линии bottom:0 */ left: 50%; width: 80px; height: 80px; @@ -217,36 +296,47 @@ onUnmounted(() => { border: 3px solid var(--color-primary); border-radius: 50%; cursor: pointer; - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); - transform-origin: center bottom; + transform-origin: center center; - /* Начальная позиция - в центре */ - transform: translate(-50%, 0) scale(0); - opacity: 0; - - /* Анимация появления */ - animation: bubblePop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + /* Анимация вылета из центра */ + animation: bubbleFlyOut 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; animation-delay: var(--delay); } -@keyframes bubblePop { +/* Анимация закрытия - возврат в центр */ +.radial-item--closing { + animation: bubbleFlyIn 0.4s cubic-bezier(0.32, 0, 0.67, 0) both; + /* Все элементы возвращаются одновременно */ + animation-delay: 0ms; +} + +@keyframes bubbleFlyOut { 0% { - transform: translate(-50%, 0) scale(0); + transform: translate(-50%, 40px) scale(0.3); opacity: 0; } - 60% { - transform: translate(calc(-50% + var(--x)), calc(var(--y) - 20px)) scale(1.2); + 50% { opacity: 1; } 100% { - transform: translate(calc(-50% + var(--x)), var(--y)) scale(1); + transform: translate(calc(-50% + var(--x)), calc(40px + var(--y))) scale(1); opacity: 1; } } +@keyframes bubbleFlyIn { + 0% { + transform: translate(calc(-50% + var(--x)), calc(40px + var(--y))) scale(1); + } + 100% { + transform: translate(-50%, 40px) scale(0.3); + } +} + .radial-item:hover:not(.radial-item--disabled) { - transform: translate(calc(-50% + var(--x)), var(--y)) scale(1.15); + transition: all 0.2s ease; /* Только для hover */ + transform: translate(calc(-50% + var(--x)), calc(40px + var(--y))) scale(1.15); box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4); border-color: var(--color-primary-hover); background: linear-gradient(145deg, var(--color-primary), var(--color-primary-hover)); diff --git a/client/src/composables/game/useSharik.js b/client/src/composables/game/useSharik.js index 2191431..c363593 100644 --- a/client/src/composables/game/useSharik.js +++ b/client/src/composables/game/useSharik.js @@ -300,6 +300,17 @@ export function useSharik() { } } + /** + * Сброс состояния шарика (при закрытии/открытии смены) + */ + const resetSharikState = () => { + sharikState.value.playersCounter = 0 + sharikState.value.playerId = null + sharikState.value.startTime = null + stopTimer() + logger.info('Шарик: состояние сброшено') + } + // Следим за изменением активности для управления таймером watch(isActive, (newVal) => { if (newVal) { @@ -325,6 +336,7 @@ export function useSharik() { handleSharikMessage, startTimer, stopTimer, - initSharikState + initSharikState, + resetSharikState } } diff --git a/client/src/composables/shift/useShiftOperations.js b/client/src/composables/shift/useShiftOperations.js index 5337917..1a465bf 100644 --- a/client/src/composables/shift/useShiftOperations.js +++ b/client/src/composables/shift/useShiftOperations.js @@ -150,6 +150,9 @@ export function useShiftOperations() { // Отправляем глобальное событие для обновления systemState в useGamePorts window.dispatchEvent(new CustomEvent('shift-status', { detail: { isOpen: true, shift: response.smena } })) + // Отправляем событие для сброса состояния шарика при открытии смены + window.dispatchEvent(new CustomEvent('shift-opened')) + return { success: true, shift: response.smena } } else if (response.do === 'err') { // Ошибка открытия смены @@ -244,6 +247,9 @@ export function useShiftOperations() { // Отправляем глобальное событие для обновления systemState в useGamePorts window.dispatchEvent(new CustomEvent('shift-status', { detail: { isOpen: false, shift: null } })) + // Отправляем событие для сброса состояния шарика + window.dispatchEvent(new CustomEvent('shift-closed')) + return { success: true } } else if (response.do === 'err') { return { success: false, error: response.err } diff --git a/client/src/pages/HomePage.vue b/client/src/pages/HomePage.vue index 5c8e333..d4412b7 100644 --- a/client/src/pages/HomePage.vue +++ b/client/src/pages/HomePage.vue @@ -435,7 +435,7 @@ const { } = useShiftOperations() // Шарик -const { handleSharikMessage } = useSharik() +const { handleSharikMessage, resetSharikState } = useSharik() const { showShiftModal, @@ -701,7 +701,7 @@ const quickActionsItems = computed(() => { id: 'stats', icon: '📊', label: 'Статистика', - disabled: true // Пока отключено + disabled: false } ] }) @@ -712,10 +712,13 @@ const handleQuickAction = (item) => { showSharikModal.value = true break case 'pause': - handlePauseClick() + togglePause() break case 'stats': - // Здесь будет открытие статистики + notification.value = { + message: '📊 Функция статистики в разработке', + type: 'info' + } break default: break @@ -958,11 +961,17 @@ onMounted(async () => { handleSharikMessage(message) } } + + // Слушаем события закрытия/открытия смены для сброса состояния шарика + window.addEventListener('shift-closed', resetSharikState) + window.addEventListener('shift-opened', resetSharikState) }) onUnmounted(() => { // Очищаем обработчик при размонтировании window.systemMessageHandler = null + window.removeEventListener('shift-closed', resetSharikState) + window.removeEventListener('shift-opened', resetSharikState) }) diff --git a/server/data/avt.ini b/server/data/avt.ini index 57ac24f..97ae109 100644 --- a/server/data/avt.ini +++ b/server/data/avt.ini @@ -6,8 +6,9 @@ "tel": 79026527096, "group": [ 3, - 6, - 5 + 5, + 4, + 6 ], "rdata": 1722848687, "color": "#4965ca", @@ -34,7 +35,7 @@ }, "info": { "setadmin": 8, - "setdata": 1759753247 + "setdata": 1759755500 }, "coord": { "x": 223, diff --git a/server/data/game.ini b/server/data/game.ini index 1319eff..8f65aec 100644 --- a/server/data/game.ini +++ b/server/data/game.ini @@ -1 +1 @@ -{"0":{"_testing":12345,"pause":false,"gamersCounter":2,"init":null,"sharik":{"playersCounter":20,"startTime":"2025-10-06T12:51:13.848Z","playerId":20},"smena":{"adminId":183,"stime":"2025-10-02T14:36:17.583Z","money_razm_open":5000,"comm_open":"123123","adminName":"Соломин Вадим"}},"1":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"2":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"3":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false,"canHaveBonus":false},"4":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"5":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false},"6":{"game":0,"gamer":{"gamerId":null,"games":[]},"timeOut":false,"canHaveBonus":false}} \ No newline at end of file +{"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 diff --git a/server/data/hash.ini b/server/data/hash.ini index 5c5c783..2cad531 100644 --- a/server/data/hash.ini +++ b/server/data/hash.ini @@ -1 +1 @@ -{"79616613126":"Xr17fURRkzaPMuv","79026527096":"8668fc8a3692ecf6"} \ No newline at end of file +{"79616613126":"E2JEmCcy5CEC1sO","79026527096":"8668fc8a3692ecf6"} \ No newline at end of file diff --git a/server/data/log.ini b/server/data/log.ini index 540c8ac..c705d5c 100644 --- a/server/data/log.ini +++ b/server/data/log.ini @@ -1 +1 @@ -{"day":"2025-10-06T10:00:50.183Z","num":10} \ No newline at end of file +{"day":"2025-10-07T10:00:50.183Z","num":9} \ No newline at end of file