This commit is contained in:
root
2025-12-08 19:10:10 +05:00
parent 043b9575bc
commit d866fbe690
@@ -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>