.
This commit is contained in:
@@ -448,6 +448,171 @@
|
|||||||
<HeatmapTable :matrix="segment.matrix" :title="segment.title || 'Матрица каналов по сегментам'" />
|
<HeatmapTable :matrix="segment.matrix" :title="segment.title || 'Матрица каналов по сегментам'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Competitive Environment Component -->
|
||||||
|
<div v-else-if="segment.type === 'competitiveEnvironment'">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-2xl font-semibold text-surface-900 dark:text-surface-0 mb-4">
|
||||||
|
<i class="pi pi-chart-line mr-2 text-primary-500"></i>{{ segment.title || 'Конкурентная среда' }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Metrics KPI Cards -->
|
||||||
|
<div v-if="segment.metrics" class="grid mb-6">
|
||||||
|
<div class="col-12 md:col-3 mb-3">
|
||||||
|
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 h-full">
|
||||||
|
<div class="flex align-items-center mb-2">
|
||||||
|
<i class="pi pi-sitemap text-primary-500 mr-2 text-xl"></i>
|
||||||
|
<span class="text-sm font-medium text-surface-600 dark:text-surface-400">Доля лидера</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ segment.metrics.leader_market_share || '—' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="segment.metrics.market_leader_name" class="text-xs text-surface-500 dark:text-surface-400 mt-1">
|
||||||
|
{{ segment.metrics.market_leader_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-3 mb-3">
|
||||||
|
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 h-full">
|
||||||
|
<div class="flex align-items-center mb-2">
|
||||||
|
<i class="pi pi-chart-bar text-green-500 mr-2 text-xl"></i>
|
||||||
|
<span class="text-sm font-medium text-surface-600 dark:text-surface-400">Уровень конкуренции</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ segment.metrics.competition_level || '—' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-3 mb-3">
|
||||||
|
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 h-full">
|
||||||
|
<div class="flex align-items-center mb-2">
|
||||||
|
<i class="pi pi-wallet text-orange-500 mr-2 text-xl"></i>
|
||||||
|
<span class="text-sm font-medium text-surface-600 dark:text-surface-400">Давление рекламы</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ segment.metrics.ad_pressure || '—' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visibility Chart -->
|
||||||
|
<div v-if="segment.visibilityChart" class="mb-6">
|
||||||
|
<div class="flex align-items-center mb-3">
|
||||||
|
<i class="pi pi-chart-bar text-primary-500 mr-2"></i>
|
||||||
|
<h4 class="text-lg font-semibold text-surface-700 dark:text-surface-300 m-0">{{ segment.visibilityChart.title }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700">
|
||||||
|
<div class="chart-container" :style="segment.visibilityChart.chartHeight">
|
||||||
|
<Chart
|
||||||
|
v-if="hasRenderableChartData(segment.visibilityChart.data)"
|
||||||
|
:type="segment.visibilityChart.chartType"
|
||||||
|
:data="segment.visibilityChart.data"
|
||||||
|
:options="segment.visibilityChart.options"
|
||||||
|
@chart:click="onChartClick"
|
||||||
|
@chart:hover="onChartHover"
|
||||||
|
ref="visibilityChartRef"
|
||||||
|
/>
|
||||||
|
<div v-else class="p-3 border-round text-center text-surface-500 dark:text-surface-400">
|
||||||
|
Нет данных для отображения
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Competitors Table -->
|
||||||
|
<div v-if="segment.competitors && segment.competitors.length > 0" class="mb-6">
|
||||||
|
<div class="flex align-items-center mb-3">
|
||||||
|
<i class="pi pi-table text-primary-500 mr-2"></i>
|
||||||
|
<h4 class="text-lg font-semibold text-surface-700 dark:text-surface-300 m-0">Таблица конкурентов</h4>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto border-round" style="border: 1px solid var(--surface-border)">
|
||||||
|
<table class="competitive-environment-table w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="competitive-env-th">Название</th>
|
||||||
|
<th class="competitive-env-th">Сайт</th>
|
||||||
|
<th class="competitive-env-th text-center">Видимость</th>
|
||||||
|
<th class="competitive-env-th">Ключевые преимущества</th>
|
||||||
|
<th class="competitive-env-th text-center">Ценовой сегмент</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(competitor, idx) in segment.competitors"
|
||||||
|
:key="competitor.name || idx"
|
||||||
|
:class="['competitive-env-tr', { 'competitive-env-tr-highlighted': highlightedRowIndex === idx }]"
|
||||||
|
@mouseenter="highlightRow(idx)"
|
||||||
|
@mouseleave="highlightRow(null)"
|
||||||
|
@click="handleRowClick(competitor)"
|
||||||
|
>
|
||||||
|
<td class="competitive-env-td font-semibold">
|
||||||
|
<i class="pi pi-building text-primary-500 mr-2"></i>{{ competitor.name }}
|
||||||
|
</td>
|
||||||
|
<td class="competitive-env-td">
|
||||||
|
<a
|
||||||
|
v-if="competitor.website"
|
||||||
|
:href="competitor.website"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-primary-500 hover:text-primary-700 dark:hover:text-primary-400 flex align-items-center"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<i class="pi pi-external-link mr-1"></i>
|
||||||
|
<span class="text-sm">{{ formatWebsite(competitor.website) }}</span>
|
||||||
|
</a>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="competitive-env-td text-center">
|
||||||
|
<div class="flex align-items-center justify-content-center">
|
||||||
|
<span
|
||||||
|
class="competitive-env-score-badge"
|
||||||
|
:class="getScoreClass(competitor.visibility_score)"
|
||||||
|
>
|
||||||
|
{{ competitor.visibility_score || 0 }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="competitive-env-td">
|
||||||
|
<div v-if="competitor.key_strengths && competitor.key_strengths.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<Tag
|
||||||
|
v-for="(strength, sIdx) in competitor.key_strengths"
|
||||||
|
:key="sIdx"
|
||||||
|
:value="strength"
|
||||||
|
severity="success"
|
||||||
|
class="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="competitive-env-td text-center">
|
||||||
|
<Tag
|
||||||
|
v-if="competitor.price_segment"
|
||||||
|
:value="competitor.price_segment"
|
||||||
|
:severity="getPriceSegmentSeverity(competitor.price_segment)"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conclusion -->
|
||||||
|
<div v-if="segment.conclusion" class="mt-6">
|
||||||
|
<div class="p-4 border-round bg-primary-50 dark:bg-primary-900 border-1 border-primary-200 dark:border-primary-700">
|
||||||
|
<div class="flex align-items-start">
|
||||||
|
<i class="pi pi-info-circle text-primary-500 mr-2 mt-1 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold text-primary-700 dark:text-primary-300 mb-2">Заключение</h4>
|
||||||
|
<p class="text-surface-700 dark:text-surface-300 m-0">{{ segment.conclusion }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="segment.type === 'competitors'">
|
<div v-else-if="segment.type === 'competitors'">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0"><i class="pi pi-chart-line mr-2"></i>Анализ конкурентов</h3>
|
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0"><i class="pi pi-chart-line mr-2"></i>Анализ конкурентов</h3>
|
||||||
@@ -1958,6 +2123,104 @@ const buildCompetitorsBarChartData = (competitors, metricType = 'reach') => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build competitors visibility score chart data (horizontal bar chart)
|
||||||
|
const buildCompetitorsVisibilityChartData = (competitors) => {
|
||||||
|
if (!Array.isArray(competitors) || competitors.length === 0) return null;
|
||||||
|
|
||||||
|
// Sort by visibility_score descending for better visualization
|
||||||
|
const sortedCompetitors = [...competitors].sort((a, b) => (b.visibility_score || 0) - (a.visibility_score || 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: sortedCompetitors.map((comp) => comp.name),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Видимость (score)',
|
||||||
|
data: sortedCompetitors.map((comp) => comp.visibility_score || 0),
|
||||||
|
backgroundColor: sortedCompetitors.map((comp) => {
|
||||||
|
const score = comp.visibility_score || 0;
|
||||||
|
// Color coding: high (green), medium (yellow), low (red)
|
||||||
|
if (score >= 70) return '#10B981'; // green-500
|
||||||
|
if (score >= 40) return '#F59E0B'; // amber-500
|
||||||
|
return '#EF4444'; // red-500
|
||||||
|
}),
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process competitive environment data into segment format
|
||||||
|
const processCompetitiveEnvironmentData = (competitiveEnv) => {
|
||||||
|
if (!competitiveEnv || typeof competitiveEnv !== 'object') return null;
|
||||||
|
|
||||||
|
const competitors = competitiveEnv.competitors || [];
|
||||||
|
if (!Array.isArray(competitors) || competitors.length === 0) return null;
|
||||||
|
|
||||||
|
// Build visibility chart data
|
||||||
|
const visibilityChartData = buildCompetitorsVisibilityChartData(competitors);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'competitiveEnvironment',
|
||||||
|
title: competitiveEnv.title || 'Конкурентная среда',
|
||||||
|
metrics: competitiveEnv.metrics || {},
|
||||||
|
competitors: competitors,
|
||||||
|
visibilityChart: visibilityChartData
|
||||||
|
? {
|
||||||
|
chartType: 'bar',
|
||||||
|
data: visibilityChartData,
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: { padding: { right: 40 } },
|
||||||
|
animation: {
|
||||||
|
duration: 500,
|
||||||
|
onComplete: (animation) => drawBarValues(animation.chart)
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `Видимость: ${context.parsed.x}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
},
|
||||||
|
font: { size: 12 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: 'rgba(0,0,0,0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
font: { size: 12, weight: '500' },
|
||||||
|
crossAlign: 'far'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: 'Видимость конкурентов',
|
||||||
|
chartHeight: 'height: 400px'
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
conclusion: competitiveEnv.conclusion || ''
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Build competitors heatmap data
|
// Build competitors heatmap data
|
||||||
const buildCompetitorsHeatmapData = (competitors) => {
|
const buildCompetitorsHeatmapData = (competitors) => {
|
||||||
if (!Array.isArray(competitors) || competitors.length === 0) return null;
|
if (!Array.isArray(competitors) || competitors.length === 0) return null;
|
||||||
@@ -2303,7 +2566,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
|||||||
title: 'Распределение по возрасту',
|
title: 'Распределение по возрасту',
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: chartData,
|
data: chartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
chartHeight: 'height: 350px'
|
chartHeight: 'height: 350px'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2389,7 +2652,23 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
|||||||
gendersChartData
|
gendersChartData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'competitiveenvironment':
|
||||||
|
case 'competitive_environment': {
|
||||||
|
// New structure: { title, metrics, competitors, conclusion }
|
||||||
|
const processed = processCompetitiveEnvironmentData(data);
|
||||||
|
if (processed) return processed;
|
||||||
|
// Fallback to old structure if processing fails
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case 'competitors': {
|
case 'competitors': {
|
||||||
|
// Check if it's new structure (has metrics, competitors array with visibility_score)
|
||||||
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||||
|
if (data.metrics && Array.isArray(data.competitors)) {
|
||||||
|
const processed = processCompetitiveEnvironmentData(data);
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Old structure: array of competitors
|
||||||
if (!Array.isArray(data) || data.length === 0) return null;
|
if (!Array.isArray(data) || data.length === 0) return null;
|
||||||
const reachChartData = buildCompetitorsBarChartData(data, 'reach');
|
const reachChartData = buildCompetitorsBarChartData(data, 'reach');
|
||||||
const followersChartData = buildCompetitorsBarChartData(data, 'followers');
|
const followersChartData = buildCompetitorsBarChartData(data, 'followers');
|
||||||
@@ -2402,7 +2681,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: reachChartData,
|
data: reachChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Охват конкурентов'
|
title: 'Охват конкурентов'
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2410,7 +2689,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: followersChartData,
|
data: followersChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Подписчики конкурентов'
|
title: 'Подписчики конкурентов'
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -2467,9 +2746,28 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
case 'CHART_COMPETITORS':
|
case 'CHART_COMPETITORS':
|
||||||
|
case 'CHART_COMPETITIVE_ENVIRONMENT': {
|
||||||
|
// Check for new competitive environment structure first
|
||||||
|
if (chartsData.competitiveEnvironment || chartsData.competitive_environment) {
|
||||||
|
const competitiveEnv = chartsData.competitiveEnvironment || chartsData.competitive_environment;
|
||||||
|
const processed = processCompetitiveEnvironmentData(competitiveEnv);
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
// Fallback to old competitors structure
|
||||||
const competitorsArray = Array.isArray(chartsData.competitors) ? chartsData.competitors : chartsData.competitors?.competitors || [];
|
const competitorsArray = Array.isArray(chartsData.competitors) ? chartsData.competitors : chartsData.competitors?.competitors || [];
|
||||||
|
|
||||||
if (competitorsArray.length > 0) {
|
if (competitorsArray.length > 0) {
|
||||||
|
// Check if it's new structure (has visibility_score)
|
||||||
|
if (competitorsArray[0]?.visibility_score !== undefined) {
|
||||||
|
const processed = processCompetitiveEnvironmentData({
|
||||||
|
competitors: competitorsArray,
|
||||||
|
metrics: chartsData.metrics || {},
|
||||||
|
title: chartsData.title || 'Конкурентная среда',
|
||||||
|
conclusion: chartsData.conclusion || ''
|
||||||
|
});
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
// Old structure
|
||||||
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
||||||
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
||||||
|
|
||||||
@@ -2481,7 +2779,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: reachChartData,
|
data: reachChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Охват конкурентов'
|
title: 'Охват конкурентов'
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2489,7 +2787,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: followersChartData,
|
data: followersChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Подписчики конкурентов'
|
title: 'Подписчики конкурентов'
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -2521,6 +2819,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
|||||||
return segment;
|
return segment;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
case 'CHART_SWOT':
|
case 'CHART_SWOT':
|
||||||
// Check for swotCompetitors first
|
// Check for swotCompetitors first
|
||||||
@@ -2602,7 +2901,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
|||||||
title: 'Распределение по возрасту',
|
title: 'Распределение по возрасту',
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: chartData,
|
data: chartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
chartHeight: 'height: 350px'
|
chartHeight: 'height: 350px'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2634,9 +2933,29 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case 'competitors':
|
case 'competitiveenvironment':
|
||||||
|
case 'competitive_environment': {
|
||||||
|
const competitiveEnv = chartsData.competitiveEnvironment || chartsData.competitive_environment;
|
||||||
|
if (competitiveEnv) {
|
||||||
|
const processed = processCompetitiveEnvironmentData(competitiveEnv);
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case 'competitors': {
|
||||||
const competitorsArray = Array.isArray(chartsData.competitors) ? chartsData.competitors : chartsData.competitors?.competitors || [];
|
const competitorsArray = Array.isArray(chartsData.competitors) ? chartsData.competitors : chartsData.competitors?.competitors || [];
|
||||||
if (competitorsArray.length > 0) {
|
if (competitorsArray.length > 0) {
|
||||||
|
// Check if it's new structure (has visibility_score)
|
||||||
|
if (competitorsArray[0]?.visibility_score !== undefined) {
|
||||||
|
const processed = processCompetitiveEnvironmentData({
|
||||||
|
competitors: competitorsArray,
|
||||||
|
metrics: chartsData.metrics || {},
|
||||||
|
title: chartsData.title || 'Конкурентная среда',
|
||||||
|
conclusion: chartsData.conclusion || ''
|
||||||
|
});
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
// Old structure
|
||||||
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
||||||
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
||||||
|
|
||||||
@@ -2648,7 +2967,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: reachChartData,
|
data: reachChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Охват конкурентов'
|
title: 'Охват конкурентов'
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2656,7 +2975,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: followersChartData,
|
data: followersChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Подписчики конкурентов'
|
title: 'Подписчики конкурентов'
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -2688,6 +3007,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
|||||||
return segment;
|
return segment;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
case 'swot':
|
case 'swot':
|
||||||
// Check for swotCompetitors first
|
// Check for swotCompetitors first
|
||||||
@@ -2836,9 +3156,27 @@ const tryParseJsonSegment = (text) => {
|
|||||||
try {
|
try {
|
||||||
const parsedJson = JSON.parse(trimmed);
|
const parsedJson = JSON.parse(trimmed);
|
||||||
|
|
||||||
|
// Check if it's new competitive environment structure
|
||||||
|
if (parsedJson && typeof parsedJson === 'object' && !Array.isArray(parsedJson)) {
|
||||||
|
if (parsedJson.competitors && Array.isArray(parsedJson.competitors) && parsedJson.metrics) {
|
||||||
|
const processed = processCompetitiveEnvironmentData(parsedJson);
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it's an array of competitors
|
// Check if it's an array of competitors
|
||||||
if (Array.isArray(parsedJson) && parsedJson.length > 0) {
|
if (Array.isArray(parsedJson) && parsedJson.length > 0) {
|
||||||
// Check if it looks like competitors data
|
// Check if it's new structure (has visibility_score)
|
||||||
|
if (parsedJson[0]?.visibility_score !== undefined) {
|
||||||
|
const processed = processCompetitiveEnvironmentData({
|
||||||
|
competitors: parsedJson,
|
||||||
|
metrics: {},
|
||||||
|
title: 'Конкурентная среда',
|
||||||
|
conclusion: ''
|
||||||
|
});
|
||||||
|
if (processed) return processed;
|
||||||
|
}
|
||||||
|
// Check if it looks like old competitors data
|
||||||
if (parsedJson[0]?.name && (parsedJson[0]?.channelsHeatmap || parsedJson[0]?.reach || parsedJson[0]?.followers)) {
|
if (parsedJson[0]?.name && (parsedJson[0]?.channelsHeatmap || parsedJson[0]?.reach || parsedJson[0]?.followers)) {
|
||||||
return {
|
return {
|
||||||
type: 'competitors',
|
type: 'competitors',
|
||||||
@@ -2855,7 +3193,7 @@ const tryParseJsonSegment = (text) => {
|
|||||||
const chartType = parsedJson.chartType || 'bar';
|
const chartType = parsedJson.chartType || 'bar';
|
||||||
|
|
||||||
// Use appropriate options based on chart type
|
// Use appropriate options based on chart type
|
||||||
const options = chartType === 'pie' || chartType === 'doughnut' ? pieChartOptions : chartType === 'line' ? seasonalityChartOptions : competitorsBarChartOptions;
|
const options = chartType === 'pie' || chartType === 'doughnut' ? pieChartOptions : chartType === 'line' ? seasonalityChartOptions : horizontalBarChartOptions;
|
||||||
|
|
||||||
// Normalize chart data (colors/borders, safety)
|
// Normalize chart data (colors/borders, safety)
|
||||||
const chartData = normalizeChartJsData(parsedJson, chartType);
|
const chartData = normalizeChartJsData(parsedJson, chartType);
|
||||||
@@ -3001,7 +3339,7 @@ const chartsDataSegments = computed(() => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: reachChartData,
|
data: reachChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Охват конкурентов'
|
title: 'Охват конкурентов'
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -3009,7 +3347,7 @@ const chartsDataSegments = computed(() => {
|
|||||||
? {
|
? {
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: followersChartData,
|
data: followersChartData,
|
||||||
options: competitorsBarChartOptions,
|
options: horizontalBarChartOptions,
|
||||||
title: 'Подписчики конкурентов'
|
title: 'Подписчики конкурентов'
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -3058,13 +3396,24 @@ const chartsDataSegments = computed(() => {
|
|||||||
const parsedContent = computed(() => {
|
const parsedContent = computed(() => {
|
||||||
let segments = [];
|
let segments = [];
|
||||||
|
|
||||||
|
// Check for v2ReportNormalized competitive_environment first
|
||||||
|
if (v2ReportNormalized.value?.sections?.competitive_environment) {
|
||||||
|
const competitiveEnv = v2ReportNormalized.value.sections.competitive_environment;
|
||||||
|
const processed = processCompetitiveEnvironmentData(competitiveEnv);
|
||||||
|
if (processed) {
|
||||||
|
segments.push(processed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we have both fullAnalysis and chartsData, parse fullAnalysis and inject charts
|
// If we have both fullAnalysis and chartsData, parse fullAnalysis and inject charts
|
||||||
if (report.value?.fullAnalysis && report.value?.chartsData) {
|
if (report.value?.fullAnalysis && report.value?.chartsData) {
|
||||||
segments = parseFullAnalysisWithCharts(report.value.fullAnalysis, report.value.chartsData);
|
const parsedSegments = parseFullAnalysisWithCharts(report.value.fullAnalysis, report.value.chartsData);
|
||||||
|
segments = [...segments, ...parsedSegments];
|
||||||
}
|
}
|
||||||
// If only chartsData exists, use it
|
// If only chartsData exists, use it
|
||||||
else if (report.value?.chartsData) {
|
else if (report.value?.chartsData) {
|
||||||
segments = chartsDataSegments.value;
|
const chartSegments = chartsDataSegments.value;
|
||||||
|
segments = [...segments, ...chartSegments];
|
||||||
}
|
}
|
||||||
// Otherwise, fall back to parsing fullAnalysis with JSON blocks (for old data)
|
// Otherwise, fall back to parsing fullAnalysis with JSON blocks (for old data)
|
||||||
else if (report.value?.fullAnalysis) {
|
else if (report.value?.fullAnalysis) {
|
||||||
@@ -3128,8 +3477,32 @@ const parsedContent = computed(() => {
|
|||||||
return segments.filter((segment) => {
|
return segments.filter((segment) => {
|
||||||
// If segment doesn't have a type, try to process it
|
// If segment doesn't have a type, try to process it
|
||||||
if (!segment || !segment.type) {
|
if (!segment || !segment.type) {
|
||||||
|
// Check if it's new competitive environment structure
|
||||||
|
if (segment && typeof segment === 'object' && !Array.isArray(segment)) {
|
||||||
|
if (segment.competitors && Array.isArray(segment.competitors) && segment.metrics) {
|
||||||
|
const processed = processCompetitiveEnvironmentData(segment);
|
||||||
|
if (processed) {
|
||||||
|
segments[segments.indexOf(segment)] = processed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// If it's an array (like competitors), process it
|
// If it's an array (like competitors), process it
|
||||||
if (Array.isArray(segment) && segment.length > 0 && segment[0]?.name) {
|
if (Array.isArray(segment) && segment.length > 0 && segment[0]?.name) {
|
||||||
|
// Check if it's new structure (has visibility_score)
|
||||||
|
if (segment[0]?.visibility_score !== undefined) {
|
||||||
|
const processed = processCompetitiveEnvironmentData({
|
||||||
|
competitors: segment,
|
||||||
|
metrics: {},
|
||||||
|
title: 'Конкурентная среда',
|
||||||
|
conclusion: ''
|
||||||
|
});
|
||||||
|
if (processed) {
|
||||||
|
segments[segments.indexOf(segment)] = processed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Old structure
|
||||||
const processed = {
|
const processed = {
|
||||||
type: 'competitors',
|
type: 'competitors',
|
||||||
competitors: segment,
|
competitors: segment,
|
||||||
@@ -3341,6 +3714,79 @@ const goToStrategy = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Competitive Environment: Chart and Table Synchronization
|
||||||
|
const highlightedRowIndex = ref(null);
|
||||||
|
const visibilityChartRef = ref(null);
|
||||||
|
|
||||||
|
const highlightRow = (index) => {
|
||||||
|
highlightedRowIndex.value = index;
|
||||||
|
// Highlight corresponding bar in chart if chart is available
|
||||||
|
if (visibilityChartRef.value && visibilityChartRef.value.chart && index !== null) {
|
||||||
|
const chart = visibilityChartRef.value.chart;
|
||||||
|
const meta = chart.getDatasetMeta(0);
|
||||||
|
if (meta && meta.data[index]) {
|
||||||
|
// Reset all bars
|
||||||
|
meta.data.forEach((bar) => {
|
||||||
|
bar._model.backgroundColor = bar._view.backgroundColor;
|
||||||
|
});
|
||||||
|
// Highlight the hovered bar
|
||||||
|
const hoveredBar = meta.data[index];
|
||||||
|
if (hoveredBar) {
|
||||||
|
const originalColor = hoveredBar._view.backgroundColor;
|
||||||
|
hoveredBar._model.backgroundColor = originalColor + 'CC'; // Add transparency
|
||||||
|
chart.update('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChartHover = (event, elements) => {
|
||||||
|
if (elements && elements.length > 0) {
|
||||||
|
const elementIndex = elements[0].index;
|
||||||
|
highlightedRowIndex.value = elementIndex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChartClick = (event, elements) => {
|
||||||
|
if (elements && elements.length > 0) {
|
||||||
|
const elementIndex = elements[0].index;
|
||||||
|
// Could trigger some action on click
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (competitor) => {
|
||||||
|
if (competitor.website) {
|
||||||
|
window.open(competitor.website, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWebsite = (url) => {
|
||||||
|
if (!url) return '';
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return urlObj.hostname.replace('www.', '');
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreClass = (score) => {
|
||||||
|
if (score >= 70) return 'competitive-env-score-high';
|
||||||
|
if (score >= 40) return 'competitive-env-score-medium';
|
||||||
|
return 'competitive-env-score-low';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriceSegmentSeverity = (segment) => {
|
||||||
|
const segmentLower = (segment || '').toLowerCase();
|
||||||
|
if (segmentLower.includes('premium') || segmentLower.includes('высок') || segmentLower.includes('премиум')) {
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
|
if (segmentLower.includes('low') || segmentLower.includes('низк')) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
return 'warning';
|
||||||
|
};
|
||||||
|
|
||||||
// Load analysis from query params
|
// Load analysis from query params
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const queryAnalysisId = route.query.analysisId;
|
const queryAnalysisId = route.query.analysisId;
|
||||||
@@ -3741,4 +4187,81 @@ onBeforeUnmount(() => {
|
|||||||
.comparison-table-tr:hover {
|
.comparison-table-tr:hover {
|
||||||
background-color: var(--surface-50);
|
background-color: var(--surface-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Competitive Environment Table Styles */
|
||||||
|
.competitive-environment-table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-th {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--surface-50);
|
||||||
|
border-bottom: 2px solid var(--surface-border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
color: var(--text-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-tr {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-tr:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-tr-highlighted {
|
||||||
|
background-color: var(--primary-50);
|
||||||
|
border-left: 3px solid var(--primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-score-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-score-high {
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-score-medium {
|
||||||
|
background-color: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitive-env-score-low {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .competitive-env-score-high {
|
||||||
|
background-color: rgba(16, 185, 129, 0.2);
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .competitive-env-score-medium {
|
||||||
|
background-color: rgba(245, 158, 11, 0.2);
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .competitive-env-score-low {
|
||||||
|
background-color: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user