.
This commit is contained in:
@@ -226,6 +226,164 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Графики и визуализации -->
|
||||
<div v-if="report.chartsData" class="space-y-4">
|
||||
<!-- Сезонность -->
|
||||
<div v-if="report.chartsData.seasonality && seasonalityChartData" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Сезонность спроса</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Chart type="line" :data="seasonalityChartData" :options="seasonalityChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Воронка конверсии -->
|
||||
<div v-if="report.chartsData.conversionFunnel && report.chartsData.conversionFunnel.length > 0 && conversionFunnelChartData" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Воронка конверсии</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container" style="height: 400px">
|
||||
<Chart type="bar" :data="conversionFunnelChartData" :options="conversionFunnelChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Потенциал каналов -->
|
||||
<div v-if="report.chartsData.channelsPotential && report.chartsData.channelsPotential.length > 0 && channelsPotentialChartData" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Потенциал каналов продвижения</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Chart type="bar" :data="channelsPotentialChartData" :options="channelsPotentialChartOptions" />
|
||||
</div>
|
||||
<div class="mt-4 space-y-2">
|
||||
<div v-for="channel in report.chartsData.channelsPotential" :key="channel.name" class="p-3 border-round bg-surface-50 dark:bg-surface-800">
|
||||
<div class="flex justify-content-between align-items-center mb-2">
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ channel.name }}</span>
|
||||
<Tag :value="`Потенциал: ${channel.potential}`" :severity="channel.potential >= 80 ? 'success' : channel.potential >= 60 ? 'warning' : 'info'" />
|
||||
</div>
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400">{{ channel.justification }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сегментация аудитории -->
|
||||
<div v-if="report.chartsData.audienceSegmentation && (ageGroupsChartData || gendersChartData)" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Сегментация целевой аудитории</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="grid">
|
||||
<div v-if="report.chartsData.audienceSegmentation.ageGroups && report.chartsData.audienceSegmentation.ageGroups.length > 0 && ageGroupsChartData" class="col-12 md:col-6 mb-4">
|
||||
<h4 class="text-lg font-semibold mb-3 text-surface-700 dark:text-surface-300">По возрасту</h4>
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Chart type="pie" :data="ageGroupsChartData" :options="pieChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="report.chartsData.audienceSegmentation.genders && report.chartsData.audienceSegmentation.genders.length > 0 && gendersChartData" class="col-12 md:col-6 mb-4">
|
||||
<h4 class="text-lg font-semibold mb-3 text-surface-700 dark:text-surface-300">По полу</h4>
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Chart type="pie" :data="gendersChartData" :options="pieChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Конкуренты -->
|
||||
<div v-if="report.chartsData.competitors && report.chartsData.competitors.length > 0" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Анализ конкурентов</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="grid">
|
||||
<div v-for="competitor in report.chartsData.competitors" :key="competitor.name" class="col-12 md:col-6 mb-4">
|
||||
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700">
|
||||
<h4 class="text-lg font-semibold mb-3 text-surface-900 dark:text-surface-0">{{ competitor.name }}</h4>
|
||||
<div class="space-y-2 mb-3">
|
||||
<div class="flex justify-content-between">
|
||||
<span class="text-surface-600 dark:text-surface-400">Охват:</span>
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ competitor.reach?.toLocaleString() || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-content-between">
|
||||
<span class="text-surface-600 dark:text-surface-400">Отзывы:</span>
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ competitor.reviews?.toLocaleString() || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-content-between">
|
||||
<span class="text-surface-600 dark:text-surface-400">Активность:</span>
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ competitor.activity }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="competitor.strengths && competitor.strengths.length > 0" class="mb-3">
|
||||
<p class="text-sm font-semibold mb-2 text-green-600 dark:text-green-400">Сильные стороны:</p>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(strength, idx) in competitor.strengths" :key="idx" class="text-sm text-surface-700 dark:text-surface-300 mb-1"><i class="pi pi-check-circle text-green-500 mr-2"></i>{{ strength }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="competitor.weaknesses && competitor.weaknesses.length > 0">
|
||||
<p class="text-sm font-semibold mb-2 text-red-600 dark:text-red-400">Слабые стороны:</p>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(weakness, idx) in competitor.weaknesses" :key="idx" class="text-sm text-surface-700 dark:text-surface-300 mb-1"><i class="pi pi-times-circle text-red-500 mr-2"></i>{{ weakness }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SWOT анализ -->
|
||||
<div v-if="report.chartsData.swot" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">SWOT-анализ</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6 mb-3">
|
||||
<div class="p-4 border-round bg-green-50 dark:bg-green-900 border-1 border-green-200 dark:border-green-700">
|
||||
<h4 class="text-lg font-semibold mb-3 text-green-700 dark:text-green-300">Сильные стороны</h4>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(strength, idx) in report.chartsData.swot.strengths" :key="idx" class="text-surface-700 dark:text-surface-300 mb-2"><i class="pi pi-check-circle text-green-500 mr-2"></i>{{ strength }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-3">
|
||||
<div class="p-4 border-round bg-red-50 dark:bg-red-900 border-1 border-red-200 dark:border-red-700">
|
||||
<h4 class="text-lg font-semibold mb-3 text-red-700 dark:text-red-300">Слабые стороны</h4>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(weakness, idx) in report.chartsData.swot.weaknesses" :key="idx" class="text-surface-700 dark:text-surface-300 mb-2"><i class="pi pi-times-circle text-red-500 mr-2"></i>{{ weakness }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-3">
|
||||
<div class="p-4 border-round bg-blue-50 dark:bg-blue-900 border-1 border-blue-200 dark:border-blue-700">
|
||||
<h4 class="text-lg font-semibold mb-3 text-blue-700 dark:text-blue-300">Возможности</h4>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(opportunity, idx) in report.chartsData.swot.opportunities" :key="idx" class="text-surface-700 dark:text-surface-300 mb-2">
|
||||
<i class="pi pi-arrow-up-circle text-blue-500 mr-2"></i>{{ opportunity }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-3">
|
||||
<div class="p-4 border-round bg-orange-50 dark:bg-orange-900 border-1 border-orange-200 dark:border-orange-700">
|
||||
<h4 class="text-lg font-semibold mb-3 text-orange-700 dark:text-orange-300">Угрозы</h4>
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(threat, idx) in report.chartsData.swot.threats" :key="idx" class="text-surface-700 dark:text-surface-300 mb-2"><i class="pi pi-exclamation-triangle text-orange-500 mr-2"></i>{{ threat }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Стратегия -->
|
||||
<div v-if="report.strategy" class="card">
|
||||
<div class="card-header mb-4">
|
||||
@@ -275,6 +433,7 @@
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import { marked } from 'marked';
|
||||
import Button from 'primevue/button';
|
||||
import Chart from 'primevue/chart';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
@@ -684,6 +843,217 @@ const renderedSummary = computed(() => {
|
||||
return renderMarkdown(report.value.summary);
|
||||
});
|
||||
|
||||
// Chart data computed properties
|
||||
const seasonalityChartData = computed(() => {
|
||||
if (!report.value?.chartsData?.seasonality) return null;
|
||||
const seasonality = report.value.chartsData.seasonality;
|
||||
const months = Object.keys(seasonality);
|
||||
const values = Object.values(seasonality);
|
||||
|
||||
return {
|
||||
labels: months,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Спрос (%)',
|
||||
data: values,
|
||||
borderColor: '#3B82F6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const seasonalityChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const conversionFunnelChartData = computed(() => {
|
||||
if (!report.value?.chartsData?.conversionFunnel || report.value.chartsData.conversionFunnel.length === 0) return null;
|
||||
const funnel = report.value.chartsData.conversionFunnel;
|
||||
|
||||
return {
|
||||
labels: funnel.map((item) => item.stage),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Значение',
|
||||
data: funnel.map((item) => item.value),
|
||||
backgroundColor: '#3B82F6',
|
||||
borderColor: '#2563EB',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Конверсия (%)',
|
||||
data: funnel.map((item) => item.conversion),
|
||||
backgroundColor: '#10B981',
|
||||
borderColor: '#059669',
|
||||
borderWidth: 1,
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const conversionFunnelChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Значение'
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Конверсия (%)'
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
},
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const channelsPotentialChartData = computed(() => {
|
||||
if (!report.value?.chartsData?.channelsPotential || report.value.chartsData.channelsPotential.length === 0) return null;
|
||||
const channels = report.value.chartsData.channelsPotential;
|
||||
|
||||
return {
|
||||
labels: channels.map((ch) => ch.name),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Потенциал',
|
||||
data: channels.map((ch) => ch.potential),
|
||||
backgroundColor: channels.map((ch) => {
|
||||
if (ch.potential >= 80) return '#10B981';
|
||||
if (ch.potential >= 60) return '#F59E0B';
|
||||
return '#3B82F6';
|
||||
}),
|
||||
borderColor: channels.map((ch) => {
|
||||
if (ch.potential >= 80) return '#059669';
|
||||
if (ch.potential >= 60) return '#D97706';
|
||||
return '#2563EB';
|
||||
}),
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const channelsPotentialChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ageGroupsChartData = computed(() => {
|
||||
if (!report.value?.chartsData?.audienceSegmentation?.ageGroups || report.value.chartsData.audienceSegmentation.ageGroups.length === 0) return null;
|
||||
const ageGroups = report.value.chartsData.audienceSegmentation.ageGroups;
|
||||
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'];
|
||||
|
||||
return {
|
||||
labels: ageGroups.map((ag) => ag.label),
|
||||
datasets: [
|
||||
{
|
||||
data: ageGroups.map((ag) => parseFloat(ag.value)),
|
||||
backgroundColor: ageGroups.map((_, idx) => colors[idx % colors.length]),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const gendersChartData = computed(() => {
|
||||
if (!report.value?.chartsData?.audienceSegmentation?.genders || report.value.chartsData.audienceSegmentation.genders.length === 0) return null;
|
||||
const genders = report.value.chartsData.audienceSegmentation.genders;
|
||||
|
||||
const colors = ['#EC4899', '#3B82F6'];
|
||||
|
||||
return {
|
||||
labels: genders.map((g) => g.label),
|
||||
datasets: [
|
||||
{
|
||||
data: genders.map((g) => parseFloat(g.value)),
|
||||
backgroundColor: genders.map((_, idx) => colors[idx % colors.length]),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const pieChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
return context.label + ': ' + context.parsed + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get analysis type label
|
||||
const getAnalysisTypeLabel = (type) => {
|
||||
const labels = {
|
||||
@@ -953,4 +1323,8 @@ onBeforeUnmount(() => {
|
||||
.markdown-content :deep(tr:hover) {
|
||||
background-color: var(--surface-50);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user