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>
|
<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>
|
||||||
|
|
||||||
|
<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-form ref="loginForm" @submit.prevent="onSubmit">
|
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="username"
|
v-model="username"
|
||||||
label="Usuario"
|
label="Usuario"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
:rules="[requiredRule]"
|
:rules="[requiredRule]"
|
||||||
|
variant="outlined"
|
||||||
required
|
required
|
||||||
|
class="mb-2"
|
||||||
|
autocomplete="username"
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="password"
|
v-model="password"
|
||||||
label="Contraseña"
|
label="Contraseña"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
type="password"
|
type="password"
|
||||||
:rules="[requiredRule]"
|
:rules="[requiredRule]"
|
||||||
|
variant="outlined"
|
||||||
required
|
required
|
||||||
|
class="mb-4"
|
||||||
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-btn type="submit" color="primary">Entrar</v-btn>
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-alert v-if="error" type="error" class="mt-2">{{ error }}</v-alert>
|
<v-alert
|
||||||
|
v-if="error"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-4"
|
||||||
|
closable
|
||||||
|
@click:close="error = ''"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</v-alert>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import AuthService from '@/services/auth';
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import AuthService from '@/services/auth'
|
||||||
|
import logo from '@/assets/logo_colorful.png'
|
||||||
|
|
||||||
export default {
|
const router = useRouter()
|
||||||
name: 'DonConfiao',
|
|
||||||
|
|
||||||
data() {
|
const username = ref('')
|
||||||
return {
|
const password = ref('')
|
||||||
username: '',
|
const error = ref('')
|
||||||
password: '',
|
const isSubmitting = ref(false)
|
||||||
error: '',
|
const loginForm = ref(null)
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function requiredRule(value) {
|
||||||
requiredRule(value) {
|
return !!value || 'Este campo es obligatorio'
|
||||||
return !!value || 'Este campo es obligatorio';
|
}
|
||||||
},
|
|
||||||
|
|
||||||
async onSubmit() {
|
async function onSubmit() {
|
||||||
this.error = '';
|
error.value = ''
|
||||||
|
isSubmitting.value = true
|
||||||
const form = this.$refs.loginForm;
|
|
||||||
const isValid = await form.validate();
|
|
||||||
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
if (!this.username || !this.password) {
|
|
||||||
this.error = 'Usuario y contraseña son obligatorios';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AuthService.login({
|
const form = loginForm.value
|
||||||
username: this.username,
|
if (form) {
|
||||||
password: this.password,
|
const { valid } = await form.validate()
|
||||||
});
|
if (!valid) return
|
||||||
this.$router.push({ path: '/' });
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e?.response?.data?.message ?? e.message;
|
|
||||||
this.error = msg ?? 'Error al iniciar sesión';
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
if (!username.value || !password.value) {
|
||||||
};
|
error.value = 'Usuario y contraseña son obligatorios'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
//
|
import Login from '@/components/Login.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user