Files
radiusNucleo/frontend/src/components/RawDbViewer.vue
josedario87 96a8f95f9e
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 58s
Refactor: Migrar UI completa a Tailwind CSS v4 + shadcn-vue
- Reemplazar CSS nativo con Tailwind CSS v4 y utilidades custom
- Crear librería de componentes UI basada en shadcn-vue (Radix Vue)
- Componentes UI: Button, Card, Input, Textarea, Badge, Dialog, Avatar, DropdownMenu
- Migrar todos los componentes existentes a Tailwind utilities
- Convertir EventCard.js (htm) a EventCard.vue (SFC)
- Implementar sistema de temas dark/light con clase .dark
- Mantener efectos glassmorphism via @utility custom
- Eliminar styles.css legacy
2025-11-24 18:12:24 -06:00

197 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { onMounted, ref, computed } from 'vue';
import { Button, Badge, Input, Card } from '@/components/ui';
import { cn } from '@/lib/utils';
const emit = defineEmits(['toggle-fullscreen']);
const tables = ref([]);
const active = ref('');
const columns = ref([]);
const rows = ref([]);
const loading = ref(false);
const total = ref(0);
const limit = ref(100);
const offset = ref(0);
const q = ref('');
const sortBy = ref('');
const sortDir = ref('asc');
function fmt(v) {
if (v === null || v === undefined) return '';
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
}
async function loadTables() {
const res = await fetch('/api/db/tables');
const data = await res.json();
tables.value = data.items || [];
if (tables.value.length && !active.value) {
await select(tables.value[0]);
}
}
async function select(t) {
active.value = t;
offset.value = 0;
await reload();
}
async function reload() {
if (!active.value) return;
loading.value = true;
try {
const params = new URLSearchParams({ limit: String(limit.value), offset: String(offset.value) });
const res = await fetch(`/api/db/table/${encodeURIComponent(active.value)}?` + params.toString());
const data = await res.json();
columns.value = data.columns || [];
rows.value = data.rows || [];
total.value = data.total || 0;
} finally {
loading.value = false;
}
}
function next() { if (offset.value + limit.value < total.value) { offset.value += limit.value; reload(); } }
function prev() { if (offset.value - limit.value >= 0) { offset.value -= limit.value; reload(); } else { offset.value = 0; reload(); } }
const filteredRows = computed(() => {
if (!q.value) return rows.value;
const t = q.value.toLowerCase();
return rows.value.filter(r => JSON.stringify(r).toLowerCase().includes(t));
});
const sortedRows = computed(() => {
if (!sortBy.value) return filteredRows.value;
const dir = sortDir.value === 'asc' ? 1 : -1;
const key = sortBy.value;
const arr = filteredRows.value.slice();
arr.sort((a, b) => {
const va = a[key];
const vb = b[key];
const na = typeof va === 'number' || (/^-?\d+(\.\d+)?$/.test(String(va)) ? Number(va) : NaN);
const nb = typeof vb === 'number' || (/^-?\d+(\.\d+)?$/.test(String(vb)) ? Number(vb) : NaN);
if (!Number.isNaN(na) && !Number.isNaN(nb)) return (na - nb) * dir;
const sa = String(va ?? '').toLowerCase();
const sb = String(vb ?? '').toLowerCase();
return sa.localeCompare(sb) * dir;
});
return arr;
});
onMounted(() => { loadTables(); });
function toggleSort(c) {
if (sortBy.value !== c) { sortBy.value = c; sortDir.value = 'asc'; return; }
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
}
function toCsv(cols, rowsData) {
const esc = v => {
const s = v === null || v === undefined ? '' : (typeof v === 'object' ? JSON.stringify(v) : String(v));
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = [cols.join(',')];
for (const r of rowsData) lines.push(cols.map(c => esc(r[c])).join(','));
return lines.join('\n');
}
function exportCsv() {
const csv = toCsv(columns.value, sortedRows.value);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const ts = new Date().toISOString().replace(/[:T]/g, '-').split('.')[0];
a.href = url;
a.download = `${active.value || 'table'}-${ts}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>
<template>
<div>
<!-- Table selector -->
<div class="flex flex-wrap gap-1.5 mb-3">
<Button
v-for="t in tables"
:key="t"
:variant="t === active ? 'default' : 'ghost'"
size="sm"
:class="t === active && 'ring-2 ring-pink-400/60'"
@click="select(t)"
>
{{ t }}
</Button>
</div>
<!-- Controls -->
<div class="flex flex-wrap items-center gap-2 mb-2">
<Input
v-model="q"
placeholder="Buscar en página (texto)"
class="flex-1 min-w-[240px]"
/>
<label class="flex items-center gap-2 glass glass-border rounded-md px-3 py-2">
<span class="text-sm">Tamaño de página</span>
<select
v-model.number="limit"
@change="reload()"
class="bg-transparent border-none text-inherit text-sm cursor-pointer"
>
<option :value="50">50</option>
<option :value="100">100</option>
<option :value="250">250</option>
<option :value="500">500</option>
</select>
</label>
<Badge>{{ offset + 1 }}{{ Math.min(offset + limit, total) }} / {{ total }}</Badge>
<div class="flex gap-1.5">
<Button size="sm" :disabled="offset <= 0" @click="prev">Anterior</Button>
<Button size="sm" :disabled="offset + limit >= total" @click="next">Siguiente</Button>
</div>
<Button size="sm" @click="exportCsv">Exportar CSV</Button>
<Button size="sm" variant="ghost" @click="emit('toggle-fullscreen')">Fullscreen</Button>
</div>
<!-- Table -->
<Card variant="panel" class="max-h-[60vh] border-pink-200 dark:border-pink-600/50">
<div class="overflow-auto p-3 scroll-custom">
<div v-if="loading" class="text-muted">Cargando</div>
<div v-else>
<table v-if="columns.length" class="w-full border-collapse">
<thead>
<tr>
<th
v-for="c in columns"
:key="c"
@click="toggleSort(c)"
class="select-none cursor-pointer text-left p-1.5 border-b border-white/10 text-sm font-medium hover:bg-white/5"
>
{{ c }}
<span v-if="sortBy === c" class="ml-1">{{ sortDir === 'asc' ? '' : '' }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in sortedRows" :key="idx" class="hover:bg-white/5">
<td
v-for="c in columns"
:key="c"
class="p-1.5 border-b border-white/5 text-xs"
>
<pre class="m-0 whitespace-pre-wrap font-mono">{{ fmt(row[c]) }}</pre>
</td>
</tr>
</tbody>
</table>
<div v-else class="text-muted">Sin columnas</div>
</div>
</div>
</Card>
</div>
</template>