Files
onboard/resources/js/Pages/Auth/Login.vue

360 lines
14 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.

<script setup>
import { ref, reactive, computed } from 'vue'
import {Head, router, useForm, usePage} from '@inertiajs/vue3'
import { useAuthStore } from '../../Stores/auth.js'
import { TbUser, TbLock } from 'vue-icons-plus/tb'
import {TbHeartRateMonitor} from 'vue-icons-plus/tb'
import {
NForm, NFormItem, NInput, NButton, NCheckbox,
NSpace, NCard, NIcon, NAlert, NModal, darkTheme,
NConfigProvider, NLayout, NP, useThemeVars
} from 'naive-ui'
import Noise from '../../Components/Noise.vue'
const topographySvg = '/assets/topography.svg'
const themeVars = useThemeVars()
const blobBg = computed(() => {
const p = themeVars.value.primaryColor
const w = themeVars.value.warningColor
const i = themeVars.value.infoColor
return [
`radial-gradient(circle 18vw at 8% 8%, color-mix(in srgb, ${p} 26%, transparent), transparent 100%)`,
`radial-gradient(circle 22vw at 100% 18%, color-mix(in srgb, ${i} 22%, transparent), transparent 100%)`,
`radial-gradient(circle 24vw at 12% 100%, color-mix(in srgb, ${w} 20%, transparent), transparent 100%)`,
].join(', ')
})
const primaryColor = computed(() => themeVars.value.primaryColor)
const infoColor = computed(() => themeVars.value.infoColor)
const cardColor = computed(() => themeVars.value.cardColor)
const borderColor = computed(() => themeVars.value.borderColor)
// Интерактивная текстура фона (следует за курсором/пальцем)
const texturePos = reactive({ x: 0, y: 0 })
const onTexturePointerMove = (e) => {
const point = e.touches ? e.touches[0] : e
texturePos.x = point.clientX
texturePos.y = point.clientY
}
const textureMaskImage = computed(() => {
return `url(${topographySvg}), radial-gradient(circle 280px at ${texturePos.x}px ${texturePos.y}px, #fff 0%, transparent 100%)`
})
const authStore = useAuthStore()
const page = usePage()
const appVersion = page.props.app.version
const appTag = page.props.app.tag
// Состояние формы
const formRef = ref(null)
const form = useForm({
login: '',
password: '',
})
const forgotForm = ref({ email: '' })
// Состояние UI
const loading = ref(false)
const forgotLoading = ref(false)
const error = ref('')
const showForgotPassword = ref(false)
// Правила валидации
const rules = {
login: [
{ required: true, message: 'Введите логин', trigger: 'blur' },
],
password: [
{ required: true, message: 'Введите пароль', trigger: 'blur' },
{ min: 3, message: 'Пароль должен содержать минимум 3 символа', trigger: 'blur' }
]
}
// Обработка входа
const handleLogin = async () => {
error.value = ''
try {
await formRef.value?.validate()
loading.value = true
form.post(
'/auth/login',
{
onSuccess: () => {},
onError: (err) => {
error.value = err[0]
}
}
)
} catch {
// валидация не прошла
} finally {
loading.value = false
}
}
// Восстановление пароля
const handleForgotPassword = async () => {
forgotLoading.value = true
// Здесь будет запрос на восстановление пароля
await new Promise(resolve => setTimeout(resolve, 1000))
forgotLoading.value = false
showForgotPassword.value = false
}
</script>
<template>
<NConfigProvider :theme="darkTheme">
<NLayout embedded>
<div
class="relative min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"
@mousemove="onTexturePointerMove"
@touchmove="onTexturePointerMove"
>
<Head title="Вход в систему" />
<!-- Цветные градиентные пятна фона -->
<!-- <div class="pointer-events-none fixed inset-0 z-0" :style="`background: ${blobBg};`" /> -->
<Noise />
<!-- Базовый слой топографической текстуры -->
<div class="texture-layer texture-base" />
<!-- Подсветка текстуры вокруг курсора -->
<div class="texture-layer texture-highlight" :style="{ maskImage: textureMaskImage, WebkitMaskImage: textureMaskImage }" />
<div class="auth-enter relative z-10 max-w-md w-full space-y-8">
<!-- Логотип и заголовок -->
<div class="text-center">
<div class="flex justify-center">
<div class="auth-logo flex items-center justify-center">
<NIcon size="30" color="#fff">
<TbHeartRateMonitor />
</NIcon>
</div>
</div>
<h2 class="mt-6 text-3xl font-bold tracking-tight">
Метрика
</h2>
<NP class="mt-2! text-sm" depth="3">
Введите данные для входа в систему
</NP>
</div>
<!-- Форма входа -->
<n-card class="auth-card" content-style="padding: 28px;">
<n-form
ref="formRef"
:model="form"
:rules="rules"
@submit.prevent="handleLogin"
>
<n-space vertical size="large">
<!-- Логин -->
<n-form-item label="Логин" path="login">
<n-input
id="login"
v-model:value="form.login"
placeholder="Ваш логин"
size="large"
round
@keydown.enter="handleLogin"
>
<template #prefix>
<n-icon :component="TbUser" depth="3" />
</template>
</n-input>
</n-form-item>
<!-- Пароль -->
<n-form-item label="Пароль" path="password">
<n-input
id="password"
v-model:value="form.password"
type="password"
placeholder="Ваш пароль"
size="large"
round
show-password-on="click"
@keydown.enter="handleLogin"
>
<template #prefix>
<n-icon :component="TbLock" depth="3" />
</template>
</n-input>
</n-form-item>
<!-- Запомнить меня -->
<!-- <n-form-item>-->
<!-- <n-checkbox v-model:checked="form.remember">-->
<!-- Запомнить меня-->
<!-- </n-checkbox>-->
<!-- </n-form-item>-->
<!-- Ошибки -->
<Transition name="fade-slide">
<n-alert v-if="error" title="Ошибка входа" type="error" closable @close="error = ''">
{{ error }}
</n-alert>
</Transition>
<!-- Кнопка входа -->
<n-button
type="primary"
size="large"
round
:loading="loading"
@click="handleLogin"
block
class="auth-submit"
>
Войти в систему
</n-button>
<!-- Дополнительные ссылки -->
<div class="text-center space-y-3">
<div class="text-sm">
<n-button text type="primary" @click="showForgotPassword = true">
Забыли пароль?
</n-button>
</div>
<!-- <div class="text-sm text-gray-600">-->
<!-- Нет аккаунта?-->
<!-- <n-button text type="primary" @click="$inertia.visit(route('register'))">-->
<!-- Зарегистрироваться-->
<!-- </n-button>-->
<!-- </div>-->
</div>
</n-space>
</n-form>
</n-card>
<!-- Информация о системе -->
<NP class="mt-2! text-xs! text-center" depth="3">
<p>Метрика v{{ appVersion }}-{{ appTag }}</p>
<p>Только для авторизованного персонала</p>
</NP>
</div>
<!-- Модальное окно восстановления пароля -->
<n-modal v-model:show="showForgotPassword">
<n-card
class="auth-card"
style="width: 400px"
title="Восстановление пароля"
:bordered="false"
size="small"
>
<n-form :model="forgotForm" @submit.prevent="handleForgotPassword">
<n-space vertical>
<n-form-item label="Email" required>
<n-input
v-model:value="forgotForm.email"
placeholder="Введите ваш email"
round
/>
</n-form-item>
<n-button type="primary" round block :loading="forgotLoading" @click="handleForgotPassword">
Отправить инструкции
</n-button>
</n-space>
</n-form>
</n-card>
</n-modal>
</div>
</NLayout>
</NConfigProvider>
</template>
<style scoped>
.texture-layer {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-color: currentColor;
mask-repeat: repeat;
-webkit-mask-repeat: repeat;
mask-size: 666px 666px;
-webkit-mask-size: 666px 666px;
}
.texture-base {
opacity: 0.04;
mask-image: url('/assets/topography.svg');
-webkit-mask-image: url('/assets/topography.svg');
}
.texture-highlight {
opacity: 0.18;
mask-repeat: repeat, no-repeat;
-webkit-mask-repeat: repeat, no-repeat;
mask-size: 666px 666px, 100% 100%;
-webkit-mask-size: 666px 666px, 100% 100%;
mask-composite: intersect;
-webkit-mask-composite: source-in;
}
/* Логотип в градиентном круге со свечением */
.auth-logo {
width: 64px;
height: 64px;
border-radius: 9999px;
background: linear-gradient(135deg, v-bind(primaryColor), v-bind(infoColor));
box-shadow: 0 8px 28px -10px color-mix(in srgb, v-bind(primaryColor) 65%, transparent);
}
/* Стеклянная карточка с лёгким размытием фона */
.auth-card {
background: color-mix(in srgb, v-bind(cardColor) 70%, transparent);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border: 1px solid color-mix(in srgb, v-bind(borderColor) 60%, transparent);
border-radius: 18px;
box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.6);
}
.auth-submit {
box-shadow: 0 10px 24px -10px color-mix(in srgb, v-bind(primaryColor) 60%, transparent);
}
/* Плавное появление карточки входа */
.auth-enter {
animation: auth-enter 0.5s ease-out;
}
@keyframes auth-enter {
from {
opacity: 0;
transform: translateY(16px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Плавное появление блока ошибки */
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.2s ease;
}
.fade-slide-enter-from,
.fade-slide-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>