UPDATE: Simplificación de funsionamiento

- ADD: Función creación de URL a backend fuera de VUEJS.
- UPDATE: Ahora las plantillas se definen en el grupo de rutas, y se heredan en las rutas hijas.
- UPDATE: Traducciones modulares faltantes.
- UPDATE: Simplificación de las rutas de autenticación.
- FIX: Títulos de modal de eliminación ahora son editables.
- FIX: Obtención de recursos de backend mediante `api.resource`.
This commit is contained in:
Moisés de Jesús Cortés Castellanos 2025-03-13 18:29:42 -06:00
parent 28c5ba153b
commit 6b7bccc500
25 changed files with 167 additions and 146 deletions

View File

@ -1,14 +0,0 @@
<!doctype html>
<html id="main-page" lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/auth.js"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
"name": "notsoweb.frontend", "name": "notsoweb.frontend",
"copyright": "Notsoweb Software Inc.", "copyright": "Notsoweb Software Inc.",
"private": true, "private": true,
"version": "0.9.8", "version": "0.9.9",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -1,50 +0,0 @@
import './css/base.css'
import axios from 'axios';
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import { useRoute, ZiggyVue } from 'ziggy-js';
import { i18n, lang } from '@/lang/i18n.js';
import router from '@Router/Auth'
import Notify from '@Plugins/Notify'
import TailwindScreen from '@Plugins/TailwindScreen'
import { defineApp, pagePlugin, reloadApp } from '@Services/Page';
import Auth from '@Holos/Layout/Auth.vue'
// Configurar axios
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Crear instancias globales
window.Lang = lang;
window.Notify = new Notify();
window.TwScreen = new TailwindScreen();
async function boot() {
try {
const routes = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/routes');
const app = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/app');
// Iniciar rutas
window.Ziggy = routes.data;
window.route = useRoute();
defineApp(app.data);
} catch (error) {
console.error(error);
alert('Failed to load routes');
}
reloadApp();
createApp(Auth)
.use(createPinia())
.use(i18n)
.use(pagePlugin)
.use(router)
.use(ZiggyVue)
.mount('#app');
}
// Iniciar aplicación
boot();

23
src/components/App.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup>
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import useLoader from '@Stores/Loader';
import { hasToken } from '@Services/Api';
/** Definidores */
const router = useRouter();
const loader = useLoader();
/** Ciclos */
onMounted(() => {
if(!hasToken()) {
return router.push({ name: 'auth.index' })
}
loader.boot()
})
</script>
<template>
<router-view />
</template>

View File

@ -6,7 +6,7 @@ defineProps({
to: String to: String
}); });
const style = 'block px-4 py-2 text-sm leading-5 hover:bg-secondary/80 dark:hover:bg-secondary-d/80 focus:outline-hidden focus:bg-gray-100 transition'; const style = 'block px-4 py-2 text-sm leading-5 hover:bg-secondary/80 dark:hover:bg-secondary-d/80 focus:outline-hidden focus:bg-gray-100 cursor-pointer transition';
</script> </script>
<template> <template>

View File

@ -1,12 +1,18 @@
<script setup> <script setup>
import { hasToken } from '@Services/Api';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
/** Definidores */ /** Definidores */
const router = useRouter(); const router = useRouter();
/** Métodos */ /** Métodos */
const home = () => router.push(view({ name: 'index' })); const home = () => {
if(hasToken()) {
router.push({ name: 'dashboard.index' });
} else {
location.replace('/');
}
}
</script> </script>
<template> <template>
<div <div

View File

