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",
"copyright": "Notsoweb Software Inc.",
"private": true,
"version": "0.9.8",
"version": "0.9.9",
"type": "module",
"scripts": {
"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
});
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>
<template>

View File

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

View File

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

View File

@ -11,9 +11,11 @@ import { bootPermissions, bootRoles } from '@Plugins/RolePermission';
import TailwindScreen from '@Plugins/TailwindScreen'
import { pagePlugin } 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 { hasToken } from './services/Api';
// Configurar axios
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
@ -29,7 +31,7 @@ async function boot() {
// Iniciar rutas
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.route = useRoute();
@ -41,12 +43,14 @@ async function boot() {
if(initRoutes) {
// Iniciar permisos
await bootPermissions();
await bootRoles();
// Iniciar broadcast
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
await import('@Services/Broadcast')
if(hasToken()) {
await bootPermissions();
await bootRoles();
// Iniciar broadcast
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
await import('@Services/Broadcast')
}
}
reloadApp();

View File

@ -298,7 +298,7 @@ export default {
roles:{
create: {
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',
onError: 'Error al crear el role',
},
@ -309,9 +309,10 @@ export default {
onError: 'Error al actualizar el role',
},
update: {
description: 'Actualiza los permisos del rol.',
description: 'Si crees necesario, puedes actualizar el nombre del rol. No afecta a los permisos.',
},
title: 'Roles',
description: 'Gestión de roles del sistema. Puedes crear los roles con los permisos que necesites.',
permissions: {
title: 'Permisos',
description: 'Permisos del rol.',

View File

@ -3,7 +3,7 @@ import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { hasPermission } from '@Plugins/RolePermission';
import { useSearcher } from '@Services/Api';
import { apiTo } from './Module';
import { apiTo, transl } from './Module';
import ModalController from '@Controllers/ModalController.js';
@ -71,7 +71,7 @@ onMounted(() => {
<template>
<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' })">
<IconButton
class="text-white"
@ -81,7 +81,7 @@ onMounted(() => {
/>
</RouterLink>
</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">
<Input

View File

@ -22,8 +22,7 @@ defineProps({
@close="$emit('close')"
>
<Header
:title="model.name"
:subtitle="model.last_name"
:title="model.event"
/>
<div class="flex w-full p-4">
<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 })
// 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
const can = (permission) => hasPermission(`activities.${permission}`)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,16 @@
<script setup>
import { onMounted } 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'
/** Definidores */
const router = useRouter();
/** Propiedades */
defineProps({
canResetPassword: Boolean,
@ -33,7 +38,7 @@ const login = () => {
/** Ciclos */
onMounted(() => {
if (hasToken()) {
location.replace('/')
router.push({ name: 'dashboard.index' });
}
})
</script>
@ -62,7 +67,7 @@ onMounted(() => {
<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="$view({ name: 'forgot-password' })"
:to="viewTo({ name: 'forgot-password' })"
>
{{ $t('auth.forgotPassword.ask') }}
</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 { useRoute, useRouter } from 'vue-router';
import { useForm } from '@Services/Api.js'
import { viewTo } from './Module';
import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/InputWithIcon.vue'
@ -24,21 +24,17 @@ const submit = () => {
form.post(route('auth.reset-password'), {
onSuccess: () => {
Notify.success(Lang('auth.reset.success'));
router.push({ name: 'index' })
router.push(viewTo({ name: 'index' }));
},
onError: () => {
router.push({ name: 'index' });
router.push(viewTo({ name: 'index' }));
}
});
};
onMounted(() => {
console.log('mount')
form.token = vroute.query.token;
email.value = vroute.query.email;
// router.replace({ query: {} });
})
</script>

View File

@ -71,7 +71,28 @@ const changelogs = [
'ADD: Visualización de historial de cambios del backend.',
],
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>

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: [
{
path: '/',
component: () => import('@Layouts/AppLayout.vue'),
children: [
{
path: '',
@ -51,6 +52,7 @@ const router = createRouter({
},
{
path: '/admin',
component: () => import('@Layouts/AppLayout.vue'),
children: [
{
path: 'users',
@ -119,7 +121,7 @@ const router = createRouter({
},
{
path: '/changelogs',
name: 'changelogs',
component: () => import('@Layouts/AppLayout.vue'),
children: [
{
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(.*)*',
name: '404',

View File

@ -73,8 +73,6 @@ const closeSession = () => {
resetCsrfToken()
Notify.info(Lang('session.closed'))
location.replace('auth.html')
}
/**
@ -86,6 +84,13 @@ function composeKey(parent, 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
*/
@ -135,8 +140,6 @@ const api = {
if(options.hasOwnProperty('onFail')) {
options.onFail(data.data);
}
console.log(data.data);
}
if(options.hasOwnProperty('onFinish')) {
@ -214,7 +217,7 @@ const api = {
})
},
resource(resources, options) {
this.post('resources/get', {
this.post(apiURL('resources/get'), {
...options,
data: resources
})
@ -569,6 +572,7 @@ const useSearcher = (options = {
export {
api,
token,
apiURL,
closeSession,
defineCsrfToken,
defineApiToken,

View File

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

View File

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