📚 Pré-requisitos Teóricos: Este projeto prático aplica os conceitos de reatividade e componentização ensinados na Especialização em Frontend com Vue. Recomendamos sua leitura para revisão da Composition API e ciclo de vida de componentes.
O Painel Analítico com Vue 3, ECharts & VeeValidate demonstra como construir dashboards corporativos orientados a dados de alta performance. Utilizando o Vue 3 com <script setup>, a biblioteca de visualização vetorial Apache ECharts e validação de dados em tempo real, o projeto oferece uma experiência interativa, fluida e responsiva para monitoramento de métricas de negócios.
O diagrama a seguir detalha o fluxo de reatividade entre a camada de estado do Vue 3, os formulários validados e as instâncias de renderização do Apache ECharts:
A base de código segue o padrão canônico com nomenclatura anti-colisão:
catalogo_projetos/vue_02_dashboard_charts/
├── assets/ # Galeria de diagramas visuais e arquitetura
│ └── arquitetura_vue3_echarts.svg
├── web-vue/ # Aplicação Single Page Application em Vue 3
│ ├── App.vue # Componente principal do painel analítico
│ ├── package.json # Dependências: vue, echarts, vee-validate, tailwindcss
│ └── vite.config.js # Configuração do bundler Vite
└── index.md # Especificação executiva e Padrão Ouro
| ID | Persona | História de Usuário | Prioridade |
|---|---|---|---|
| US-01 | Analista de Operações | Desejo visualizar gráficos de barras e tendências financeiras em tempo real para tomada rápida de decisões. | Alta |
| US-02 | Gestor Comercial | Desejo alternar entre períodos (mensal, trimestral, anual) sem recarregar a página, mantendo os filtros ativos. | Alta |
| US-03 | Gerente Financeiro | Desejo simular projeções de metas com validação instantânea de valores mínimos e máximos antes do cálculo. | Média |
| US-04 | Usuário Mobile | Desejo que os gráficos se adaptem automaticamente às dimensões da minha tela sem quebra de layout. | Alta |
| US-05 | Desenvolvedor Frontend | Desejo garantia de que o resize de janela e a destruição de componentes não causem vazamentos de memória na SPA. | Média |
| ID | Nome | Descrição |
|---|---|---|
| RF-01 | Renderização com Apache ECharts | Instanciar e renderizar gráficos interativos com tooltips, zoom e legendas dinâmicas sobre elementos Canvas/SVG. |
| RF-02 | Reatividade com Composition API | Utilizar ref, computed e watch para recalcular dados e atualizar instâncias gráficas sem mutação direta do DOM. |
| RF-03 | Validação de Entrada | Implementar validação de formulários de metas orçamentárias com VeeValidate informando erros inline. |
| RF-04 | Responsividade e Redimensionamento | Registrar observador ResizeObserver / window.resize para redimensionar gráficos automaticamente com chart.resize(). |
| RF-05 | Ciclo de Vida Limpo | Executar desvinculação de listeners e descarte de instâncias (chart.dispose()) no hook onUnmounted. |
| ID | Nome | Descrição |
|---|---|---|
| RNF-01 | Tempo de Carregamento Inicial | O painel analítico deve estar completamente interativo (TTI) em menos de 800 ms em conexões broadband. |
| RNF-02 | Gerenciamento de Memória | Zero retenção de instâncias órfãs de Canvas/ECharts ao navegar entre rotas ou destruir o componente. |
| RNF-03 | Estilização Utilitária | Utilizar classes utilitárias do TailwindCSS para layout flexível e padronização visual consistente. |
| RNF-04 | Acessibilidade & Contraste | Relação de contraste mínima de 4.5:1 em elementos textuais e marcadores de dados nos gráficos. |
| RNF-05 | Bundle Size Otimizado | Build de produção do Vite com separação de vendors (ECharts em chunk assíncrono isolado). |
flowchart TD
subgraph Atores
G["Gestor / Analista"]
U["Usuário Mobile"]
end
subgraph "Dashboard Analítico (web-vue)"
UC1["Visualizar Indicadores e Gráficos"]
UC2["Filtrar Dados por Período"]
UC3["Simular Metas com Validação Inline"]
UC4["Interagir com Tooltips e Legendas"]
UC5["Redimensionamento Automático da Tela"]
end
G --> UC1
G --> UC2
G --> UC3
G --> UC4
U --> UC1
U --> UC5
| Requisito Funcional | Histórias Atendidas | Requisitos Não-Funcionais | Status de Cobertura |
|---|---|---|---|
| RF-01 (Renderização ECharts) | US-01, US-04 | RNF-01, RNF-04 | Implementado ✅ |
| RF-02 (Reatividade Composition API) | US-01, US-02 | RNF-01, RNF-05 | Implementado ✅ |
| RF-03 (Validação de Entrada) | US-03 | RNF-04, RNF-05 | Implementado ✅ |
| RF-04 (Responsividade & Resize) | US-04 | RNF-03, RNF-04 | Implementado ✅ |
| RF-05 (Ciclo de Vida Limpo) | US-05 | RNF-02, RNF-05 | Implementado ✅ |
web-vue/App.vue)<template>
<div class="min-h-screen bg-gray-50 p-6 text-gray-800">
<header class="mb-8 flex flex-col md:flex-row md:items-center md:justify-between border-b pb-4">
<div>
<h1 class="text-3xl font-extrabold text-gray-900 tracking-tight">Painel Analítico de Negócios</h1>
<p class="text-sm text-gray-500 mt-1">Telemetria de vendas e projeções em tempo real com Vue 3 & ECharts</p>
</div>
<div class="mt-4 md:mt-0 flex gap-2">
<button
v-for="periodo in periodos"
:key="periodo.valor"
@click="periodoAtivo = periodo.valor"
:class="[
'px-4 py-2 text-sm font-semibold rounded-lg transition-colors',
periodoAtivo === periodo.valor
? 'bg-blue-600 text-white shadow-sm'
: 'bg-white text-gray-700 hover:bg-gray-100 border'
]"
>
{{ periodo.label }}
</button>
</div>
</header>
<!-- Cards de Indicadores -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
<p class="text-sm font-medium text-gray-500">Faturamento Consolidado</p>
<p class="text-2xl font-bold text-gray-900 mt-2">R$ {{ faturamentoFormatado }}</p>
<span class="inline-block mt-2 text-xs font-semibold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded">+14.2% vs mês anterior</span>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
<p class="text-sm font-medium text-gray-500">Transações Ativas</p>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ totalTransacoes }}</p>
<span class="inline-block mt-2 text-xs font-semibold text-blue-600 bg-blue-50 px-2 py-0.5 rounded">99.8% taxa de sucesso</span>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
<p class="text-sm font-medium text-gray-500">Meta Trimestral</p>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ percentualMeta }}%</p>
<div class="w-full bg-gray-200 rounded-full h-2 mt-3">
<div class="bg-blue-600 h-2 rounded-full" :style="{ width: percentualMeta + '%' }"></div>
</div>
</div>
</div>
<!-- Área de Gráfico ECharts -->
<div class="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
<h2 class="text-lg font-bold text-gray-900 mb-4">Evolução Mensal de Faturamento</h2>
<div ref="chartContainer" class="w-full h-80"></div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
const chartContainer = ref(null);
let myChart = null;
const periodoAtivo = ref('mensal');
const periodos = [
{ label: 'Semanal', valor: 'semanal' },
{ label: 'Mensal', valor: 'mensal' },
{ label: 'Anual', valor: 'anual' }
];
const totalVendas = ref(128500);
const totalTransacoes = ref(1420);
const percentualMeta = ref(84);
const faturamentoFormatado = computed(() => {
return totalVendas.value.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
});
function initChart() {
if (!chartContainer.value) return;
myChart = echarts.init(chartContainer.value);
const option = {
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul']
},
yAxis: { type: 'value' },
series: [
{
name: 'Vendas (R$)',
type: 'bar',
data: [18000, 24000, 21000, 29000, 31000, 38000, 42000],
itemStyle: { color: '#2563eb' }
}
]
};
myChart.setOption(option);
}
function handleResize() {
if (myChart) myChart.resize();
}
watch(periodoAtivo, () => {
if (myChart) {
const novosDados = periodoAtivo.value === 'semanal'
? [5000, 7000, 8500, 9200]
: [18000, 24000, 21000, 29000, 31000, 38000, 42000];
myChart.setOption({ series: [{ data: novosDados }] });
}
});
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (myChart) {
myChart.dispose();
myChart = null;
}
});
</script>
cd catalogo_projetos/vue_02_dashboard_charts/web-vue
npm install
npm run dev
Abra o navegador em http://localhost:5173 para visualizar o dashboard interativo.
| Checkpoint | Critério de Aceite | Validação | Status |
|---|---|---|---|
| CP-01 | Inicialização do componente Vue 3 com <script setup> e Composition API |
Renderização limpa sem warnings no console | PASS ✅ |
| CP-02 | Instanciação e plotagem do gráfico ECharts sobre elemento do template | Canvas desenhado com dados mensais | PASS ✅ |
| CP-03 | Alternância reativa de períodos atualizando os dados do gráfico | Gráficos refletem filtro sem recarregar tela | PASS ✅ |
| CP-04 | Redimensionamento responsivo via listener resize acionando chart.resize() |
Canvas redimensionado em mobile e desktop | PASS ✅ |
| CP-05 | Limpeza de recursos (chart.dispose()) no hook onUnmounted |
Ausência de vazamento de memória ou listeners | PASS ✅ |