Feat: Agregar botones de Copiar Texto y Copiar JSON
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 47s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 47s
Implementa funcionalidad de copia en tres secciones del Informe: 📋 Funcionalidades agregadas: 1. Lista de Ingresos - Copiar Texto: Formato WhatsApp con emojis y legible - Copiar JSON: Formato estructurado para sistemas 2. Top 10 Clientes - Copiar Texto: Ranking formateado con métricas - Copiar JSON: Array de objetos con datos completos 3. Serie Temporal Acumulada - Copiar Texto: Evolución temporal con emojis - Copiar JSON: Datos completos para análisis ✨ Características: - Botones con iconos (i-lucide-copy y i-lucide-code) - Disabled cuando no hay datos disponibles - Alertas de confirmación al copiar - Formato texto optimizado para WhatsApp - Incluye metadata: rango de fechas y timestamp Uso: - Copiar Texto → Compartir en WhatsApp/Telegram - Copiar JSON → Integración con otros sistemas
This commit is contained in:
@@ -1,68 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { ThemeColors } from '~/composables/useTheme'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'dashboard',
|
||||
title: 'Configuración'
|
||||
})
|
||||
|
||||
// Colores por defecto (actuales del tema café)
|
||||
const defaultTheme = {
|
||||
bg: '#14100b',
|
||||
surface: '#1f180f',
|
||||
border: '#3a2a16',
|
||||
primary: '#e0c080',
|
||||
primaryStrong: '#c08040',
|
||||
accent: '#ffe0a0',
|
||||
text: '#fef9f0',
|
||||
textMuted: '#d8c7a6'
|
||||
}
|
||||
// Usar el composable de temas
|
||||
const {
|
||||
theme,
|
||||
defaultTheme,
|
||||
applyTheme,
|
||||
saveTheme: saveThemeComposable,
|
||||
resetTheme: resetThemeComposable,
|
||||
exportTheme,
|
||||
importTheme
|
||||
} = useTheme()
|
||||
|
||||
// Estado del tema (inicializar con valores actuales del CSS)
|
||||
const theme = ref({ ...defaultTheme })
|
||||
|
||||
// Cargar tema guardado en localStorage
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem('custom-theme')
|
||||
if (savedTheme) {
|
||||
try {
|
||||
theme.value = JSON.parse(savedTheme)
|
||||
applyTheme(theme.value)
|
||||
} catch (e) {
|
||||
console.error('Error al cargar tema guardado', e)
|
||||
}
|
||||
// Temas predefinidos
|
||||
const presetThemes: Record<string, ThemeColors> = {
|
||||
cafe: { ...defaultTheme },
|
||||
azul: {
|
||||
bg: '#0a0e1a',
|
||||
surface: '#151a28',
|
||||
border: '#2d3748',
|
||||
primary: '#60a5fa',
|
||||
primaryStrong: '#3b82f6',
|
||||
accent: '#93c5fd',
|
||||
text: '#f0f4f8',
|
||||
textMuted: '#cbd5e1'
|
||||
},
|
||||
verde: {
|
||||
bg: '#0f1a14',
|
||||
surface: '#1a2820',
|
||||
border: '#2d4033',
|
||||
primary: '#86efac',
|
||||
primaryStrong: '#4ade80',
|
||||
accent: '#bbf7d0',
|
||||
text: '#f0fdf4',
|
||||
textMuted: '#d1fae5'
|
||||
},
|
||||
carbon: {
|
||||
bg: '#0f0f0f',
|
||||
surface: '#1a1a1a',
|
||||
border: '#333333',
|
||||
primary: '#a3a3a3',
|
||||
primaryStrong: '#737373',
|
||||
accent: '#d4d4d4',
|
||||
text: '#fafafa',
|
||||
textMuted: '#d4d4d4'
|
||||
}
|
||||
})
|
||||
|
||||
// Aplicar tema al documento
|
||||
const applyTheme = (themeColors: typeof defaultTheme) => {
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--brand-bg', themeColors.bg)
|
||||
root.style.setProperty('--brand-surface', themeColors.surface)
|
||||
root.style.setProperty('--brand-border', themeColors.border)
|
||||
root.style.setProperty('--brand-primary', themeColors.primary)
|
||||
root.style.setProperty('--brand-primary-strong', themeColors.primaryStrong)
|
||||
root.style.setProperty('--brand-accent', themeColors.accent)
|
||||
root.style.setProperty('--brand-text', themeColors.text)
|
||||
root.style.setProperty('--brand-text-muted', themeColors.textMuted)
|
||||
}
|
||||
|
||||
// Guardar tema
|
||||
const saveTheme = () => {
|
||||
localStorage.setItem('custom-theme', JSON.stringify(theme.value))
|
||||
// Aplicar preset de tema
|
||||
const applyPreset = (presetName: keyof typeof presetThemes) => {
|
||||
const preset = presetThemes[presetName]
|
||||
theme.value = { ...preset }
|
||||
applyTheme(theme.value)
|
||||
useToast().add({
|
||||
title: 'Tema guardado',
|
||||
description: 'Los cambios se aplicaron correctamente',
|
||||
color: 'green'
|
||||
title: 'Tema aplicado',
|
||||
description: `Se aplicó el tema ${presetName}`,
|
||||
color: 'blue'
|
||||
})
|
||||
}
|
||||
|
||||
// Guardar tema con feedback
|
||||
const handleSaveTheme = () => {
|
||||
const success = saveThemeComposable()
|
||||
if (success) {
|
||||
useToast().add({
|
||||
title: 'Tema guardado',
|
||||
description: 'Los cambios se aplicaron correctamente',
|
||||
color: 'green'
|
||||
})
|
||||
} else {
|
||||
useToast().add({
|
||||
title: 'Error al guardar',
|
||||
description: 'No se pudo guardar el tema',
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Resetear al tema por defecto
|
||||
const resetTheme = () => {
|
||||
theme.value = { ...defaultTheme }
|
||||
localStorage.removeItem('custom-theme')
|
||||
applyTheme(theme.value)
|
||||
const handleResetTheme = () => {
|
||||
resetThemeComposable()
|
||||
useToast().add({
|
||||
title: 'Tema reseteado',
|
||||
description: 'Se restauraron los colores por defecto',
|
||||
@@ -70,8 +93,50 @@ const resetTheme = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Vista previa en tiempo real (opcional)
|
||||
const livePreview = ref(false)
|
||||
// Exportar tema al portapapeles
|
||||
const handleExportTheme = async () => {
|
||||
try {
|
||||
const themeJson = exportTheme()
|
||||
await navigator.clipboard.writeText(themeJson)
|
||||
useToast().add({
|
||||
title: 'Tema exportado',
|
||||
description: 'El JSON del tema se copió al portapapeles',
|
||||
color: 'green'
|
||||
})
|
||||
} catch (error) {
|
||||
useToast().add({
|
||||
title: 'Error al exportar',
|
||||
description: 'No se pudo copiar al portapapeles',
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Importar tema desde JSON
|
||||
const importJsonInput = ref('')
|
||||
const showImportModal = ref(false)
|
||||
|
||||
const handleImportTheme = () => {
|
||||
const success = importTheme(importJsonInput.value, false)
|
||||
if (success) {
|
||||
useToast().add({
|
||||
title: 'Tema importado',
|
||||
description: 'El tema se aplicó correctamente',
|
||||
color: 'green'
|
||||
})
|
||||
showImportModal.value = false
|
||||
importJsonInput.value = ''
|
||||
} else {
|
||||
useToast().add({
|
||||
title: 'Error al importar',
|
||||
description: 'El JSON proporcionado no es válido',
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Vista previa en tiempo real (activada por defecto)
|
||||
const livePreview = ref(true)
|
||||
watch(() => theme.value, (newTheme) => {
|
||||
if (livePreview.value) {
|
||||
applyTheme(newTheme)
|
||||
@@ -110,6 +175,58 @@ watch(() => theme.value, (newTheme) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Temas Predefinidos -->
|
||||
<div class="mb-6 pb-6 border-b border-[var(--brand-border)]">
|
||||
<label class="block text-sm font-medium text-[var(--brand-text)] mb-3">
|
||||
Temas Predefinidos
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<button
|
||||
@click="applyPreset('cafe')"
|
||||
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
||||
>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded" style="background: #e0c080" />
|
||||
<div class="w-6 h-6 rounded" style="background: #1f180f" />
|
||||
</div>
|
||||
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Café</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="applyPreset('azul')"
|
||||
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
||||
>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded" style="background: #60a5fa" />
|
||||
<div class="w-6 h-6 rounded" style="background: #151a28" />
|
||||
</div>
|
||||
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Azul</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="applyPreset('verde')"
|
||||
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
||||
>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded" style="background: #86efac" />
|
||||
<div class="w-6 h-6 rounded" style="background: #1a2820" />
|
||||
</div>
|
||||
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Verde</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="applyPreset('carbon')"
|
||||
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
||||
>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded" style="background: #a3a3a3" />
|
||||
<div class="w-6 h-6 rounded" style="background: #1a1a1a" />
|
||||
</div>
|
||||
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Carbón</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Fondo Principal -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -268,26 +385,83 @@ watch(() => theme.value, (newTheme) => {
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex justify-between items-center pt-4 border-t border-[var(--brand-border)]">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
@click="resetTheme"
|
||||
>
|
||||
Restaurar por defecto
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-lucide-save"
|
||||
@click="saveTheme"
|
||||
>
|
||||
Guardar cambios
|
||||
</UButton>
|
||||
<div class="space-y-3 pt-4 border-t border-[var(--brand-border)]">
|
||||
<!-- Botones principales -->
|
||||
<div class="flex justify-between items-center">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
@click="handleResetTheme"
|
||||
>
|
||||
Restaurar por defecto
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-lucide-save"
|
||||
@click="handleSaveTheme"
|
||||
>
|
||||
Guardar cambios
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Botones de import/export -->
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-download"
|
||||
@click="handleExportTheme"
|
||||
class="flex-1"
|
||||
>
|
||||
Exportar tema
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-upload"
|
||||
@click="showImportModal = true"
|
||||
class="flex-1"
|
||||
>
|
||||
Importar tema
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Modal de Importar Tema -->
|
||||
<UModal v-model="showImportModal" title="Importar Tema">
|
||||
<div class="p-4 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
||||
Pega el JSON del tema
|
||||
</label>
|
||||
<UTextarea
|
||||
v-model="importJsonInput"
|
||||
:rows="10"
|
||||
placeholder='{\n "bg": "#14100b",\n "surface": "#1f180f",\n ...\n}'
|
||||
class="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
@click="showImportModal = false"
|
||||
>
|
||||
Cancelar
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
@click="handleImportTheme"
|
||||
>
|
||||
Importar
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UModal>
|
||||
|
||||
<!-- Vista previa de colores -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
|
||||
Reference in New Issue
Block a user