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

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 {useGlobalLoading} from "../Composables/useGlobalLoading.js";
import AppDialogManager from "../Components/AppDialogManager.vue";
import Noise from "../Components/Noise.vue";
import { useThemeVars } from "naive-ui";
import { computed } from "vue";
@@ -22,6 +23,7 @@ const blobBg = computed(() => {
</script>
<template>
<Noise />
<AppDialogManager />
<NLayout position="absolute">

View File

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