переброс на gittea

This commit is contained in:
Archibald 2025-05-13 14:13:28 +05:00
parent 0aee1e625b
commit 51b16ab3d6
4 changed files with 240 additions and 41 deletions

View File

@ -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=["*"],

View File

@ -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>

View File

@ -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>

View File

@ -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()
} }