1
0
Files
TaskBoard/front_vue/src/views/LoginPage.vue
Falknat e8a4480747 Важный фикс
Забыл добавить управление отделами :)
2026-01-18 20:45:17 +07:00

1039 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="login-page" :class="{ 'is-mobile': isMobile }">
<!-- Левитирующие иконки на фоне -->
<div class="floating-icons">
<div class="float-icon icon-1"><i data-lucide="check-square"></i></div>
<div class="float-icon icon-2"><i data-lucide="clipboard-list"></i></div>
<div class="float-icon icon-3"><i data-lucide="calendar"></i></div>
<div class="float-icon icon-4"><i data-lucide="users"></i></div>
<div class="float-icon icon-5"><i data-lucide="folder"></i></div>
<div class="float-icon icon-6"><i data-lucide="target"></i></div>
<div class="float-icon icon-7"><i data-lucide="star"></i></div>
<div class="float-icon icon-8"><i data-lucide="zap"></i></div>
<div class="float-icon icon-9"><i data-lucide="flag"></i></div>
<div class="float-icon icon-10"><i data-lucide="layers"></i></div>
<div class="float-icon icon-11"><i data-lucide="clock"></i></div>
<div class="float-icon icon-12"><i data-lucide="message-circle"></i></div>
</div>
<!-- Градиентные сферы на фоне -->
<div class="bg-glow glow-2"></div>
<div class="bg-glow glow-3"></div>
<!-- Контент авторизации -->
<div class="login-content" :class="{ 'is-loading': isProcessing, 'is-success': showSuccess }">
<!-- Состояние успеха -->
<Transition name="success-fade">
<div v-if="showSuccess" class="success-state">
<div class="success-icon">
<i data-lucide="check"></i>
</div>
<h1 class="success-title">{{ isRegisterMode ? 'Регистрация завершена!' : 'Добро пожаловать!' }}</h1>
<p class="success-text">{{ isRegisterMode ? 'Сейчас вы будете перенаправлены' : 'Авторизация прошла успешно' }}</p>
<div class="success-loader">
<span></span><span></span><span></span>
</div>
</div>
</Transition>
<!-- Форма входа / регистрации -->
<Transition name="form-fade">
<div v-if="!showSuccess" class="login-form-wrapper">
<!-- Логотип -->
<div class="login-header">
<div class="logo-icon">
<i data-lucide="layout-grid"></i>
</div>
<h1 class="login-title">TaskBoard</h1>
<p class="login-subtitle">
{{ isRegisterMode ? 'Создайте аккаунт для работы с задачами' : 'Войдите в систему управления задачами' }}
</p>
</div>
<!-- Форма входа -->
<form v-if="!isRegisterMode" @submit.prevent="handleLogin" class="login-form">
<!-- Поле логина -->
<div class="input-group" :class="{ 'has-value': login, 'has-focus': loginFocused }">
<div class="input-icon">
<i data-lucide="user"></i>
</div>
<input
type="text"
v-model="login"
@focus="loginFocused = true"
@blur="loginFocused = false"
placeholder="Введите логин"
autocomplete="username"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Поле пароля -->
<div class="input-group" :class="{ 'has-value': password, 'has-focus': passwordFocused }">
<div class="input-icon">
<i data-lucide="lock"></i>
</div>
<input
type="password"
v-model="password"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
placeholder="Введите пароль"
autocomplete="current-password"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Ошибка -->
<Transition name="error-shake">
<div v-if="error" class="error-message">
<i data-lucide="alert-circle"></i>
<span>{{ error }}</span>
</div>
</Transition>
<!-- Кнопка входа -->
<button
type="submit"
class="login-btn"
:class="{ 'is-loading': isProcessing }"
:disabled="isProcessing || !login || !password"
>
<span class="btn-text">Войти</span>
<span class="btn-loader">
<span></span><span></span><span></span>
</span>
<i data-lucide="arrow-right" class="btn-arrow"></i>
</button>
<!-- Ссылка на регистрацию -->
<div class="form-switch">
<span>Нет аккаунта?</span>
<button type="button" class="switch-btn" @click="switchToRegister">Зарегистрироваться</button>
</div>
</form>
<!-- Форма регистрации -->
<form v-else @submit.prevent="handleRegister" class="login-form">
<!-- Логин -->
<div class="input-group" :class="{ 'has-value': regUsername, 'has-focus': regUsernameFocused, 'has-error': errors.username, 'shake': shakeUsername }">
<div class="input-icon">
<i data-lucide="at-sign"></i>
</div>
<input
type="text"
v-model="regUsername"
@focus="regUsernameFocused = true"
@blur="regUsernameFocused = false"
@input="filterUsername"
placeholder="Придумайте логин"
autocomplete="username"
maxlength="32"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Имя -->
<div class="input-group" :class="{ 'has-value': regName, 'has-focus': regNameFocused, 'has-error': errors.name, 'shake': shakeName }">
<div class="input-icon">
<i data-lucide="user"></i>
</div>
<input
type="text"
v-model="regName"
@focus="regNameFocused = true"
@blur="regNameFocused = false"
@input="filterName"
placeholder="Ваше имя"
autocomplete="name"
maxlength="50"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Пароль -->
<div class="input-group" :class="{ 'has-value': regPassword, 'has-focus': regPasswordFocused, 'has-error': errors.password }">
<div class="input-icon">
<i data-lucide="lock"></i>
</div>
<input
type="password"
v-model="regPassword"
@focus="regPasswordFocused = true"
@blur="regPasswordFocused = false"
@input="clearPasswordError"
placeholder="Придумайте пароль"
autocomplete="new-password"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Telegram (опционально) -->
<div class="input-group" :class="{ 'has-value': regTelegram.length > 0, 'has-focus': regTelegramFocused, 'shake': shakeTelegram }">
<div class="input-icon">
<i data-lucide="send"></i>
</div>
<input
type="text"
v-model="regTelegram"
@focus="regTelegramFocused = true"
@blur="regTelegramFocused = false"
@input="filterTelegram"
@keydown="preventDeleteAt"
placeholder="@telegram (необязательно)"
autocomplete="off"
:disabled="isProcessing"
>
<div class="input-line"></div>
</div>
<!-- Ошибка -->
<Transition name="error-shake">
<div v-if="error" class="error-message">
<i data-lucide="alert-circle"></i>
<span>{{ error }}</span>
</div>
</Transition>
<!-- Кнопка регистрации -->
<button
type="submit"
class="login-btn"
:class="{ 'is-loading': isProcessing }"
:disabled="isProcessing || !regName || !regUsername || !regPassword"
>
<span class="btn-text">Зарегистрироваться</span>
<span class="btn-loader">
<span></span><span></span><span></span>
</span>
<i data-lucide="arrow-right" class="btn-arrow"></i>
</button>
<!-- Ссылка на вход -->
<div class="form-switch">
<span>Уже есть аккаунт?</span>
<button type="button" class="switch-btn" @click="switchToLogin">Войти</button>
</div>
</form>
</div>
</Transition>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
import { useRouter } from 'vue-router'
import { authApi, cardsApi } from '../api'
import { useProjectsStore } from '../stores/projects'
import { useMobile } from '../composables/useMobile'
import { setAuthCache } from '../router'
const { isMobile } = useMobile()
const router = useRouter()
const store = useProjectsStore()
// Режим: вход или регистрация
const isRegisterMode = ref(false)
// Вход
const login = ref('')
const password = ref('')
const loginFocused = ref(false)
const passwordFocused = ref(false)
// Регистрация
const regName = ref('')
const regUsername = ref('')
const regPassword = ref('')
const regTelegram = ref('')
const regNameFocused = ref(false)
const regUsernameFocused = ref(false)
const regPasswordFocused = ref(false)
const regTelegramFocused = ref(false)
// Общие
const error = ref('')
const errors = reactive({})
const isProcessing = ref(false)
const showSuccess = ref(false)
// Состояния тряски полей
const shakeUsername = ref(false)
const shakeName = ref(false)
const shakeTelegram = ref(false)
// Функция тряски поля
const triggerShake = (shakeRef) => {
shakeRef.value = true
setTimeout(() => { shakeRef.value = false }, 400)
}
// Сброс ошибки пароля при вводе
const clearPasswordError = () => {
if (errors.password) delete errors.password
}
// Фильтрация логина — начинается с буквы, далее a-zA-Z, 0-9, _
const filterUsername = () => {
// Сбрасываем ошибку при вводе
if (errors.username) delete errors.username
const before = regUsername.value
let value = before.replace(/[^a-zA-Z0-9_]/g, '')
// Первый символ должен быть буквой — убираем цифры/_ из начала
value = value.replace(/^[0-9_]+/, '')
regUsername.value = value
// Если что-то отфильтровалось — тряска
if (before !== value && before.length >= value.length) {
triggerShake(shakeUsername)
}
}
// Фильтрация имени — только буквы, пробелы, дефис
const filterName = () => {
// Сбрасываем ошибку при вводе
if (errors.name) delete errors.name
const before = regName.value
let value = before.replace(/[^a-zA-Zа-яА-ЯёЁ\s\-]/g, '')
value = value.replace(/\s+/g, ' ')
regName.value = value
// Если что-то отфильтровалось — тряска
if (before !== value && before.length > value.length) {
triggerShake(shakeName)
}
}
// Фильтрация telegram — только a-z, 0-9, _ и начинается с @
const filterTelegram = () => {
const before = regTelegram.value
let value = before
value = value.replace(/@/g, '')
value = value.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase()
const after = value ? '@' + value : ''
regTelegram.value = after
// Если что-то отфильтровалось (кроме добавления @) — тряска
const beforeClean = before.replace(/@/g, '').toLowerCase()
if (beforeClean !== value && beforeClean.length > value.length) {
triggerShake(shakeTelegram)
}
}
// Не даём удалить @ — очищаем поле полностью
const preventDeleteAt = (e) => {
if ((e.key === 'Backspace' || e.key === 'Delete') && regTelegram.value === '@') {
e.preventDefault()
regTelegram.value = ''
}
}
// Переключение режимов
const switchToRegister = () => {
error.value = ''
Object.keys(errors).forEach(k => delete errors[k])
isRegisterMode.value = true
nextTick(refreshIcons)
}
const switchToLogin = () => {
error.value = ''
Object.keys(errors).forEach(k => delete errors[k])
isRegisterMode.value = false
regTelegram.value = ''
nextTick(refreshIcons)
}
// Предзагрузка данных пока показывается анимация успеха
const prefetchData = async () => {
try {
await store.init()
if (store.currentProjectId) {
const result = await cardsApi.getAll(store.currentProjectId)
if (result.success) {
sessionStorage.setItem('prefetchedCards', JSON.stringify(result.data))
}
}
} catch (e) {
console.error('Prefetch error:', e)
}
}
// Вход
const handleLogin = async () => {
error.value = ''
isProcessing.value = true
try {
const data = await authApi.login(login.value, password.value)
if (data.success) {
// Получаем данные пользователя для кэша
const checkResult = await authApi.check()
setAuthCache(true, checkResult.user || null)
showSuccess.value = true
await nextTick()
refreshIcons()
const prefetchPromise = prefetchData()
const animationPromise = new Promise(resolve => setTimeout(resolve, 1800))
await Promise.all([prefetchPromise, animationPromise])
router.push('/')
} else {
isProcessing.value = false
error.value = data.errors?.username || data.errors?.password || 'Неверный логин или пароль'
await nextTick()
refreshIcons()
}
} catch (e) {
isProcessing.value = false
error.value = 'Ошибка подключения к серверу'
await nextTick()
refreshIcons()
}
}
// Регистрация
const handleRegister = async () => {
error.value = ''
Object.keys(errors).forEach(k => delete errors[k])
isProcessing.value = true
try {
// Убираем @ из telegram при отправке, если только @ — отправляем null
const telegramValue = regTelegram.value.replace('@', '').trim()
const data = await authApi.register({
name: regName.value.trim(),
username: regUsername.value.trim(),
password: regPassword.value,
telegram: telegramValue || null
})
if (data.success) {
// Автоматически входим после регистрации
const loginResult = await authApi.login(regUsername.value.trim(), regPassword.value)
if (loginResult.success) {
// Получаем данные пользователя для кэша
const checkResult = await authApi.check()
setAuthCache(true, checkResult.user || null)
showSuccess.value = true
await nextTick()
refreshIcons()
const prefetchPromise = prefetchData()
const animationPromise = new Promise(resolve => setTimeout(resolve, 1800))
await Promise.all([prefetchPromise, animationPromise])
router.push('/')
} else {
// Если автовход не удался — переключаем на форму входа
isProcessing.value = false
switchToLogin()
error.value = 'Аккаунт создан. Войдите с вашими данными.'
}
} else {
isProcessing.value = false
// Показываем ошибки с бэка
if (data.errors) {
Object.assign(errors, data.errors)
// Берём первую ошибку для отображения
const firstError = Object.values(data.errors)[0]
error.value = firstError || 'Ошибка регистрации'
} else {
error.value = 'Ошибка регистрации'
}
await nextTick()
refreshIcons()
}
} catch (e) {
isProcessing.value = false
error.value = 'Ошибка подключения к серверу'
await nextTick()
refreshIcons()
}
}
const refreshIcons = () => {
if (window.lucide) {
window.lucide.createIcons()
}
}
onMounted(refreshIcons)
watch([showSuccess, isRegisterMode], () => {
nextTick(refreshIcons)
})
</script>
<style scoped>
.login-page {
min-height: 100vh;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0a0a0c 0%, #111113 50%, #0d1117 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
/* Градиентные сферы на фоне */
.bg-glow {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.4;
pointer-events: none;
}
.glow-2 {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(96, 165, 250, 0.25) 0%, transparent 70%);
bottom: -150px;
left: -100px;
animation: glowFloat2 10s ease-in-out infinite;
}
.glow-3 {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(244, 114, 182, 0.2) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: glowFloat3 12s ease-in-out infinite;
}
@keyframes glowFloat2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(40px, -20px) scale(1.05); }
}
@keyframes glowFloat3 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.5; }
}
/* Левитирующие иконки */
.floating-icons {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.float-icon {
position: absolute;
color: rgba(255, 255, 255, 0.04);
transition: color 0.3s;
}
.float-icon i,
.float-icon svg {
width: 100%;
height: 100%;
}
.icon-1 { width: 48px; height: 48px; top: 8%; left: 12%; animation: float1 6s ease-in-out infinite; }
.icon-2 { width: 36px; height: 36px; top: 15%; right: 18%; animation: float2 7s ease-in-out infinite; }
.icon-3 { width: 42px; height: 42px; top: 25%; left: 8%; animation: float3 8s ease-in-out infinite; }
.icon-4 { width: 32px; height: 32px; top: 35%; right: 10%; animation: float1 5s ease-in-out infinite; }
.icon-5 { width: 40px; height: 40px; top: 55%; left: 15%; animation: float2 9s ease-in-out infinite; }
.icon-6 { width: 44px; height: 44px; top: 65%; right: 20%; animation: float3 6s ease-in-out infinite; }
.icon-7 { width: 28px; height: 28px; top: 75%; left: 10%; animation: float1 7s ease-in-out infinite; }
.icon-8 { width: 38px; height: 38px; top: 85%; right: 15%; animation: float2 8s ease-in-out infinite; }
.icon-9 { width: 34px; height: 34px; bottom: 20%; left: 25%; animation: float3 5s ease-in-out infinite; }
.icon-10 { width: 46px; height: 46px; top: 45%; left: 5%; animation: float1 10s ease-in-out infinite; }
.icon-11 { width: 30px; height: 30px; top: 20%; left: 25%; animation: float2 6s ease-in-out infinite; }
.icon-12 { width: 36px; height: 36px; bottom: 30%; right: 8%; animation: float3 7s ease-in-out infinite; }
@keyframes float1 {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-15px) rotate(5deg); }
75% { transform: translateY(10px) rotate(-3deg); }
}
@keyframes float2 {
0%, 100% { transform: translateY(0) rotate(0deg); }
33% { transform: translateY(-20px) rotate(-5deg); }
66% { transform: translateY(15px) rotate(3deg); }
}
@keyframes float3 {
0%, 100% { transform: translateY(0) translateX(0); }
50% { transform: translateY(-12px) translateX(8px); }
}
/* Контент */
.login-content {
position: relative;
z-index: 10;
width: 100%;
max-width: 420px;
display: flex;
align-items: center;
justify-content: center;
}
.login-form-wrapper {
display: flex;
flex-direction: column;
gap: 40px;
width: 100%;
}
/* Заголовок */
.login-header {
text-align: center;
}
.logo-icon {
width: 72px;
height: 72px;
margin: 0 auto 20px;
background: linear-gradient(135deg, var(--accent) 0%, #00e6b8 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 20px 60px rgba(0, 212, 170, 0.3);
animation: logoFloat 4s ease-in-out infinite;
}
.logo-icon i,
.logo-icon svg {
width: 36px;
height: 36px;
color: #000;
}
@keyframes logoFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.login-title {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.login-subtitle {
font-size: 15px;
color: var(--text-muted);
font-weight: 400;
}
/* Форма */
.login-form {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Группа инпута */
.input-group {
position: relative;
display: flex;
align-items: center;
gap: 16px;
padding: 0 4px;
}
.input-icon {
width: 24px;
height: 24px;
color: var(--text-muted);
transition: color 0.3s, transform 0.3s;
flex-shrink: 0;
}
.input-icon i,
.input-icon svg {
width: 24px;
height: 24px;
}
.input-group.has-focus .input-icon,
.input-group.has-value .input-icon {
color: var(--accent);
transform: scale(1.1);
}
.input-group.has-error .input-icon {
color: #f87171;
}
.input-group input {
flex: 1;
background: transparent;
border: none;
padding: 16px 0;
color: var(--text-primary);
font-family: inherit;
font-size: 16px;
font-weight: 400;
outline: none;
}
.input-group input::placeholder {
color: var(--text-muted);
transition: opacity 0.3s;
}
.input-group.has-focus input::placeholder {
opacity: 0.5;
}
.input-group input:disabled {
opacity: 0.5;
}
/* Линия под инпутом */
.input-line {
position: absolute;
bottom: 0;
left: 44px;
right: 0;
height: 2px;
background: rgba(255, 255, 255, 0.08);
border-radius: 1px;
overflow: hidden;
}
.input-line::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, var(--accent), #00e6b8);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.input-group.has-focus .input-line::after,
.input-group.has-value .input-line::after {
transform: scaleX(1);
}
.input-group.has-error .input-line::after {
background: #f87171;
transform: scaleX(1);
}
/* Анимация тряски при неверном вводе */
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-4px); }
40% { transform: translateX(4px); }
60% { transform: translateX(-3px); }
80% { transform: translateX(3px); }
}
.input-group.shake {
animation: shake 0.4s ease-out;
}
.input-group.shake .input-line::after {
background: #f87171 !important;
transform: scaleX(1) !important;
}
/* Ошибка */
.error-message {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
background: rgba(248, 113, 113, 0.08);
border: 1px solid rgba(248, 113, 113, 0.2);
border-radius: 12px;
color: #f87171;
font-size: 14px;
}
.error-message i,
.error-message svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
/* Кнопка входа */
.login-btn {
position: relative;
width: 100%;
padding: 18px 24px;
background: linear-gradient(135deg, var(--accent) 0%, #00e6b8 100%);
border: none;
border-radius: 14px;
color: #000;
font-family: inherit;
font-size: 16px;
font-weight: 600;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 8px;
box-shadow: 0 10px 40px rgba(0, 212, 170, 0.3);
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 15px 50px rgba(0, 212, 170, 0.4);
}
.login-btn:active:not(:disabled) {
transform: translateY(0);
}
.login-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
.btn-text {
transition: opacity 0.3s, transform 0.3s;
}
.btn-arrow {
width: 20px;
height: 20px;
transition: opacity 0.3s, transform 0.3s;
}
.btn-loader {
position: absolute;
display: flex;
gap: 6px;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s, transform 0.3s;
}
.btn-loader span {
width: 8px;
height: 8px;
background: #000;
border-radius: 50%;
animation: btnLoaderBounce 1.2s ease-in-out infinite;
}
.btn-loader span:nth-child(2) { animation-delay: 0.1s; }
.btn-loader span:nth-child(3) { animation-delay: 0.2s; }
@keyframes btnLoaderBounce {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1.2); opacity: 1; }
}
.login-btn.is-loading .btn-text,
.login-btn.is-loading .btn-arrow {
opacity: 0;
transform: scale(0.8);
}
.login-btn.is-loading .btn-loader {
opacity: 1;
transform: scale(1);
}
/* Переключение между входом и регистрацией */
.form-switch {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
color: var(--text-muted);
}
.switch-btn {
background: none;
border: none;
color: var(--accent);
font-family: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.switch-btn:hover {
opacity: 0.8;
}
/* Состояние успеха */
.success-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
animation: successAppear 0.6s ease;
}
@keyframes successAppear {
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
.success-icon {
width: 100px;
height: 100px;
background: linear-gradient(135deg, var(--accent) 0%, #00e6b8 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28px;
box-shadow: 0 25px 80px rgba(0, 212, 170, 0.4);
animation: successPulse 1.5s ease-in-out infinite;
}
.success-icon i,
.success-icon svg {
width: 48px;
height: 48px;
color: #000;
animation: successCheck 0.5s ease 0.2s both;
}
@keyframes successPulse {
0%, 100% { transform: scale(1); box-shadow: 0 25px 80px rgba(0, 212, 170, 0.4); }
50% { transform: scale(1.05); box-shadow: 0 30px 100px rgba(0, 212, 170, 0.5); }
}
@keyframes successCheck {
0% { transform: scale(0) rotate(-45deg); opacity: 0; }
50% { transform: scale(1.2) rotate(10deg); }
100% { transform: scale(1) rotate(0); opacity: 1; }
}
.success-title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.success-text {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 32px;
}
.success-loader {
display: flex;
gap: 8px;
}
.success-loader span {
width: 10px;
height: 10px;
background: var(--accent);
border-radius: 50%;
animation: successLoaderBounce 1.4s ease-in-out infinite;
}
.success-loader span:nth-child(2) { animation-delay: 0.16s; }
.success-loader span:nth-child(3) { animation-delay: 0.32s; }
@keyframes successLoaderBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* Transitions */
.form-fade-enter-active,
.form-fade-leave-active {
transition: opacity 0.4s ease, transform 0.4s ease;
}
.form-fade-enter-from {
opacity: 0;
transform: scale(0.95);
}
.form-fade-leave-to {
opacity: 0;
transform: scale(0.95);
position: absolute;
width: 100%;
}
.success-fade-enter-active {
transition: opacity 0.5s ease 0.2s, transform 0.5s ease 0.2s;
}
.success-fade-enter-from {
opacity: 0;
transform: scale(0.9);
}
.error-shake-enter-active {
animation: errorShake 0.5s ease;
}
.error-shake-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.error-shake-leave-to {
opacity: 0;
transform: translateY(-10px);
}
@keyframes errorShake {
0% { opacity: 0; transform: translateX(0) translateY(-10px); }
20% { opacity: 1; transform: translateX(-8px) translateY(0); }
40% { transform: translateX(8px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
100% { transform: translateX(0); }
}
/* Мобильная адаптация (MOBILE_BREAKPOINT из config.js) */
.login-page.is-mobile .login-title {
font-size: 26px;
}
.login-page.is-mobile .login-subtitle {
font-size: 14px;
}
.login-page.is-mobile .logo-icon {
width: 64px;
height: 64px;
}
.login-page.is-mobile .logo-icon i,
.login-page.is-mobile .logo-icon svg {
width: 32px;
height: 32px;
}
.login-page.is-mobile .float-icon {
opacity: 0.5;
}
</style>