@luis-angel-martin-dzul/vue3-tmpl 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/bin/create.js +81 -0
- package/package.json +18 -0
- package/template/_gitignore +6 -0
- package/template/index.html +12 -0
- package/template/package.json +25 -0
- package/template/postcss.config.js +6 -0
- package/template/src/App.vue +3 -0
- package/template/src/main.js +12 -0
- package/template/src/models/Auth.js +32 -0
- package/template/src/router/index.js +56 -0
- package/template/src/stores/auth.js +25 -0
- package/template/src/style.css +33 -0
- package/template/src/utils/static.js +11 -0
- package/template/src/views/auth/LoginView.vue +60 -0
- package/template/src/views/dashboard/DashboardView.vue +9 -0
- package/template/src/views/errors/404NotFound.vue +16 -0
- package/template/src/views/home/HomeView.vue +9 -0
- package/template/src/views/menu/BaseLayout.vue +205 -0
- package/template/src/views/modulo/Opcion1View.vue +9 -0
- package/template/src/views/modulo/Opcion2View.vue +9 -0
- package/template/vite.config.js +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# vue3-tmpl
|
|
2
|
+
|
|
3
|
+
Generador de plantillas para proyectos Vue 3 con Router, Pinia, Tailwind CSS y FontAwesome preconfigurados.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Requisitos
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
- npm 7+
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Uso
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @luis-angel-martin-dzul/vue3-tmpl <nombre-del-proyecto>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Ejemplo
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @luis-angel-martin-dzul/vue3-tmpl mi-proyecto
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Primeros pasos
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd mi-proyecto
|
|
32
|
+
npm install
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Estructura generada
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
mi-proyecto/
|
|
42
|
+
├── index.html
|
|
43
|
+
├── vite.config.js ← alias @ → src/
|
|
44
|
+
├── postcss.config.js ← Tailwind + autoprefixer
|
|
45
|
+
├── package.json
|
|
46
|
+
├── .gitignore
|
|
47
|
+
└── src/
|
|
48
|
+
├── main.js ← Vue + Pinia + Router + FontAwesome
|
|
49
|
+
├── style.css ← Tailwind + clases utilitarias (.btnGreen, .btnBlue, etc.)
|
|
50
|
+
├── App.vue ← solo <RouterView />
|
|
51
|
+
├── router/
|
|
52
|
+
│ └── index.js
|
|
53
|
+
├── stores/
|
|
54
|
+
│ └── auth.js ← store de ejemplo con Pinia
|
|
55
|
+
├── models/
|
|
56
|
+
│ └── Auth.js ← clase Auth con ping y updatePassword
|
|
57
|
+
├── utils/
|
|
58
|
+
│ └── static.js ← BASE url + apiServerRequest
|
|
59
|
+
└── views/
|
|
60
|
+
├── auth/
|
|
61
|
+
│ └── LoginView.vue ← formulario de login
|
|
62
|
+
├── menu/
|
|
63
|
+
│ └── BaseLayout.vue ← sidebar colapsable + header
|
|
64
|
+
├── home/
|
|
65
|
+
│ └── HomeView.vue
|
|
66
|
+
├── dashboard/
|
|
67
|
+
│ └── DashboardView.vue
|
|
68
|
+
├── modulo/ ← ejemplo de submenu
|
|
69
|
+
│ ├── Opcion1View.vue
|
|
70
|
+
│ └── Opcion2View.vue
|
|
71
|
+
└── errors/
|
|
72
|
+
└── 404NotFound.vue
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Rutas incluidas
|
|
78
|
+
|
|
79
|
+
| Ruta | Componente | Descripción |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `/login` | `LoginView` | Pantalla de inicio de sesión |
|
|
82
|
+
| `/` | `HomeView` | Inicio (dentro de BaseLayout) |
|
|
83
|
+
| `/dashboard` | `DashboardView` | Dashboard (dentro de BaseLayout) |
|
|
84
|
+
| `/modulo/opcion-1` | `Opcion1View` | Ejemplo submenu opción 1 |
|
|
85
|
+
| `/modulo/opcion-2` | `Opcion2View` | Ejemplo submenu opción 2 |
|
|
86
|
+
| `/:pathMatch(.*)` | `404NotFound` | Página no encontrada |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Incluido por defecto
|
|
91
|
+
|
|
92
|
+
| Paquete | Uso |
|
|
93
|
+
|---|---|
|
|
94
|
+
| **Vue 3** | Framework |
|
|
95
|
+
| **Vue Router 4** | Navegación entre vistas |
|
|
96
|
+
| **Pinia** | Estado global |
|
|
97
|
+
| **Tailwind CSS v4** | Estilos utilitarios |
|
|
98
|
+
| **FontAwesome 6** | Iconos |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Layout (BaseLayout)
|
|
103
|
+
|
|
104
|
+
El sidebar incluye:
|
|
105
|
+
- **Colapso** en desktop (icono + texto → solo icono)
|
|
106
|
+
- **Overlay** en mobile con botón hamburguesa
|
|
107
|
+
- **Estado activo** calculado por computed según la ruta actual
|
|
108
|
+
- **Submenus** con animación de apertura/cierre y apertura automática al navegar directo a una ruta hija
|
|
109
|
+
|
|
110
|
+
Para agregar un nuevo submenu, replica el bloque `Módulo` en `BaseLayout.vue` y añade las rutas correspondientes en `router/index.js`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Clases de botones
|
|
115
|
+
|
|
116
|
+
Definidas en `src/style.css` listas para usar en cualquier vista:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<button class="btnGreen px-4 py-2">Guardar</button>
|
|
120
|
+
<button class="btnBlue px-4 py-2">Buscar</button>
|
|
121
|
+
<button class="btnOrange px-4 py-2">Editar</button>
|
|
122
|
+
<button class="btnRose px-4 py-2">Eliminar</button>
|
|
123
|
+
<button class="btnWhite px-4 py-2">Cancelar</button>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Patrón de modelos
|
|
129
|
+
|
|
130
|
+
Los modelos en `src/models/` usan `src/utils/static.js` como base para las peticiones HTTP:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// src/utils/static.js
|
|
134
|
+
export const BASE = 'https://tu-api.com/'
|
|
135
|
+
|
|
136
|
+
export async function apiServerRequest(url, options = {}) {
|
|
137
|
+
try {
|
|
138
|
+
return await fetch(url, options)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Request error:', error)
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
// src/models/MiModelo.js
|
|
148
|
+
import * as Static from '@/utils/static'
|
|
149
|
+
|
|
150
|
+
class MiModelo {
|
|
151
|
+
#endpoint = 'mi-endpoint'
|
|
152
|
+
|
|
153
|
+
constructor() {
|
|
154
|
+
this.#endpoint = Static.BASE + this.#endpoint
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getAll() {
|
|
158
|
+
const request = await Static.apiServerRequest(this.#endpoint)
|
|
159
|
+
if (request?.status === 200) return await request.json()
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async create(data) {
|
|
164
|
+
const request = await Static.apiServerRequest(this.#endpoint, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify(data),
|
|
168
|
+
})
|
|
169
|
+
if (request?.status === 200) return await request.json()
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default new MiModelo()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Patrón de stores (Pinia)
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
// src/stores/miStore.js
|
|
183
|
+
import { defineStore } from 'pinia'
|
|
184
|
+
import { ref } from 'vue'
|
|
185
|
+
import MiModelo from '@/models/MiModelo'
|
|
186
|
+
|
|
187
|
+
export const useMiStore = defineStore('mi-store', () => {
|
|
188
|
+
const datos = ref([])
|
|
189
|
+
const loading = ref(false)
|
|
190
|
+
|
|
191
|
+
async function cargar() {
|
|
192
|
+
loading.value = true
|
|
193
|
+
datos.value = await MiModelo.getAll()
|
|
194
|
+
loading.value = false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { datos, loading, cargar }
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Scripts disponibles
|
|
204
|
+
|
|
205
|
+
| Comando | Descripción |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `npm run dev` | Servidor de desarrollo |
|
|
208
|
+
| `npm run build` | Build de producción en `dist/` |
|
|
209
|
+
| `npm run preview` | Vista previa del build |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Publicar el CLI en npm
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm publish --access public
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Autor
|
|
222
|
+
|
|
223
|
+
Luis A Martin Dzul — luis.martin@gamasis.mx
|
package/bin/create.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync, cpSync, readFileSync, writeFileSync,
|
|
5
|
+
existsSync, renameSync, readdirSync, statSync,
|
|
6
|
+
} from 'node:fs'
|
|
7
|
+
import { resolve, dirname, join, extname } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
|
|
12
|
+
const TEXT_EXTENSIONS = new Set(['.js', '.ts', '.vue', '.json', '.html', '.css', '.md', ''])
|
|
13
|
+
|
|
14
|
+
function toPascalCase(str) {
|
|
15
|
+
return str
|
|
16
|
+
.replace(/[@/]/g, '-')
|
|
17
|
+
.split(/[-_]/)
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
20
|
+
.join('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function replaceInFile(filePath, projectName, libName) {
|
|
24
|
+
const ext = extname(filePath)
|
|
25
|
+
if (!TEXT_EXTENSIONS.has(ext)) return
|
|
26
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
27
|
+
const updated = content
|
|
28
|
+
.replaceAll('{{PROJECT_NAME}}', projectName)
|
|
29
|
+
.replaceAll('{{LIB_NAME}}', libName)
|
|
30
|
+
writeFileSync(filePath, updated, 'utf-8')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function walkAndReplace(dir, projectName, libName) {
|
|
34
|
+
for (const entry of readdirSync(dir)) {
|
|
35
|
+
const full = join(dir, entry)
|
|
36
|
+
if (statSync(full).isDirectory()) {
|
|
37
|
+
walkAndReplace(full, projectName, libName)
|
|
38
|
+
} else {
|
|
39
|
+
replaceInFile(full, projectName, libName)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Main ────────────────────────────────────────────────────────
|
|
45
|
+
const projectName = process.argv[2]
|
|
46
|
+
|
|
47
|
+
if (!projectName) {
|
|
48
|
+
console.log('\nUso: npx @luis-angel-martin-dzul/vue3-tmpl <nombre-del-proyecto>\n')
|
|
49
|
+
console.log('Ejemplo:')
|
|
50
|
+
console.log(' npx @luis-angel-martin-dzul/vue3-tmpl mi-proyecto\n')
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const targetDir = resolve(process.cwd(), projectName)
|
|
55
|
+
|
|
56
|
+
if (existsSync(targetDir)) {
|
|
57
|
+
console.error(`\nError: el directorio "${projectName}" ya existe.\n`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const templateDir = resolve(__dirname, '../template')
|
|
62
|
+
const libName = toPascalCase(projectName)
|
|
63
|
+
|
|
64
|
+
console.log(`\nCreando proyecto "${projectName}"...`)
|
|
65
|
+
|
|
66
|
+
mkdirSync(targetDir, { recursive: true })
|
|
67
|
+
cpSync(templateDir, targetDir, { recursive: true })
|
|
68
|
+
|
|
69
|
+
// Restaurar .gitignore (npm lo elimina al publicar)
|
|
70
|
+
const gitignoreSrc = join(targetDir, '_gitignore')
|
|
71
|
+
const gitignoreDst = join(targetDir, '.gitignore')
|
|
72
|
+
if (existsSync(gitignoreSrc)) renameSync(gitignoreSrc, gitignoreDst)
|
|
73
|
+
|
|
74
|
+
// Reemplazar placeholders en todos los archivos de texto
|
|
75
|
+
walkAndReplace(targetDir, projectName, libName)
|
|
76
|
+
|
|
77
|
+
console.log(`\n✓ Listo\n`)
|
|
78
|
+
console.log(`Siguientes pasos:\n`)
|
|
79
|
+
console.log(` cd ${projectName}`)
|
|
80
|
+
console.log(` npm install`)
|
|
81
|
+
console.log(` npm run dev\n`)
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luis-angel-martin-dzul/vue3-tmpl",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Generador de plantillas para proyectos Vue 3 con Router, Pinia, Tailwind y FontAwesome",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vue3-tmpl": "./bin/create.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npm pack",
|
|
15
|
+
"publish:npm": "npm publish --access public"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{PROJECT_NAME}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fortawesome/fontawesome-free": "^6.7.2",
|
|
13
|
+
"pinia": "^2.3.1",
|
|
14
|
+
"vue": "^3.5.24",
|
|
15
|
+
"vue-router": "^4.5.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/postcss": "^4.1.8",
|
|
19
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
20
|
+
"autoprefixer": "^10.4.21",
|
|
21
|
+
"postcss": "^8.5.3",
|
|
22
|
+
"tailwindcss": "^4.1.8",
|
|
23
|
+
"vite": "^7.2.4"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
import '@fortawesome/fontawesome-free/css/all.min.css'
|
|
4
|
+
import './style.css'
|
|
5
|
+
import App from './App.vue'
|
|
6
|
+
import router from './router'
|
|
7
|
+
|
|
8
|
+
const app = createApp(App)
|
|
9
|
+
|
|
10
|
+
app.use(createPinia())
|
|
11
|
+
app.use(router)
|
|
12
|
+
app.mount('#app')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Static from '@/utils/static'
|
|
2
|
+
|
|
3
|
+
class Auth {
|
|
4
|
+
#endpoint = 'auth'
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.#endpoint = Static.BASE + this.#endpoint
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async ping() {
|
|
11
|
+
const request = await Static.apiServerRequest(Static.BASE, {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
mode: 'no-cors',
|
|
14
|
+
})
|
|
15
|
+
return request !== null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async updatePassword(data) {
|
|
19
|
+
const request = await Static.apiServerRequest(Static.BASE + 'user/update_password', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(data),
|
|
23
|
+
})
|
|
24
|
+
let response = null
|
|
25
|
+
if (request?.status === 200) {
|
|
26
|
+
response = await request.json()
|
|
27
|
+
}
|
|
28
|
+
return response
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default new Auth()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
import LoginView from '@/views/auth/LoginView.vue'
|
|
4
|
+
import BaseLayout from '@/views/menu/BaseLayout.vue'
|
|
5
|
+
import HomeView from '@/views/home/HomeView.vue'
|
|
6
|
+
import E404 from '@/views/errors/404NotFound.vue'
|
|
7
|
+
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
10
|
+
routes: [
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
path: '/login',
|
|
14
|
+
name: 'Login',
|
|
15
|
+
component: LoginView,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
path: '/',
|
|
20
|
+
component: BaseLayout,
|
|
21
|
+
children: [
|
|
22
|
+
{
|
|
23
|
+
path: '',
|
|
24
|
+
name: 'Home',
|
|
25
|
+
component: HomeView,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: '/dashboard',
|
|
29
|
+
name: 'Dashboard',
|
|
30
|
+
component: () => import('@/views/dashboard/DashboardView.vue'),
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Módulo (submenu)
|
|
34
|
+
{
|
|
35
|
+
path: '/modulo/opcion-1',
|
|
36
|
+
name: 'ModuloOpcion1',
|
|
37
|
+
component: () => import('@/views/modulo/Opcion1View.vue'),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
path: '/modulo/opcion-2',
|
|
41
|
+
name: 'ModuloOpcion2',
|
|
42
|
+
component: () => import('@/views/modulo/Opcion2View.vue'),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
path: '/:pathMatch(.*)*',
|
|
49
|
+
name: 'NotFound',
|
|
50
|
+
component: E404,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export default router
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import Auth from '@/models/Auth'
|
|
4
|
+
|
|
5
|
+
export const useAuthStore = defineStore('auth', () => {
|
|
6
|
+
const user = ref(null)
|
|
7
|
+
const loading = ref(false)
|
|
8
|
+
const online = ref(null)
|
|
9
|
+
|
|
10
|
+
async function ping() {
|
|
11
|
+
loading.value = true
|
|
12
|
+
online.value = await Auth.ping()
|
|
13
|
+
loading.value = false
|
|
14
|
+
return online.value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function updatePassword(data) {
|
|
18
|
+
loading.value = true
|
|
19
|
+
const response = await Auth.updatePassword(data)
|
|
20
|
+
loading.value = false
|
|
21
|
+
return response
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { user, loading, online, ping, updatePassword }
|
|
25
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
.scroller {
|
|
4
|
+
@apply overflow-y-auto;
|
|
5
|
+
}
|
|
6
|
+
.scroller::-webkit-scrollbar {
|
|
7
|
+
width: 6px;
|
|
8
|
+
}
|
|
9
|
+
.scroller::-webkit-scrollbar-thumb {
|
|
10
|
+
background-color: #a5b4fc;
|
|
11
|
+
border-radius: 9999px;
|
|
12
|
+
}
|
|
13
|
+
.scroller::-webkit-scrollbar-track {
|
|
14
|
+
background-color: #f3f4f6;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.btnGreen {
|
|
19
|
+
@apply bg-emerald-600 hover:bg-emerald-500 text-white rounded-sm disabled:bg-emerald-300 disabled:text-gray-500 cursor-pointer disabled:cursor-not-allowed;
|
|
20
|
+
}
|
|
21
|
+
.btnOrange {
|
|
22
|
+
@apply bg-orange-500 hover:bg-orange-400 text-white rounded-sm disabled:bg-orange-200 disabled:text-gray-500 cursor-pointer disabled:cursor-not-allowed;
|
|
23
|
+
}
|
|
24
|
+
.btnRose {
|
|
25
|
+
@apply bg-rose-500 hover:bg-rose-400 text-white rounded-sm disabled:bg-rose-200 disabled:text-gray-500 cursor-pointer disabled:cursor-not-allowed;
|
|
26
|
+
}
|
|
27
|
+
.btnBlue {
|
|
28
|
+
@apply bg-sky-600 hover:bg-sky-500 text-white rounded-sm disabled:bg-sky-300 disabled:text-gray-500 cursor-pointer disabled:cursor-not-allowed;
|
|
29
|
+
}
|
|
30
|
+
.btnWhite {
|
|
31
|
+
@apply bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-sm disabled:bg-gray-300 disabled:text-gray-500 cursor-pointer disabled:cursor-not-allowed;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const BASE = 'https://www.google.com.mx/'
|
|
2
|
+
|
|
3
|
+
export async function apiServerRequest(url, options = {}) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await fetch(url, options)
|
|
6
|
+
return response
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error('Request error:', error)
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
name: 'LoginView',
|
|
4
|
+
data() {
|
|
5
|
+
return {
|
|
6
|
+
form: {
|
|
7
|
+
user: '',
|
|
8
|
+
pass: '',
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
methods: {
|
|
13
|
+
async handleLogin() {
|
|
14
|
+
this.$router.push('/')
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
|
22
|
+
<div class="w-full p-8 bg-white border border-gray-100 rounded-lg shadow-md max-w-md">
|
|
23
|
+
|
|
24
|
+
<h2 class="text-2xl font-bold text-center mb-6 text-gray-800">Iniciar sesión</h2>
|
|
25
|
+
|
|
26
|
+
<form @submit.prevent="handleLogin" class="space-y-3">
|
|
27
|
+
|
|
28
|
+
<div class="flex flex-col gap-1">
|
|
29
|
+
<label class="text-sm font-medium text-gray-700">Usuario</label>
|
|
30
|
+
<input
|
|
31
|
+
v-model="form.user"
|
|
32
|
+
type="text"
|
|
33
|
+
placeholder="Usuario"
|
|
34
|
+
required
|
|
35
|
+
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="flex flex-col gap-1">
|
|
40
|
+
<label class="text-sm font-medium text-gray-700">Contraseña</label>
|
|
41
|
+
<input
|
|
42
|
+
v-model="form.pass"
|
|
43
|
+
type="password"
|
|
44
|
+
placeholder="••••••••"
|
|
45
|
+
required
|
|
46
|
+
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<button
|
|
51
|
+
type="submit"
|
|
52
|
+
class="mt-2 w-full bg-indigo-500 text-white py-2 rounded-lg hover:bg-indigo-600 transition text-sm font-medium cursor-pointer"
|
|
53
|
+
>
|
|
54
|
+
Ingresar
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
name: 'NotFound',
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="min-h-screen flex flex-col items-center justify-center bg-gray-100 text-gray-500">
|
|
9
|
+
<i class="fa-solid fa-triangle-exclamation text-5xl mb-4 text-gray-300" />
|
|
10
|
+
<p class="text-6xl font-bold text-gray-300">404</p>
|
|
11
|
+
<p class="mt-2 text-sm">Página no encontrada</p>
|
|
12
|
+
<router-link to="/" class="mt-6 text-sm text-sky-600 hover:underline">
|
|
13
|
+
Volver al inicio
|
|
14
|
+
</router-link>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
name: 'BaseLayout',
|
|
4
|
+
data() {
|
|
5
|
+
return {
|
|
6
|
+
sidebarOpen: false,
|
|
7
|
+
collapsed: false,
|
|
8
|
+
openMenus: {},
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
computed: {
|
|
12
|
+
currentRoute() {
|
|
13
|
+
return this.$route.path
|
|
14
|
+
},
|
|
15
|
+
activeItem() {
|
|
16
|
+
const path = this.currentRoute
|
|
17
|
+
if (path === '/') return 'home'
|
|
18
|
+
if (path === '/dashboard') return 'dashboard'
|
|
19
|
+
if (path === '/modulo/opcion-1') return 'modulo-opcion-1'
|
|
20
|
+
if (path === '/modulo/opcion-2') return 'modulo-opcion-2'
|
|
21
|
+
return ''
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
watch: {
|
|
25
|
+
currentRoute: {
|
|
26
|
+
handler(path) {
|
|
27
|
+
if (path.startsWith('/modulo/')) {
|
|
28
|
+
this.openMenus = { ...this.openMenus, modulo: true }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
immediate: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
methods: {
|
|
35
|
+
toggleSidebar() {
|
|
36
|
+
if (window.innerWidth < 768) {
|
|
37
|
+
this.sidebarOpen = !this.sidebarOpen
|
|
38
|
+
} else {
|
|
39
|
+
this.collapsed = !this.collapsed
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
toggleMenu(menu) {
|
|
43
|
+
this.openMenus[menu] = !this.openMenus[menu]
|
|
44
|
+
},
|
|
45
|
+
isMenuOpen(menu) {
|
|
46
|
+
return this.openMenus[menu]
|
|
47
|
+
},
|
|
48
|
+
enter(el) {
|
|
49
|
+
el.style.height = '0'
|
|
50
|
+
el.style.opacity = '0'
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
el.style.height = el.scrollHeight + 'px'
|
|
53
|
+
el.style.opacity = '1'
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
leave(el) {
|
|
57
|
+
el.style.height = el.scrollHeight + 'px'
|
|
58
|
+
el.style.opacity = '1'
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
el.style.height = '0'
|
|
61
|
+
el.style.opacity = '0'
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div class="flex h-screen bg-white">
|
|
70
|
+
|
|
71
|
+
<!-- Sidebar -->
|
|
72
|
+
<aside :class="[
|
|
73
|
+
'fixed md:static z-40 top-0 left-0 h-full border-r border-gray-200 bg-white transition-all duration-300',
|
|
74
|
+
sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
|
75
|
+
collapsed ? 'w-20' : 'w-64',
|
|
76
|
+
]">
|
|
77
|
+
|
|
78
|
+
<!-- Logo -->
|
|
79
|
+
<div class="p-4 h-16 border-b border-gray-200 flex items-center justify-center">
|
|
80
|
+
<span v-if="!collapsed" class="text-xl font-bold text-sky-600">
|
|
81
|
+
{{PROJECT_NAME}}
|
|
82
|
+
</span>
|
|
83
|
+
<span v-else class="text-xl font-bold text-sky-600">
|
|
84
|
+
<i class="fa-solid fa-cube" />
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Menu -->
|
|
89
|
+
<nav class="p-4">
|
|
90
|
+
<ul class="space-y-1">
|
|
91
|
+
|
|
92
|
+
<!-- Home -->
|
|
93
|
+
<li>
|
|
94
|
+
<router-link to="/" :class="[
|
|
95
|
+
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition cursor-pointer',
|
|
96
|
+
activeItem === 'home'
|
|
97
|
+
? 'bg-sky-200 text-sky-700'
|
|
98
|
+
: 'hover:bg-sky-100 text-slate-500',
|
|
99
|
+
]">
|
|
100
|
+
<span class="text-xl"><i class="fa-solid fa-house" /></span>
|
|
101
|
+
<span v-if="!collapsed">Home</span>
|
|
102
|
+
</router-link>
|
|
103
|
+
</li>
|
|
104
|
+
|
|
105
|
+
<!-- Dashboard -->
|
|
106
|
+
<li>
|
|
107
|
+
<router-link to="/dashboard" :class="[
|
|
108
|
+
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition cursor-pointer',
|
|
109
|
+
activeItem === 'dashboard'
|
|
110
|
+
? 'bg-sky-200 text-sky-700'
|
|
111
|
+
: 'hover:bg-sky-100 text-slate-500',
|
|
112
|
+
]">
|
|
113
|
+
<span class="text-xl"><i class="fa-solid fa-chart-line" /></span>
|
|
114
|
+
<span v-if="!collapsed">Dashboard</span>
|
|
115
|
+
</router-link>
|
|
116
|
+
</li>
|
|
117
|
+
|
|
118
|
+
<!-- Módulo (submenu) -->
|
|
119
|
+
<li>
|
|
120
|
+
<button @click="toggleMenu('modulo')" :class="[
|
|
121
|
+
'w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition cursor-pointer',
|
|
122
|
+
activeItem.startsWith('modulo-')
|
|
123
|
+
? 'bg-sky-200 text-sky-700'
|
|
124
|
+
: 'hover:bg-sky-100 text-slate-500',
|
|
125
|
+
]">
|
|
126
|
+
<div class="flex items-center gap-3">
|
|
127
|
+
<span class="text-xl"><i class="fa-solid fa-layer-group" /></span>
|
|
128
|
+
<span v-if="!collapsed">Módulo</span>
|
|
129
|
+
</div>
|
|
130
|
+
<span
|
|
131
|
+
v-if="!collapsed"
|
|
132
|
+
:class="isMenuOpen('modulo') ? 'rotate-90' : ''"
|
|
133
|
+
class="transition-transform"
|
|
134
|
+
>
|
|
135
|
+
<i class="fa-solid fa-angle-right" />
|
|
136
|
+
</span>
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
<!-- Submenu -->
|
|
140
|
+
<transition @enter="enter" @leave="leave">
|
|
141
|
+
<ul v-if="isMenuOpen('modulo') && !collapsed" class="mt-1 space-y-1 pl-9">
|
|
142
|
+
<li>
|
|
143
|
+
<router-link to="/modulo/opcion-1" :class="[
|
|
144
|
+
'block p-2 rounded-lg text-sm transition cursor-pointer',
|
|
145
|
+
activeItem === 'modulo-opcion-1'
|
|
146
|
+
? 'bg-sky-100 text-sky-600'
|
|
147
|
+
: 'hover:bg-sky-50 text-slate-500',
|
|
148
|
+
]">
|
|
149
|
+
Opción 1
|
|
150
|
+
</router-link>
|
|
151
|
+
</li>
|
|
152
|
+
<li>
|
|
153
|
+
<router-link to="/modulo/opcion-2" :class="[
|
|
154
|
+
'block p-2 rounded-lg text-sm transition cursor-pointer',
|
|
155
|
+
activeItem === 'modulo-opcion-2'
|
|
156
|
+
? 'bg-sky-100 text-sky-600'
|
|
157
|
+
: 'hover:bg-sky-50 text-slate-500',
|
|
158
|
+
]">
|
|
159
|
+
Opción 2
|
|
160
|
+
</router-link>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
</transition>
|
|
164
|
+
</li>
|
|
165
|
+
|
|
166
|
+
</ul>
|
|
167
|
+
</nav>
|
|
168
|
+
|
|
169
|
+
</aside>
|
|
170
|
+
|
|
171
|
+
<!-- Overlay mobile -->
|
|
172
|
+
<div v-if="sidebarOpen" @click="sidebarOpen = false" class="fixed inset-0 bg-black/40 z-30 md:hidden" />
|
|
173
|
+
|
|
174
|
+
<!-- Main -->
|
|
175
|
+
<div class="flex-1 flex flex-col bg-gray-100 overflow-hidden">
|
|
176
|
+
|
|
177
|
+
<!-- Header -->
|
|
178
|
+
<header class="bg-white h-16 px-4 border-b border-gray-200 flex items-center justify-between shrink-0">
|
|
179
|
+
<button @click="toggleSidebar" class="text-gray-400 text-xl cursor-pointer">
|
|
180
|
+
<i class="fa-solid fa-bars" />
|
|
181
|
+
</button>
|
|
182
|
+
<div class="flex items-center gap-3">
|
|
183
|
+
<div class="flex flex-col leading-tight text-right">
|
|
184
|
+
<span class="hidden sm:block text-sm font-bold text-gray-800">Usuario</span>
|
|
185
|
+
<span class="hidden sm:block text-xs font-semibold text-gray-500">Administrador</span>
|
|
186
|
+
</div>
|
|
187
|
+
<img src="https://i.pravatar.cc/40" class="w-9 h-9 rounded-full object-cover" />
|
|
188
|
+
</div>
|
|
189
|
+
</header>
|
|
190
|
+
|
|
191
|
+
<!-- Content -->
|
|
192
|
+
<main class="p-4 scroller flex-1">
|
|
193
|
+
<router-view />
|
|
194
|
+
</main>
|
|
195
|
+
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<style scoped>
|
|
201
|
+
ul {
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
transition: height 0.3s ease, opacity 0.2s ease;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [vue()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': resolve(__dirname, './src'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|