Визуальные правки приложения

This commit is contained in:
brusnitsyn
2026-06-11 14:37:47 +09:00
parent a916a7532a
commit 37420693d1
5 changed files with 174 additions and 16 deletions

BIN
public/assets/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,22 @@
<script setup>
const props = defineProps({
opacity: {
type: Number,
default: 0.025,
},
})
</script>
<template>
<div
class="pointer-events-none fixed inset-0 z-9999 size-full"
:style="{
backgroundImage: 'url(/assets/noise.png)',
opacity: props.opacity,
}"
/>
</template>
<style scoped>
</style>

View File

@@ -3,6 +3,7 @@ import {NLayout, NLayoutHeader, NEl, NAlert, NSpin, NText} from "naive-ui";
import AppHeader from "./Components/AppHeader.vue"; import AppHeader from "./Components/AppHeader.vue";
import {useGlobalLoading} from "../Composables/useGlobalLoading.js"; import {useGlobalLoading} from "../Composables/useGlobalLoading.js";
import AppDialogManager from "../Components/AppDialogManager.vue"; import AppDialogManager from "../Components/AppDialogManager.vue";
import Noise from "../Components/Noise.vue";
import { useThemeVars } from "naive-ui"; import { useThemeVars } from "naive-ui";
import { computed } from "vue"; import { computed } from "vue";
@@ -22,6 +23,7 @@ const blobBg = computed(() => {
</script> </script>
<template> <template>
<Noise />
<AppDialogManager /> <AppDialogManager />
<NLayout position="absolute"> <NLayout position="absolute">

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, reactive } from 'vue' import { ref, reactive, computed } from 'vue'
import {Head, router, useForm, usePage} from '@inertiajs/vue3' import {Head, router, useForm, usePage} from '@inertiajs/vue3'
import { useAuthStore } from '../../Stores/auth.js' import { useAuthStore } from '../../Stores/auth.js'
import { TbUser, TbLock } from 'vue-icons-plus/tb' import { TbUser, TbLock } from 'vue-icons-plus/tb'
@@ -7,8 +7,42 @@ import {TbHeartRateMonitor} from 'vue-icons-plus/tb'
import { import {
NForm, NFormItem, NInput, NButton, NCheckbox, NForm, NFormItem, NInput, NButton, NCheckbox,
NSpace, NCard, NIcon, NAlert, NModal, darkTheme, NSpace, NCard, NIcon, NAlert, NModal, darkTheme,
NConfigProvider, NLayout, NP, NEl NConfigProvider, NLayout, NP, useThemeVars
} from 'naive-ui' } 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 authStore = useAuthStore()
const page = usePage() const page = usePage()
@@ -81,20 +115,34 @@ const handleForgotPassword = async () => {
<template> <template>
<NConfigProvider :theme="darkTheme"> <NConfigProvider :theme="darkTheme">
<NLayout embedded> <NLayout embedded>
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <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="Вход в систему" /> <Head title="Вход в систему" />
<div class="max-w-md w-full space-y-8"> <!-- Цветные градиентные пятна фона -->
<!-- <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="text-center">
<div class="flex justify-center"> <div class="flex justify-center">
<NEl class="w-16 h-16 rounded-full flex items-center justify-center" style="background-color: var(--primary-color);"> <div class="auth-logo flex items-center justify-center">
<NIcon size="32" color="var(--base-color)"> <NIcon size="30" color="#fff">
<TbHeartRateMonitor /> <TbHeartRateMonitor />
</NIcon> </NIcon>
</NEl> </div>
</div> </div>
<h2 class="mt-6 text-3xl font-bold"> <h2 class="mt-6 text-3xl font-bold tracking-tight">
Метрика Метрика
</h2> </h2>
<NP class="mt-2! text-sm" depth="3"> <NP class="mt-2! text-sm" depth="3">
@@ -103,7 +151,7 @@ const handleForgotPassword = async () => {
</div> </div>
<!-- Форма входа --> <!-- Форма входа -->
<n-card> <n-card class="auth-card" content-style="padding: 28px;">
<n-form <n-form
ref="formRef" ref="formRef"
:model="form" :model="form"
@@ -111,17 +159,18 @@ const handleForgotPassword = async () => {
@submit.prevent="handleLogin" @submit.prevent="handleLogin"
> >
<n-space vertical size="large"> <n-space vertical size="large">
<!-- Email --> <!-- Логин -->
<n-form-item label="Логин" path="login"> <n-form-item label="Логин" path="login">
<n-input <n-input
id="login" id="login"
v-model:value="form.login" v-model:value="form.login"
placeholder="Ваш логин" placeholder="Ваш логин"
size="large" size="large"
round
@keydown.enter="handleLogin" @keydown.enter="handleLogin"
> >
<template #prefix> <template #prefix>
<n-icon><TbUser /></n-icon> <n-icon :component="TbUser" depth="3" />
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
@@ -134,11 +183,12 @@ const handleForgotPassword = async () => {
type="password" type="password"
placeholder="Ваш пароль" placeholder="Ваш пароль"
size="large" size="large"
round
show-password-on="click" show-password-on="click"
@keydown.enter="handleLogin" @keydown.enter="handleLogin"
> >
<template #prefix> <template #prefix>
<n-icon><TbLock /></n-icon> <n-icon :component="TbLock" depth="3" />
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
@@ -151,17 +201,21 @@ const handleForgotPassword = async () => {
<!-- </n-form-item>--> <!-- </n-form-item>-->
<!-- Ошибки --> <!-- Ошибки -->
<n-alert v-if="error" title="Ошибка входа" type="error"> <Transition name="fade-slide">
{{ error }} <n-alert v-if="error" title="Ошибка входа" type="error" closable @close="error = ''">
</n-alert> {{ error }}
</n-alert>
</Transition>
<!-- Кнопка входа --> <!-- Кнопка входа -->
<n-button <n-button
type="primary" type="primary"
size="large" size="large"
round
:loading="loading" :loading="loading"
@click="handleLogin" @click="handleLogin"
block block
class="auth-submit"
> >
Войти в систему Войти в систему
</n-button> </n-button>
@@ -195,6 +249,7 @@ const handleForgotPassword = async () => {
<!-- Модальное окно восстановления пароля --> <!-- Модальное окно восстановления пароля -->
<n-modal v-model:show="showForgotPassword"> <n-modal v-model:show="showForgotPassword">
<n-card <n-card
class="auth-card"
style="width: 400px" style="width: 400px"
title="Восстановление пароля" title="Восстановление пароля"
:bordered="false" :bordered="false"
@@ -206,10 +261,11 @@ const handleForgotPassword = async () => {
<n-input <n-input
v-model:value="forgotForm.email" v-model:value="forgotForm.email"
placeholder="Введите ваш email" placeholder="Введите ваш email"
round
/> />
</n-form-item> </n-form-item>
<n-button type="primary" block :loading="forgotLoading"> <n-button type="primary" round block :loading="forgotLoading" @click="handleForgotPassword">
Отправить инструкции Отправить инструкции
</n-button> </n-button>
</n-space> </n-space>
@@ -222,5 +278,82 @@ const handleForgotPassword = async () => {
</template> </template>
<style scoped> <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> </style>