feat: rediseñar página de login con glassmorphism y diseño responsive
- Rediseño completo de Login.vue con burbujas animadas de colores - Implementado glassmorphism (backdrop-filter blur) en tarjeta de login - Migrado a Composition API con <script setup> - Formulario mejorado: iconos mdi, variant outlined, loading state - Diseño responsive con breakpoints mobile/tablet/desktop - Burbuja amarilla con opacidad reducida (0.08-0.25) para mejor UX - Fix: agregado import en autenticarse.vue
This commit is contained in:
@@ -1,70 +1,272 @@
|
||||
<template>
|
||||
<h1>Login</h1>
|
||||
<v-container fluid class="pa-0">
|
||||
<v-sheet class="hero-section d-flex align-center justify-center">
|
||||
<div class="glow-bubble bubble-blue"></div>
|
||||
<div class="glow-bubble bubble-green"></div>
|
||||
<div class="glow-bubble bubble-yellow"></div>
|
||||
<div class="glow-bubble bubble-red"></div>
|
||||
|
||||
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
label="Usuario"
|
||||
:rules="[requiredRule]"
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Contraseña"
|
||||
type="password"
|
||||
:rules="[requiredRule]"
|
||||
required
|
||||
/>
|
||||
<div class="login-card">
|
||||
<v-img
|
||||
:src="logo"
|
||||
alt="Don Confiao"
|
||||
max-width="140"
|
||||
class="mx-auto mb-4"
|
||||
/>
|
||||
<h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1">
|
||||
Iniciar Sesión
|
||||
</h1>
|
||||
<p class="text-body-2 text-medium-emphasis text-center mb-6">
|
||||
Ingresa tus credenciales para acceder
|
||||
</p>
|
||||
|
||||
<v-btn type="submit" color="primary">Entrar</v-btn>
|
||||
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
label="Usuario"
|
||||
prepend-inner-icon="mdi-account"
|
||||
:rules="[requiredRule]"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-2"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Contraseña"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
type="password"
|
||||
:rules="[requiredRule]"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-4"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<v-alert v-if="error" type="error" class="mt-2">{{ error }}</v-alert>
|
||||
</v-form>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Entrar
|
||||
</v-btn>
|
||||
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
closable
|
||||
@click:close="error = ''"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthService from '@/services/auth';
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AuthService from '@/services/auth'
|
||||
import logo from '@/assets/logo_colorful.png'
|
||||
|
||||
export default {
|
||||
name: 'DonConfiao',
|
||||
const router = useRouter()
|
||||
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const loginForm = ref(null)
|
||||
|
||||
methods: {
|
||||
requiredRule(value) {
|
||||
return !!value || 'Este campo es obligatorio';
|
||||
},
|
||||
function requiredRule(value) {
|
||||
return !!value || 'Este campo es obligatorio'
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
this.error = '';
|
||||
async function onSubmit() {
|
||||
error.value = ''
|
||||
isSubmitting.value = true
|
||||
|
||||
const form = this.$refs.loginForm;
|
||||
const isValid = await form.validate();
|
||||
try {
|
||||
const form = loginForm.value
|
||||
if (form) {
|
||||
const { valid } = await form.validate()
|
||||
if (!valid) return
|
||||
}
|
||||
|
||||
if (!isValid) return;
|
||||
if (!username.value || !password.value) {
|
||||
error.value = 'Usuario y contraseña son obligatorios'
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.username || !this.password) {
|
||||
this.error = 'Usuario y contraseña son obligatorios';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AuthService.login({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
this.$router.push({ path: '/' });
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message ?? e.message;
|
||||
this.error = msg ?? 'Error al iniciar sesión';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
await AuthService.login({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
})
|
||||
router.push({ path: '/' })
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message ?? e.message
|
||||
error.value = msg ?? 'Error al iniciar sesión'
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
min-height: calc(100vh - 80px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.glow-bubble {
|
||||
position: absolute;
|
||||
border-radius: 10%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
.bubble-blue {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(66, 165, 245, 0.8) 10%,
|
||||
rgba(66, 165, 245, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
top: -180px;
|
||||
left: -150px;
|
||||
animation: floatCornerTL 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bubble-green {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 200, 83, 0.7) 10%,
|
||||
rgba(0, 200, 83, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
bottom: -150px;
|
||||
right: -120px;
|
||||
animation: floatCornerBR 9s ease-in-out infinite;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.bubble-yellow {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 213, 0, 0.3) 20%,
|
||||
rgba(255, 193, 7, 0) 1000%
|
||||
);
|
||||
filter: blur(80px);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
animation: glowPulseCenter 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bubble-red {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(239, 83, 80, 0.7) 10%,
|
||||
rgba(239, 83, 80, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
top: -150px;
|
||||
right: -120px;
|
||||
animation: floatCornerTR 8s ease-in-out infinite;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 2.5rem 2rem;
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hero-section {
|
||||
padding: 1rem;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 1.5rem 1.25rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerTL {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
transform: scale(0.9) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.25) translate(40px, 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerTR {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
transform: scale(1.2) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95) translate(-30px, 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerBR {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
transform: scale(0.85) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.75;
|
||||
transform: scale(1.15) translate(-40px, -30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowPulseCenter {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.08;
|
||||
transform: translate(-50%, -50%) scale(0.85);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
transform: translate(-50%, -50%) scale(1.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
import Login from '@/components/Login.vue'
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user