Improve realtime feed
This commit is contained in:
@@ -147,7 +147,7 @@ source.onmessage = (e) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
La interfaz incluye una vista **Feed** accesible desde la barra lateral. Allí se muestran en tiempo real las tarjetas de cada módulo conforme llegan estos eventos.
|
La interfaz incluye una vista **Feed** accesible desde la barra lateral. Allí se muestran en tiempo real las tarjetas de cada módulo conforme llegan estos eventos. Las actualizaciones exhiben la tarjeta anterior y la nueva con una flecha que indica el cambio.
|
||||||
|
|
||||||
#### Card Animation
|
#### Card Animation
|
||||||
The data cards implemented in `ui/src/components/ui/NucleoDataCard.vue` now feature a subtle growing animation when hovered over. This animation is implemented purely with CSS using keyframes and transitions defined within the component's `<style scoped>` section, ensuring the styles are encapsulated and don't affect other elements.
|
The data cards implemented in `ui/src/components/ui/NucleoDataCard.vue` now feature a subtle growing animation when hovered over. This animation is implemented purely with CSS using keyframes and transitions defined within the component's `<style scoped>` section, ensuring the styles are encapsulated and don't affect other elements.
|
||||||
|
|||||||
@@ -105,5 +105,5 @@ The specific prop name matches the module (e.g., `asistencia` for `cardAsistenci
|
|||||||
|
|
||||||
## Realtime Feed
|
## Realtime Feed
|
||||||
|
|
||||||
The UI provides a *Feed* view that consumes the realtime events generated by the backend. You can access it via the sidebar link "Feed". Each incoming event is rendered using these standardized cards so you can monitor inserts, updates and deletes as they happen.
|
The UI provides a *Feed* view that consumes the realtime events generated by the backend. You can access it via the sidebar link "Feed". Each incoming event is rendered using these standardized cards so you can monitor inserts, updates and deletes as they happen. Update events show the old card followed by the new one with an arrow in between.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,28 @@
|
|||||||
<p class="text-xs text-gray-500 mb-1">
|
<p class="text-xs text-gray-500 mb-1">
|
||||||
{{ event.operation }} · {{ event.table }} • {{ formatTimestamp(event.receivedAt) }}
|
{{ event.operation }} · {{ event.table }} • {{ formatTimestamp(event.receivedAt) }}
|
||||||
</p>
|
</p>
|
||||||
<component v-if="component" :is="component" v-bind="componentProps" />
|
<template v-if="event.operation === 'UPDATE'">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
v-if="component"
|
||||||
|
:is="component"
|
||||||
|
v-bind="componentPropsOld"
|
||||||
|
class="old-card opacity-70"
|
||||||
|
/>
|
||||||
|
<span class="text-2xl">→</span>
|
||||||
|
<component
|
||||||
|
v-if="component"
|
||||||
|
:is="component"
|
||||||
|
v-bind="componentPropsNew"
|
||||||
|
class="new-card"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<component
|
||||||
|
v-else-if="component"
|
||||||
|
:is="component"
|
||||||
|
v-bind="componentProps"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -31,22 +52,28 @@ const itemData = computed(() =>
|
|||||||
props.event.operation === 'DELETE' ? props.event.old : props.event.new
|
props.event.operation === 'DELETE' ? props.event.old : props.event.new
|
||||||
)
|
)
|
||||||
|
|
||||||
const componentProps = computed(() => {
|
const buildProps = (data) => {
|
||||||
const data = itemData.value || {}
|
|
||||||
switch (props.event.table) {
|
switch (props.event.table) {
|
||||||
case 'Planilla':
|
case 'Planilla':
|
||||||
return { planilla: data }
|
return { planilla: data || {} }
|
||||||
case 'Cliente':
|
case 'Cliente':
|
||||||
return { employee: data }
|
return { employee: data || {} }
|
||||||
case 'TareaRealizada':
|
case 'TareaRealizada':
|
||||||
return { tarea: data }
|
return { tarea: data || {} }
|
||||||
case 'Asistencia':
|
case 'Asistencia':
|
||||||
return { asistencia: data }
|
return { asistencia: data || {} }
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentProps = computed(() => {
|
||||||
|
return buildProps(itemData.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const componentPropsOld = computed(() => buildProps(props.event.old))
|
||||||
|
const componentPropsNew = computed(() => buildProps(props.event.new))
|
||||||
|
|
||||||
const formatTimestamp = (ts) => {
|
const formatTimestamp = (ts) => {
|
||||||
if (!ts) return ''
|
if (!ts) return ''
|
||||||
return new Date(ts).toLocaleString('es-HN', { hour12: true })
|
return new Date(ts).toLocaleString('es-HN', { hour12: true })
|
||||||
@@ -57,4 +84,9 @@ const formatTimestamp = (ts) => {
|
|||||||
.realtime-event {
|
.realtime-event {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.old-card,
|
||||||
|
.new-card {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ onMounted(() => {
|
|||||||
<h1>Feed en Tiempo Real</h1>
|
<h1>Feed en Tiempo Real</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="feed-list overflow-auto pr-2" style="max-height: calc(100vh - 160px);">
|
<transition-group name="feed" tag="div" class="feed-list overflow-auto pr-2" style="max-height: calc(100vh - 160px);">
|
||||||
<RealtimeEventCard
|
<RealtimeEventCard
|
||||||
v-for="(ev, idx) in realtime.events"
|
v-for="(ev, idx) in realtime.events"
|
||||||
:key="idx"
|
:key="ev.receivedAt + idx"
|
||||||
:event="ev"
|
:event="ev"
|
||||||
/>
|
/>
|
||||||
<div v-if="realtime.events.length === 0" class="text-center text-gray-500 mt-4">
|
<div v-if="realtime.events.length === 0" class="text-center text-gray-500 mt-4">
|
||||||
No hay eventos aún.
|
No hay eventos aún.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,4 +50,17 @@ onMounted(() => {
|
|||||||
font-size: 2.2em;
|
font-size: 2.2em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-enter-active,
|
||||||
|
.feed-leave-active {
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.feed-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
.feed-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user