Lógica Principal
Este código apresenta uma abordagem para criar animações de carrregamento e barras de progresso em Vue usando generators, permitindo transição entre estados, atualização em tempo real e limpeza de recursos. Ideal para processamento increemntal em aplicações web.
/**
* Função utilitária para envolver generators com inicialização automática.
* @param {Function} fn - Função geradora.
* @returns {Function} - Função que retorna a instância do generator após a primeira chamada.
*/
const envolverGerador = fn => function(...argumentos) {
const instancia = fn.apply(this, argumentos);
instancia.next();
return instancia;
};
/**
* Estado reativo para gerenciar múltiplas instâncias de loading e progresso.
*/
const estadoCarregamento = shallowReactive({ contagem: 0, texto: '' }),
listaProgresso = shallowReactive([]);
/**
* Cria e retorna uma instância de barra de progresso controlada por generator.
* @param {Object} config - Configuração da barra de progresso.
* @param {string} config.texto - Texto descritivo.
* @param {number} [config.total=100] - Valor total do progresso.
* @param {number} [config.zIndex=7] - Prioridade de camada.
* @param {string} [config.teleport='body'] - Elemento destino para teletransporte.
* @returns {Generator} - Instância do generator para controle externo.
*/
const criarProgresso = envolverGerador(function*({ texto, total = 100, zIndex = 7, teleport = 'body' }) {
const item = shallowReactive({
id: Date.now(),
teleport,
zIndex,
total,
texto,
concluido: 0,
percentual: 0,
});
listaProgresso.push(item);
while (true) {
const atualizacao = yield;
if (!atualizacao) break;
Object.assign(item, atualizacao);
if (atualizacao.concluido !== undefined || atualizacao.total) {
item.percentual = parseInt(item.total === 100 ? item.concluido : (item.concluido / item.total) * 100);
}
}
const indice = listaProgresso.indexOf(item);
if (indice !== -1) listaProgresso.splice(indice, 1);
});
/**
* Função pública para iniciar animações de carregamento ou barras de progresso.
* Suporta transição de loading para progresso via chamadas subsequentes.
* @param {string} [texto=''] - Texto inicial para o loading.
* @param {Object} [configuracao=null] - Configuração opcional para barra de progresso.
* @returns {Generator} - Instância do generator para atualização e destruição.
*/
const iniciarCarregamento = envolverGerador(function*(texto = '', configuracao = null) {
let progresso;
if (configuracao) {
configuracao.texto = texto;
progresso = criarProgresso(configuracao);
} else {
estadoCarregamento.contagem++;
estadoCarregamento.texto = texto || true;
}
while (true) {
const dados = yield;
if (!dados) break;
if (progresso) {
progresso.next(dados);
} else if (dados.total) {
progresso = criarProgresso(dados);
estadoCarregamento.contagem--;
} else {
estadoCarregamento.texto = dados.texto || true;
}
}
if (progresso) {
progresso.next();
} else {
estadoCarregamento.contagem--;
}
});
// Expor a função para uso externo em componentes Vue.
const setup = (_, { expose }) => {
expose({ iniciarCarregamento });
};
// Ou dentro de <script setup>
// defineExpose({ iniciarCarregamento });
Visualização do Componente
<div id="aplicativo" v-carregamento="estadoCarregamento.contagem > 0 ? estadoCarregamento.texto : false"></div>
<template v-for="progresso in listaProgresso" :key="progresso.id">
<teleport :to="progresso.teleport">
<div class="barra-progresso">
<span v-if="progresso.texto" class="rotulo">{{ progresso.texto }}</span>
<div v-if="progresso.total === progresso.concluido" class="indicador concluido">Completo</div>
<div v-else class="indicador" :style="{ background: `linear-gradient(90deg, #0a7, var(--cor-tema) ${progresso.percentual}%, rgba(100,100,100,0.6) ${progresso.percentual + 5}%, rgba(100,100,100,0.8))` }">
{{ progresso.total === 100 ? `${progresso.concluido}%` : `${progresso.concluido}/${progresso.total}` }}
</div>
</div>
</teleport>
</template>
Estilos CSS
/* Estilos base para o container e loading */
#env-app, #aplicativo {
height: 100%;
overflow: auto;
position: relative;
}
#env-app[v-cloak] {
--conteudo: 'Iniciando...';
font-size: 50px;
color: #aaa;
}
#env-app[v-cloak] > * {
display: none;
}
.v-carregamento:not(#aplicativo) {
pointer-events: none;
overflow: hidden;
}
.mascara-carregamento {
--conteudo: '';
position: absolute;
inset: 0;
background-color: rgba(255, 255, 255, 0.5);
}
#aplicativo.v-carregamento > .mascara-carregamento {
position: fixed;
font-size: 30px;
z-index: 7;
color: #666;
}
#env-app[v-cloak], .mascara-carregamento {
display: flex;
align-items: center;
justify-content: center;
}
:where(.mascara-carregamento, #env-app[v-cloak])::before {
content: '';
width: 1.5em;
height: 1.5em;
margin: 0.5em;
background: url('/caminho/para/animacao.svg') no-repeat center/100%;
animation: girar 0.4s linear infinite;
}
:where(.mascara-carregamento, #env-app[v-cloak])::after {
content: var(--conteudo);
}
/* Estilos para a barra de progresso */
.barra-progresso {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 1.3% 3px;
color: #fff;
background: rgba(99, 99, 99, 0.6);
}
.barra-progresso .rotulo {
margin: 0 5px 5px;
}
.barra-progresso .indicador {
padding: 5px 0;
border-radius: 5px;
text-align: center;
}
.barra-progresso .indicador.concluido {
background: #0a7;
}
/* Barra de progresso fixa no centro da tela */
body > .barra-progresso {
border-radius: 10px;
width: 300px;
height: 90px;
position: fixed;
z-index: 7;
box-shadow: 1px 1px 5px #666;
border: 1px solid #666;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* Barra de progresso local sobre elementos específicos */
#env-app .barra-progresso {
position: absolute;
inset: 0;
}
Directive Personalizada para Carregamento
/**
* Directive Vue para controlar animações de carregamento em elementos.
*/
const diretrizCarregamento = {
mounted(elemento, ligacao) {
const estiloComputado = window.getComputedStyle(elemento);
estilosComputados.set(elemento, estiloComputado);
if (ligacao.value) adicionarMascara(elemento, ligacao, estiloComputado);
},
updated(elemento, ligacao) {
if (ligacao.value === ligacao.oldValue) return;
if (ligacao.value) {
if (ligacao.oldValue) {
const mascaraExistente = encontrarFilhoPorClasse(elemento, 'mascara-carregamento');
if (mascaraExistente) {
if (typeof ligacao.value === 'string') {
mascaraExistente.style.setProperty('--conteudo', `'${ligacao.value}'`);
} else if (typeof ligacao.oldValue === 'string') {
mascaraExistente.style.removeProperty('--conteudo');
}
return;
}
}
adicionarMascara(elemento, ligacao, estilosComputados.get(elemento));
} else {
elemento.classList.remove('v-carregamento');
if (elemento.dataset.posicaoOriginal !== undefined) {
if (!elemento.dataset.posicaoOriginal) {
elemento.style.removeProperty('position');
} else {
elemento.style.setProperty('position', elemento.dataset.posicaoOriginal);
}
delete elemento.dataset.posicaoOriginal;
}
encontrarFilhoPorClasse(elemento, 'mascara-carregamento')?.remove();
}
},
unmounted(elemento) {
estilosComputados.delete(elemento);
}
};
// Funções auxiliares para a directive
const estilosComputados = new Map();
function encontrarFilhoPorClasse(pai, classe) {
return pai.querySelector(`.${classe}`);
}
function adicionarMascara(elemento, ligacao, estilo) {
elemento.classList.add('v-carregamento');
const posicaoOriginal = estilo.position;
elemento.dataset.posicaoOriginal = posicaoOriginal || '';
if (!posicaoOriginal || posicaoOriginal === 'static') {
elemento.style.position = 'relative';
}
const mascara = document.createElement('div');
mascara.className = 'mascara-carregamento';
if (typeof ligacao.value === 'string') {
mascara.style.setProperty('--conteudo', `'${ligacao.value}'`);
}
elemento.appendChild(mascara);
}