@ -15,6 +15,14 @@ const props = defineProps({
model: Object, model: Object,
show: Boolean, show: Boolean,
to: Function, to: Function,
title: {
type: String,
default: 'name'
},
subtitle: {
type: String,
default: 'description'
}
}); });
/** Métodos */ /** Métodos */
@ -38,8 +46,8 @@ const destroy = (id) => api.delete(props.to(id), {
@destroy="destroy(model.id)" @destroy="destroy(model.id)"
> >
<Header <Header
:subtitle="model.full_last_name" :title="model[title]"
:title="model.name" :subtitle="model[subtitle]"
/> />
</DestroyModal> </DestroyModal>
</template> </template>

View File

@ -11,9 +11,11 @@ import { bootPermissions, bootRoles } from '@Plugins/RolePermission';
import TailwindScreen from '@Plugins/TailwindScreen' import TailwindScreen from '@Plugins/TailwindScreen'
import { pagePlugin } from '@Services/Page'; import { pagePlugin } from '@Services/Page';
import { reloadApp, view } from '@Services/Page'; import { reloadApp, view } from '@Services/Page';
import { apiURL } from '@Services/Api';
import App from '@Layouts/AppLayout.vue' import App from '@Components/App.vue'
import Error503 from '@Pages/Errors/503.vue' import Error503 from '@Pages/Errors/503.vue'
import { hasToken } from './services/Api';
// Configurar axios // Configurar axios
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
@ -29,7 +31,7 @@ async function boot() {
// Iniciar rutas // Iniciar rutas
try { try {
const routes = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/routes'); const routes = await axios.get(apiURL('resources/routes'));
window.Ziggy = routes.data; window.Ziggy = routes.data;
window.route = useRoute(); window.route = useRoute();
@ -41,6 +43,7 @@ async function boot() {
if(initRoutes) { if(initRoutes) {
// Iniciar permisos // Iniciar permisos
if(hasToken()) {
await bootPermissions(); await bootPermissions();
await bootRoles(); await bootRoles();
@ -48,6 +51,7 @@ async function boot() {
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') { if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
await import('@Services/Broadcast') await import('@Services/Broadcast')
} }
}
reloadApp(); reloadApp();

View File

@ -298,7 +298,7 @@ export default {
roles:{ roles:{
create: { create: {
title: 'Crear rol', title: 'Crear rol',
description: 'Estos roles serán usados para dar permisos en el sistema.', description: 'Este nombre sera necesario para identificar el rol en el sistema. Procura que sea algo simple.',
onSuccess: 'Rol creado exitosamente', onSuccess: 'Rol creado exitosamente',
onError: 'Error al crear el role', onError: 'Error al crear el role',
}, },
@ -309,9 +309,10 @@ export default {
onError: 'Error al actualizar el role', onError: 'Error al actualizar el role',
}, },
update: { update: {
description: 'Actualiza los permisos del rol.', description: 'Si crees necesario, puedes actualizar el nombre del rol. No afecta a los permisos.',
}, },
title: 'Roles', title: 'Roles',
description: 'Gestión de roles del sistema. Puedes crear los roles con los permisos que necesites.',
permissions: { permissions: {
title: 'Permisos', title: 'Permisos',
description: 'Permisos del rol.', description: 'Permisos del rol.',

View File

@ -3,7 +3,7 @@ import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { hasPermission } from '@Plugins/RolePermission'; import { hasPermission } from '@Plugins/RolePermission';
import { useSearcher } from '@Services/Api'; import { useSearcher } from '@Services/Api';
import { apiTo } from './Module'; import { apiTo, transl } from './Module';
import ModalController from '@Controllers/ModalController.js'; import ModalController from '@Controllers/ModalController.js';
@ -71,7 +71,7 @@ onMounted(() => {
<template> <template>
<div> <div>
<Header :title="$t('admin.activity.title')"> <Header :title="transl('title')">
<RouterLink v-if="filters.user && hasPermission('users.index')" :to="$view({ name: 'admin.users.index' })"> <RouterLink v-if="filters.user && hasPermission('users.index')" :to="$view({ name: 'admin.users.index' })">
<IconButton <IconButton
class="text-white" class="text-white"
@ -81,7 +81,7 @@ onMounted(() => {
/> />
</RouterLink> </RouterLink>
</Header> </Header>
<p class="mt-2 text-sm">{{ $t('admin.activity.description') }}</p> <p class="mt-2 text-sm">{{ transl('description') }}</p>
<div id="filters" class="grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-4"> <div id="filters" class="grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-4">
<Input <Input

View File

@ -22,8 +22,7 @@ defineProps({
@close="$emit('close')" @close="$emit('close')"
> >
<Header <Header
:title="model.name" :title="model.event"
:subtitle="model.last_name"
/> />
<div class="flex w-full p-4"> <div class="flex w-full p-4">
<GoogleIcon <GoogleIcon

View File

@ -8,7 +8,7 @@ const apiTo = (name, params = {}) => route(`admin.activities.${name}`, params)
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.activities.${name}`, params, query }) const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.activities.${name}`, params, query })
// Obtener traducción del componente // Obtener traducción del componente
const transl = (str) => lang(`activities.${str}`) const transl = (str) => lang(`admin.activity.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos // Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`activities.${permission}`) const can = (permission) => hasPermission(`activities.${permission}`)

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { can, apiTo, viewTo } from './Module' import { can, apiTo, viewTo, transl } from './Module'
import { useSearcher } from '@Services/Api'; import { useSearcher } from '@Services/Api';
import ModalController from '@Controllers/ModalController.js'; import ModalController from '@Controllers/ModalController.js';
@ -36,7 +36,7 @@ onMounted(() => {
<template> <template>
<div> <div>
<SearcherHead <SearcherHead
:title="$t('roles.title')" :title="transl('title')"
@search="(x) => searcher.search(x)" @search="(x) => searcher.search(x)"
> >
<RouterLink <RouterLink
@ -56,7 +56,10 @@ onMounted(() => {
@click="searcher.search()" @click="searcher.search()"
/> />
</SearcherHead> </SearcherHead>
<div class="pt-2 w-full"> <div class="pt-2 space-y-2 w-full">
<p class="text-sm">
{{ transl('description') }}
</p>
<Table <Table
:items="models" :items="models"
@send-pagination="(page) => searcher.pagination(page)" @send-pagination="(page) => searcher.pagination(page)"
@ -79,7 +82,7 @@ onMounted(() => {
<IconButton <IconButton
v-if="can('edit') && ![1,2].includes(model.id)" v-if="can('edit') && ![1,2].includes(model.id)"
icon="license" icon="license"
:title="$t('roles.permissions.title')" :title="transl('permissions.title')"
@click="Modal.switchEditModal(model)" @click="Modal.switchEditModal(model)"
outline outline
/> />
@ -116,6 +119,8 @@ onMounted(() => {
/> />
<DestroyView <DestroyView
v-if="can('destroy')" v-if="can('destroy')"
title="description"
subtitle=""
:model="modelModal" :model="modelModal"
:show="destroyModal" :show="destroyModal"
:to="(role) => apiTo('destroy', { role })" :to="(role) => apiTo('destroy', { role })"

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { can, apiTo, viewTo } from './Module' import { can, apiTo, viewTo, transl } from './Module'
import { useSearcher } from '@Services/Api'; import { useSearcher } from '@Services/Api';
import { hasPermission } from '@Plugins/RolePermission'; import { hasPermission } from '@Plugins/RolePermission';
@ -38,7 +38,7 @@ onMounted(() => {
<template> <template>
<div> <div>
<SearcherHead <SearcherHead
:title="$t('users.title')" :title="transl('title')"
@search="(x) => searcher.search(x)" @search="(x) => searcher.search(x)"
> >
<RouterLink <RouterLink
@ -173,6 +173,7 @@ onMounted(() => {
/> />
<DestroyView <DestroyView
v-if="can('destroy')" v-if="can('destroy')"
subtitle="last_name"
:model="modelModal" :model="modelModal"
:show="destroyModal" :show="destroyModal"
:to="(user) => apiTo('destroy', { user })" :to="(user) => apiTo('destroy', { user })"

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { can, apiTo, viewTo } from './Module' import { can, apiTo, viewTo, transl } from './Module'
import { users } from '@Plugins/AuthUsers' import { users } from '@Plugins/AuthUsers'
import ModalController from '@Controllers/ModalController.js'; import ModalController from '@Controllers/ModalController.js';
@ -24,7 +24,7 @@ const modelModal = ref(Modal.modelModal);
<template> <template>
<div> <div>
<Header <Header
:title="$t('users.online.title')" :title="transl('online.title')"
> >
<RouterLink <RouterLink
v-if="can('create')" v-if="can('create')"
@ -38,7 +38,7 @@ const modelModal = ref(Modal.modelModal);
/> />
</RouterLink> </RouterLink>
</Header> </Header>
<p class="mt-2">{{ $t('users.online.description') }} {{ users.length - 1 }} {{ $t('users.online.count') }}</p> <p class="mt-2">{{ transl('online.description') }} {{ users.length - 1 }} {{ transl('online.count') }}</p>
<div class="w-full -mt-2"> <div class="w-full -mt-2">
<Table <Table
:items="users" :items="users"

View File

@ -1,6 +1,8 @@
<script setup> <script setup>
import { useForm } from '@Services/Api';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useForm } from '@Services/Api';
import { viewTo } from './Module';
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
import PrimaryButton from '@Holos/Button/Primary.vue' import PrimaryButton from '@Holos/Button/Primary.vue'
@ -21,11 +23,11 @@ const submit = () => {
form.post(route('auth.forgot-password'), { form.post(route('auth.forgot-password'), {
onSuccess: () => { onSuccess: () => {
Notify.success(Lang('auth.forgotPassword.success')); Notify.success(Lang('auth.forgotPassword.success'));
router.push({ name: 'index' }); router.push(viewTo({ name: 'index' }));
}, },
onError: () => { onError: () => {
Notify.error(Lang('auth.forgotPassword.error')); Notify.error(Lang('auth.forgotPassword.error'));
router.push({ name: 'index' }); router.push(viewTo({ name: 'index' }));
} }
}); });
}; };

View File

@ -1,11 +1,16 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { defineApiToken, defineCsrfToken, hasToken, useForm } from '@Services/Api.js' import { defineApiToken, defineCsrfToken, hasToken, useForm } from '@Services/Api.js'
import { defineUser } from '@Services/Page'; import { defineUser } from '@Services/Page';
import { viewTo } from './Module.js';
import PrimaryButton from '@Holos/Button/Primary.vue' import PrimaryButton from '@Holos/Button/Primary.vue'
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
/** Definidores */
const router = useRouter();
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
canResetPassword: Boolean, canResetPassword: Boolean,
@ -33,7 +38,7 @@ const login = () => {
/** Ciclos */ /** Ciclos */
onMounted(() => { onMounted(() => {
if (hasToken()) { if (hasToken()) {
location.replace('/') router.push({ name: 'dashboard.index' });
} }
}) })
</script> </script>
@ -62,7 +67,7 @@ onMounted(() => {
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<RouterLink <RouterLink
class="text-sm ml-2 hover:text-blue-200 cursor-pointer hover:-translate-y-1 duration-500 transition-all" class="text-sm ml-2 hover:text-blue-200 cursor-pointer hover:-translate-y-1 duration-500 transition-all"
:to="$view({ name: 'forgot-password' })" :to="viewTo({ name: 'forgot-password' })"
> >
{{ $t('auth.forgotPassword.ask') }} {{ $t('auth.forgotPassword.ask') }}
</RouterLink> </RouterLink>

17
src/pages/Auth/Module.js Normal file
View File

@ -0,0 +1,17 @@
import { lang } from '@Lang/i18n';
// Ruta API
const apiTo = (name, params = {}) => route(`auth.${name}`, params)
// Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `auth.${name}`, params, query })
// Obtener traducción del componente
const transl = (str) => lang(`auth.${str}`)
export {
viewTo,
apiTo,
transl
}

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useForm } from '@Services/Api.js' import { useForm } from '@Services/Api.js'
import { viewTo } from './Module';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
@ -24,21 +24,17 @@ const submit = () => {
form.post(route('auth.reset-password'), { form.post(route('auth.reset-password'), {
onSuccess: () => { onSuccess: () => {
Notify.success(Lang('auth.reset.success')); Notify.success(Lang('auth.reset.success'));
router.push({ name: 'index' }) router.push(viewTo({ name: 'index' }));
}, },
onError: () => { onError: () => {
router.push({ name: 'index' }); router.push(viewTo({ name: 'index' }));
} }
}); });
}; };
onMounted(() => { onMounted(() => {
console.log('mount')
form.token = vroute.query.token; form.token = vroute.query.token;
email.value = vroute.query.email; email.value = vroute.query.email;
// router.replace({ query: {} });
}) })
</script> </script>

View File

@ -71,7 +71,28 @@ const changelogs = [
'ADD: Visualización de historial de cambios del backend.', 'ADD: Visualización de historial de cambios del backend.',
], ],
date: '2025-01-17' date: '2025-01-17'
} },
{
version: '0.9.8',
details: [
'UPDATE: Actualización de dependencias.',
'UPDATE: TailwindCSS 3 => 4.',
'UPDATE: Actualización de Diseño, mejoras visuales.',
],
date: '2025-03-04'
},
{
version: '0.9.9',
details: [
'FIX: Obtención de recursos de backend mediante `api.resource`.',
'FIX: Títulos de modal de eliminación ahora son editables.',
'UPDATE: Simplificación de las rutas de autenticación.',
'UPDATE: Traducciones modulares faltantes.',
'UPDATE: Ahora las plantillas se definen en el grupo de rutas, y se heredan en las rutas hijas.',
'ADD: Función creación de URL a backend fuera de VUEJS.',
],
date: '2025-03-13'
},
] ]
</script> </script>

View File

@ -1,24 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: () => import('@Pages/Auth/Login.vue')
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('@Pages/Auth/ForgotPassword.vue')
},
{
path: '/reset-password',
name: 'reset-password',
component: () => import('@Pages/Auth/ResetPassword.vue')
}
]
})
export default router

View File

@ -16,6 +16,7 @@ const router = createRouter({
routes: [ routes: [
{ {
path: '/', path: '/',
component: () => import('@Layouts/AppLayout.vue'),
children: [ children: [
{ {
path: '', path: '',
@ -51,6 +52,7 @@ const router = createRouter({
}, },
{ {
path: '/admin', path: '/admin',
component: () => import('@Layouts/AppLayout.vue'),
children: [ children: [
{ {
path: 'users', path: 'users',
@ -119,7 +121,7 @@ const router = createRouter({
}, },
{ {
path: '/changelogs', path: '/changelogs',
name: 'changelogs', component: () => import('@Layouts/AppLayout.vue'),
children: [ children: [
{ {
path: '', path: '',
@ -133,6 +135,27 @@ const router = createRouter({
} }
] ]
}, },
{
path: '/auth',
component: () => import('@Holos/Layout/Auth.vue'),
children: [
{
path: '',
name: 'auth.index',
component: () => import('@Pages/Auth/Login.vue')
},
{
path: 'forgot-password',
name: 'auth.forgot-password',
component: () => import('@Pages/Auth/ForgotPassword.vue')
},
{
path: 'reset-password',
name: 'auth.reset-password',
component: () => import('@Pages/Auth/ResetPassword.vue')
}
]
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: '404', name: '404',

View File

@ -73,8 +73,6 @@ const closeSession = () => {
resetCsrfToken() resetCsrfToken()
Notify.info(Lang('session.closed')) Notify.info(Lang('session.closed'))
location.replace('auth.html')
} }
/** /**
@ -86,6 +84,13 @@ function composeKey(parent, key) {
return parent ? parent + '[' + key + ']' : key return parent ? parent + '[' + key + ']' : key
} }
/**
* URL API
*/
const apiURL = (path) => {
return import.meta.env.VITE_API_URL + '/api/' + path
}
/** /**
* Instancia de la API de uso directo * Instancia de la API de uso directo
*/ */
@ -135,8 +140,6 @@ const api = {
if(options.hasOwnProperty('onFail')) { if(options.hasOwnProperty('onFail')) {
options.onFail(data.data); options.onFail(data.data);
} }
console.log(data.data);
} }
if(options.hasOwnProperty('onFinish')) { if(options.hasOwnProperty('onFinish')) {
@ -214,7 +217,7 @@ const api = {
}) })
}, },
resource(resources, options) { resource(resources, options) {
this.post('resources/get', { this.post(apiURL('resources/get'), {
...options, ...options,
data: resources data: resources
}) })
@ -569,6 +572,7 @@ const useSearcher = (options = {
export { export {
api, api,
token, token,
apiURL,
closeSession, closeSession,
defineCsrfToken, defineCsrfToken,
defineApiToken, defineApiToken,

View File

@ -110,6 +110,8 @@ const logout = () => {
onSuccess: (r) => { onSuccess: (r) => {
if(r.is_revoked === true) { if(r.is_revoked === true) {
closeSession() closeSession()
location.replace('/')
} }
} }
}); });

View File

@ -7,14 +7,6 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue(), tailwindcss()], plugins: [vue(), tailwindcss()],
build: {
rollupOptions: {
input: {
main: './index.html', // Ruta al archivo index.html
auth: './auth.html', // Ruta al archivo auth.html
},
},
},
server: { server: {
allowedHosts: true, allowedHosts: true,
}, },