fix: login diseño

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-14 17:07:17 -06:00
parent 0ffa93019c
commit b1d0f73e94
2 changed files with 159 additions and 75 deletions

View File

@ -4,7 +4,6 @@ import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
import useDarkMode from '@Stores/DarkMode'
import Logo from '@Holos/Logo.vue'
import IconButton from '@Holos/Button/Icon.vue'
/** Definidores */
const darkMode = useDarkMode()
@ -21,49 +20,56 @@ onMounted(() => {
</script>
<template>
<div class="h-screen flex bg-primary dark:bg-primary-d">
<div
class="relative flex w-full lg:w-full justify-around items-center with-transition"
:class="{'app-bg-light':darkMode.isLight,'app-bg-dark':darkMode.isDark}"
>
<header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white">
<div>
<IconButton v-if="darkMode.isLight"
icon="light_mode"
:title="$t('app.theme.light')"
@click="darkMode.applyDark()"
/>
<IconButton v-else
icon="dark_mode"
:title="$t('app.theme.dark')"
@click="darkMode.applyLight()"
/>
</div>
</header>
<div class="flex w-full flex-col items-center justify-center space-y-2">
<div class="flex space-x-2 items-center justify-start text-white">
<Logo
class="text-lg inline-flex"
/>
<div class="min-h-screen flex">
<!-- Panel Izquierdo - Branding -->
<div class="hidden lg:flex lg:w-1/2 bg-primary dark:bg-primary-d relative overflow-hidden">
<!-- Fondo con patron sutil -->
<div class="absolute inset-0 opacity-5">
<div class="absolute inset-0" style="background-image: radial-gradient(circle at 25px 25px, white 2px, transparent 0); background-size: 50px 50px;"></div>
</div>
<!-- Contenido centrado -->
<div class="relative z-10 flex flex-col items-center justify-center w-full px-12 text-white">
<!-- Logo/Icono -->
<div class="mb-8">
<Logo size="xl" class="text-lg inline-flex" />
</div>
<!-- Titulo -->
<h1 class="text-3xl font-bold text-center mb-4">
Portal Administrativo
</h1>
<!-- Descripcion -->
<p class="text-white/70 text-center max-w-sm mb-8">
Sistema de Gestion de Inventario y Ventas.<br>
</p>
</div>
</div>
<!-- Panel Derecho - Formulario -->
<div class="w-full lg:w-1/2 bg-page dark:bg-page-d flex flex-col">
<!-- Contenido principal -->
<div class="flex-1 flex items-center justify-center px-6 py-12">
<div class="w-full max-w-md">
<!-- Logo mobile -->
<div class="lg:hidden flex justify-center mb-8">
<div class="w-16 h-16 bg-primary dark:bg-primary-d rounded-xl flex items-center justify-center">
<Logo class="scale-50" />
</div>
</div>
<main class="bg-white/10 w-full backdrop-blur-xs text-white px-4 py-4 rounded-sm max-w-80">
<RouterView />
</main>
<footer class="absolute bottom-0 flex w-full h-8 px-4 items-center justify-between bg-primary dark:bg-primary-d backdrop-blur-md text-white transition-colors duration-global">
<div>
<span>
&copy;2024 {{ APP_COPYRIGHT }}
</span>
</div>
<div>
<span>
APP {{ APP_VERSION }} API {{ $page.app.version }}
</span>
</div>
<!-- Footer del panel derecho -->
<footer class="py-4 px-6 border-t border-primary/10 dark:border-primary-dt/10">
<div class="flex items-center justify-between text-xs text-page-t/50 dark:text-page-dt/50">
<span>&copy; {{ new Date().getFullYear() }} {{ APP_COPYRIGHT }}</span>
<span>v{{ APP_VERSION }}</span>
</div>
</footer>
</div>
</div>
</div>
</template>

View File

@ -1,12 +1,13 @@
<script setup>
import { onMounted } from 'vue';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { defineApiToken, defineCsrfToken, hasToken, useForm } from '@Services/Api.js'
import { defineUser } from '@Services/Page';
import { viewTo } from './Module.js';
import PrimaryButton from '@Holos/Button/Primary.vue'
import Input from '@Holos/Form/InputWithIcon.vue'
import GoogleIcon from '@Shared/GoogleIcon.vue'
import Error from '@Holos/Form/Elements/Error.vue'
/** Definidores */
const router = useRouter();
@ -22,19 +23,29 @@ const form = useForm({
password: ''
});
const showPassword = ref(false);
const isLoading = ref(false);
/** Métodos */
const login = () => {
isLoading.value = true;
form.post(route('auth.login'), {
onSuccess: (res) => {
defineApiToken(res.token)
defineUser(res.user)
defineCsrfToken(res.csrf)
location.replace('/')
},
onFinish: () => {
isLoading.value = false;
}
});
};
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
/** Ciclos */
onMounted(() => {
if (hasToken()) {
@ -44,33 +55,100 @@ onMounted(() => {
</script>
<template>
<form @submit.prevent="login">
<Input
icon="mail"
<div class="w-full">
<!-- Header del formulario -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-page-t dark:text-page-dt mb-2">
Iniciar Sesion
</h1>
<p class="text-page-t/60 dark:text-page-dt/60 text-sm">
Ingrese sus credenciales.
</p>
</div>
<form @submit.prevent="login" class="space-y-6">
<!-- Campo Email/Usuario -->
<div class="space-y-2">
<label for="email" class="block text-xs font-semibold text-page-t/70 dark:text-page-dt/70 uppercase tracking-wider">
Usuario / ID
</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<GoogleIcon
name="person"
class="text-page-t/40 dark:text-page-dt/40 group-focus-within:text-primary dark:group-focus-within:text-primary-dt transition-colors"
/>
</div>
<input
id="email"
type="email"
v-model="form.email"
:onError="form.errors.email"
:placeholder="$t('email.title')"
placeholder="ID de empleado"
class="w-full pl-12 pr-4 py-3.5 bg-page dark:bg-page-d border border-primary/20 dark:border-primary-dt/20 rounded-lg
text-page-t dark:text-page-dt placeholder-page-t/40 dark:placeholder-page-dt/40
focus:outline-none focus:border-primary/50 dark:focus:border-primary-dt/50 focus:ring-2 focus:ring-primary/10 dark:focus:ring-primary-dt/10
hover:border-primary/30 dark:hover:border-primary-dt/30 transition-all duration-200"
:class="{ 'border-danger! focus:border-danger! focus:ring-danger/20!': form.errors.email }"
autocomplete="email"
/>
<Input
v-model="form.password"
icon="password"
id="password"
type="password"
:onError="form.errors.password"
:placeholder="$t('password')"
/>
<PrimaryButton class="!w-full">
{{ $t('auth.login') }}
</PrimaryButton>
<div class="flex justify-end mt-4">
<RouterLink
class="text-sm ml-2 hover:text-blue-200 cursor-pointer hover:-translate-y-1 duration-500 transition-all"
:to="viewTo({ name: 'forgot-password' })"
>
{{ $t('auth.forgotPassword.ask') }}
</RouterLink>
</div>
<Error :onError="form.errors.email" />
</div>
<!-- Campo Password -->
<div class="space-y-2">
<label for="password" class="block text-xs font-semibold text-page-t/70 dark:text-page-dt/70 uppercase tracking-wider">
Contraseña
</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<GoogleIcon
name="lock"
class="text-page-t/40 dark:text-page-dt/40 group-focus-within:text-primary dark:group-focus-within:text-primary-dt transition-colors"
/>
</div>
<input
id="password"
:type="showPassword ? 'text' : 'password'"
v-model="form.password"
placeholder="••••••••••"
class="w-full pl-12 pr-12 py-3.5 bg-page dark:bg-page-d border border-primary/20 dark:border-primary-dt/20 rounded-lg
text-page-t dark:text-page-dt placeholder-page-t/40 dark:placeholder-page-dt/40
focus:outline-none focus:border-primary/50 dark:focus:border-primary-dt/50 focus:ring-2 focus:ring-primary/10 dark:focus:ring-primary-dt/10
hover:border-primary/30 dark:hover:border-primary-dt/30 transition-all duration-200"
:class="{ 'border-danger! focus:border-danger! focus:ring-danger/20!': form.errors.password }"
autocomplete="current-password"
/>
<button
type="button"
@click="togglePassword"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-page-t/40 dark:text-page-dt/40 hover:text-page-t dark:hover:text-page-dt transition-colors"
>
<GoogleIcon :name="showPassword ? 'visibility_off' : 'visibility'" />
</button>
</div>
<Error :onError="form.errors.password" />
</div>
<!-- Boton de Login -->
<PrimaryButton
class="w-full! py-4! text-sm! font-semibold! rounded-lg! mt-2!
bg-primary! hover:bg-primary/90! dark:bg-primary-d! dark:hover:bg-primary-d/90!
transition-all duration-200"
:disabled="isLoading || form.processing"
:class="{ 'opacity-70 cursor-not-allowed': isLoading || form.processing }"
>
<span v-if="isLoading || form.processing" class="flex items-center justify-center gap-2">
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Verificando...
</span>
<span v-else>
Acceder al Sistema
</span>
</PrimaryButton>
</form>
</div>
</template>