Barra de Progresso em Vue com Implementação via Generator

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);
}

Tags: Vue.js JavaScript Generators Barra de Progresso Diretivas Vue Vite

Publicado em 6-20 20:47