Toast system_alert ganha botão de ação com deeplink
Novo <Toast group="system-alerts"> no AppLayout com template custom (vive no bloco global — persiste em qualquer layout/rota). Renderiza: - Ícone de alerta + título em bold - Detail em texto menor com opacity - Botão com deeplink quando payload.deeplink existe, severity danger Label do botão inferido do deeplink: - /configuracoes/creditos-whatsapp → "Ir pra loja" - /configuracoes/whatsapp-pessoal → "Ver conexão" - /configuracoes/whatsapp-oficial → "Ver canal oficial" - outros → "Abrir" (ou payload.actionLabel se vier explícito) Clique navega via router.push se é path interno, senão window.location.href. Toast continua sticky (24h) + closable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,15 @@ export function useNotifications() {
|
||||
let ownerId = null;
|
||||
let pollTimer = null;
|
||||
|
||||
// Label curto do botão do toast baseado no deeplink
|
||||
function defaultActionLabel(deeplink) {
|
||||
if (!deeplink) return null;
|
||||
if (deeplink.includes('creditos-whatsapp')) return 'Ir pra loja';
|
||||
if (deeplink.includes('whatsapp-pessoal')) return 'Ver conexão';
|
||||
if (deeplink.includes('whatsapp-oficial')) return 'Ver canal oficial';
|
||||
return 'Abrir';
|
||||
}
|
||||
|
||||
function showToastFor(item) {
|
||||
if (item?.type !== 'system_alert') return;
|
||||
if (alertedIds.has(item.id)) return;
|
||||
@@ -43,7 +52,12 @@ export function useNotifications() {
|
||||
summary: payload.title || 'Alerta do sistema',
|
||||
detail: payload.detail || '',
|
||||
life: STICKY_TOAST_LIFE_MS,
|
||||
closable: true
|
||||
closable: true,
|
||||
group: 'system-alerts',
|
||||
data: {
|
||||
deeplink: payload.deeplink,
|
||||
actionLabel: payload.actionLabel || defaultActionLabel(payload.deeplink)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,18 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToDeeplink(link) {
|
||||
if (!link) return;
|
||||
if (typeof link === 'string' && link.startsWith('/')) {
|
||||
router.push(link);
|
||||
} else {
|
||||
window.location.href = link;
|
||||
}
|
||||
}
|
||||
|
||||
import AppFooter from './AppFooter.vue';
|
||||
import AppSidebar from './AppSidebar.vue';
|
||||
@@ -175,6 +186,32 @@ onBeforeUnmount(() => {
|
||||
<GlobalNoticeBanner />
|
||||
<ConversationDrawer />
|
||||
<GlobalInboundNotifier />
|
||||
|
||||
<!-- Toast especial pra alertas de sistema (heartbeat, saldo baixo, etc) —
|
||||
vermelho sticky com botão de ação opcional via payload.deeplink -->
|
||||
<Toast group="system-alerts" position="top-right" :pt="{ root: { style: 'width: 28rem; max-width: 92vw' } }">
|
||||
<template #message="slotProps">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-exclamation-circle text-lg mt-0.5" style="color: var(--p-red-500)" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-bold text-sm">{{ slotProps.message.summary }}</div>
|
||||
<div class="text-xs mt-0.5 leading-relaxed opacity-85">{{ slotProps.message.detail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="slotProps.message.data?.deeplink"
|
||||
:label="slotProps.message.data?.actionLabel || 'Abrir'"
|
||||
icon="pi pi-arrow-right"
|
||||
iconPos="right"
|
||||
size="small"
|
||||
severity="danger"
|
||||
class="rounded-full self-end"
|
||||
@click="goToDeeplink(slotProps.message.data.deeplink)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Toast>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user