переброс на gittea
This commit is contained in:
parent
0aee1e625b
commit
51b16ab3d6
@ -15,7 +15,7 @@ def start_app():
|
|||||||
|
|
||||||
api_app.add_middleware(
|
api_app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["http://localhost:5173"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@ -1,11 +1,44 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Реактивное вычисление статуса авторизации
|
||||||
|
const isAuthenticated = computed(() => !!localStorage.getItem('access_token'))
|
||||||
|
|
||||||
|
const goToLogin = () => router.push('/login')
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
router.go(0) // Принудительное обновление страницы для обновления состояния
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
12345
|
<div class="home-page q-pa-md q-gutter-md">
|
||||||
|
<h4 class="text-center">Добро пожаловать!</h4>
|
||||||
|
|
||||||
|
<div v-if="isAuthenticated" class="q-gutter-sm">
|
||||||
|
<q-btn label="Редактировать профиль" color="secondary" />
|
||||||
|
<q-btn label="Выход" color="negative" @click="logout" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
label="Авторизация"
|
||||||
|
color="primary"
|
||||||
|
@click="goToLogin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,19 +1,41 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Notify } from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
import loginUser from "../api/auth/loginRequest.js";
|
import loginUser from "../api/auth/loginRequest.js"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const login = ref('')
|
const login = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const marqueeTrack = ref(null)
|
||||||
|
const marqueeText = ref(null)
|
||||||
|
const animationDuration = ref(7) // секунд
|
||||||
|
|
||||||
|
// Динамически рассчитываем длительность анимации в зависимости от ширины текста
|
||||||
|
const updateAnimation = () => {
|
||||||
|
if (marqueeTrack.value && marqueeText.value) {
|
||||||
|
const textWidth = marqueeText.value.offsetWidth
|
||||||
|
const containerWidth = marqueeTrack.value.offsetWidth / 2
|
||||||
|
// Скорость: 100px/sec (можно подстроить)
|
||||||
|
const speed = 100
|
||||||
|
animationDuration.value = (textWidth + containerWidth) / speed
|
||||||
|
marqueeTrack.value.style.setProperty('--marquee-width', `${textWidth}px`)
|
||||||
|
marqueeTrack.value.style.setProperty('--marquee-duration', `${animationDuration.value}s`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(updateAnimation, 100) // Дать DOM отрисоваться
|
||||||
|
window.addEventListener('resize', updateAnimation)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateAnimation)
|
||||||
|
})
|
||||||
|
|
||||||
const authorisation = async () => {
|
const authorisation = async () => {
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = await loginUser({
|
const accessToken = await loginUser({
|
||||||
login: login.value,
|
login: login.value,
|
||||||
@ -24,14 +46,16 @@ const authorisation = async () => {
|
|||||||
|
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Успешный вход!'
|
message: 'Успешный вход!',
|
||||||
|
icon: 'check_circle'
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push('/dashboard')
|
router.push('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: error.message || 'Ошибка входа'
|
message: error.message || 'Ошибка входа',
|
||||||
|
icon: 'error'
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -39,12 +63,30 @@ const authorisation = async () => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fullscreen flex flex-center bg-grey-2">
|
<div class="fullscreen flex flex-center bg-violet-strong">
|
||||||
<q-card class="q-pa-lg shadow-2" style="width: 350px">
|
<q-card class="q-pa-xl shadow-violet card-animate violet-card" style="width: 370px; max-width: 92vw;">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6 text-center">Вход</div>
|
<div class="marquee-outer q-mb-md">
|
||||||
|
<div
|
||||||
|
class="marquee-track"
|
||||||
|
ref="marqueeTrack"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="marquee-text text-h5 text-weight-bold text-center text-violet-strong"
|
||||||
|
ref="marqueeText"
|
||||||
|
>
|
||||||
|
<q-icon name="key" size="32px" class="q-mr-sm text-violet-accent"/>
|
||||||
|
Добро пожаловать
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="marquee-text text-h5 text-weight-bold text-center text-violet-strong"
|
||||||
|
>
|
||||||
|
<q-icon name="key" size="32px" class="q-mr-sm text-violet-accent"/>
|
||||||
|
Добро пожаловать
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@ -56,27 +98,46 @@ const authorisation = async () => {
|
|||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
autofocus
|
autofocus
|
||||||
class="q-mb-md"
|
class="q-mb-md lavender-input"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[ val => (val && val.length > 0) || 'Введите логин' ]"
|
:rules="[ val => (val && val.length > 0) || 'Введите логин' ]"
|
||||||
/>
|
:disable="loading"
|
||||||
|
rounded
|
||||||
|
color="deep-purple-5"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="person" :color="'deep-purple-5'"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
label="Пароль"
|
label="Пароль"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
class="q-mb-lg"
|
class="q-mb-lg lavender-input"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[ val => (val && val.length > 0) || 'Введите пароль' ]"
|
:rules="[ val => (val && val.length > 0) || 'Введите пароль' ]"
|
||||||
/>
|
:disable="loading"
|
||||||
|
rounded
|
||||||
|
color="deep-purple-5"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="lock" :color="'deep-purple-5'"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Войти"
|
label="Войти"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="deep-purple-5"
|
||||||
class="full-width"
|
class="full-width q-mt-sm btn-rounded shimmer-btn"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
unelevated
|
||||||
|
size="lg"
|
||||||
|
icon="arrow_forward"
|
||||||
|
no-caps
|
||||||
|
rounded
|
||||||
/>
|
/>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@ -85,7 +146,122 @@ const authorisation = async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fullscreen {
|
.bg-violet-strong {
|
||||||
height: 100vh;
|
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.violet-card {
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #ede9fe;
|
||||||
|
box-shadow: 0 6px 32px 0 rgba(124, 58, 237, 0.13), 0 1.5px 6px 0 rgba(124, 58, 237, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-animate {
|
||||||
|
animation: fadeInUp 0.7s cubic-bezier(.39, .575, .565, 1) both;
|
||||||
|
}
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
0% { opacity: 0; transform: translateY(40px);}
|
||||||
|
100% { opacity: 1; transform: none;}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Бегущая строка на всю ширину карточки, без видимых полей */
|
||||||
|
.marquee-outer {
|
||||||
|
width: 100%;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-track {
|
||||||
|
--marquee-width: 370px;
|
||||||
|
--marquee-duration: 7s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
will-change: transform;
|
||||||
|
animation: marquee-move var(--marquee-duration) linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.marquee-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: 'Montserrat', 'Roboto', sans-serif;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-move {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(calc(-1 * var(--marquee-width, 370px))); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-input,
|
||||||
|
.q-field__control,
|
||||||
|
.q-field__native,
|
||||||
|
.q-field__inner {
|
||||||
|
border-radius: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-rounded {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shimmer-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.shimmer-btn::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
rgba(167, 139, 250, 0.2) 0%,
|
||||||
|
rgba(167, 139, 250, 0.6) 50%,
|
||||||
|
rgba(167, 139, 250, 0.2) 100%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.shimmer-btn:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
animation: shimmer 1.2s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-60%) skewX(-20deg);}
|
||||||
|
100% { transform: translateX(60%) skewX(-20deg);}
|
||||||
|
}
|
||||||
|
.shimmer-btn:hover {
|
||||||
|
box-shadow: 0 0 18px 0 #a78bfa, 0 1.5px 6px 0 rgba(124, 58, 237, 0.10);
|
||||||
|
}
|
||||||
|
.shimmer-btn:active {
|
||||||
|
background: #ede9fe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lavender-input .q-field__control,
|
||||||
|
.lavender-input .q-field__native,
|
||||||
|
.lavender-input .q-field__inner {
|
||||||
|
background: #f6f0ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-violet-strong {
|
||||||
|
color: #4f046f;
|
||||||
|
}
|
||||||
|
.text-violet-accent {
|
||||||
|
color: #a78bfa;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,20 +1,13 @@
|
|||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
// import DashboardPage from '@/pages/DashboardPage.vue'
|
import LoginPage from "../pages/LoginPage.vue"
|
||||||
import LoginPage from "../pages/LoginPage.vue";
|
import HomePage from "../pages/HomePage.vue"
|
||||||
import HomePage from "../pages/HomePage.vue"; // пример
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/', component: HomePage,
|
path: '/',
|
||||||
meta: {requiresAuth: true}
|
component: HomePage
|
||||||
},
|
},
|
||||||
|
{path: '/login', component: LoginPage}
|
||||||
{path: '/login', component: LoginPage},
|
|
||||||
// {
|
|
||||||
// path: '/dashboard',
|
|
||||||
// component: DashboardPage,
|
|
||||||
// meta: { requiresAuth: true } // требовать авторизацию
|
|
||||||
// }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -22,14 +15,11 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Навешиваем перехватчик навигации
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const isAuthenticated = !!localStorage.getItem('access_token')
|
const isAuthenticated = !!localStorage.getItem('access_token')
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
if (to.path === '/login' && isAuthenticated) {
|
||||||
next('/login')
|
next('/') // теперь редирект на главную страницу
|
||||||
} else if (to.path === '/login' && isAuthenticated) {
|
|
||||||
next('/dashboard')
|
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user