.
This commit is contained in:
@@ -448,6 +448,171 @@
|
||||
<HeatmapTable :matrix="segment.matrix" :title="segment.title || 'Матрица каналов по сегментам'" />
|
||||
</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 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>
|
||||
@@ -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
|
||||
const buildCompetitorsHeatmapData = (competitors) => {
|
||||
if (!Array.isArray(competitors) || competitors.length === 0) return null;
|
||||
@@ -2303,7 +2566,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
||||
title: 'Распределение по возрасту',
|
||||
chartType: 'bar',
|
||||
data: chartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
chartHeight: 'height: 350px'
|
||||
};
|
||||
}
|
||||
@@ -2389,7 +2652,23 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
||||
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': {
|
||||
// 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;
|
||||
const reachChartData = buildCompetitorsBarChartData(data, 'reach');
|
||||
const followersChartData = buildCompetitorsBarChartData(data, 'followers');
|
||||
@@ -2402,7 +2681,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: reachChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Охват конкурентов'
|
||||
}
|
||||
: null,
|
||||
@@ -2410,7 +2689,7 @@ const mapJsonBlockToSegment = (identifier, data, rawText) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: followersChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Подписчики конкурентов'
|
||||
}
|
||||
: null
|
||||
@@ -2467,9 +2746,28 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
||||
return null;
|
||||
|
||||
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 || [];
|
||||
|
||||
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 followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
||||
|
||||
@@ -2481,7 +2779,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: reachChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Охват конкурентов'
|
||||
}
|
||||
: null,
|
||||
@@ -2489,7 +2787,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: followersChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Подписчики конкурентов'
|
||||
}
|
||||
: null
|
||||
@@ -2521,6 +2819,7 @@ const getChartSegmentByPlaceholder = (placeholder, chartsData) => {
|
||||
return segment;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'CHART_SWOT':
|
||||
// Check for swotCompetitors first
|
||||
@@ -2602,7 +2901,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
||||
title: 'Распределение по возрасту',
|
||||
chartType: 'bar',
|
||||
data: chartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
chartHeight: 'height: 350px'
|
||||
};
|
||||
}
|
||||
@@ -2634,9 +2933,29 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
||||
}
|
||||
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 || [];
|
||||
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 followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
||||
|
||||
@@ -2648,7 +2967,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: reachChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Охват конкурентов'
|
||||
}
|
||||
: null,
|
||||
@@ -2656,7 +2975,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: followersChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Подписчики конкурентов'
|
||||
}
|
||||
: null
|
||||
@@ -2688,6 +3007,7 @@ const getChartSegmentByIdentifier = (identifier, chartsData) => {
|
||||
return segment;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'swot':
|
||||
// Check for swotCompetitors first
|
||||
@@ -2836,9 +3156,27 @@ const tryParseJsonSegment = (text) => {
|
||||
try {
|
||||
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
|
||||
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)) {
|
||||
return {
|
||||
type: 'competitors',
|
||||
@@ -2855,7 +3193,7 @@ const tryParseJsonSegment = (text) => {
|
||||
const chartType = parsedJson.chartType || 'bar';
|
||||
|
||||
// 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)
|
||||
const chartData = normalizeChartJsData(parsedJson, chartType);
|
||||
@@ -3001,7 +3339,7 @@ const chartsDataSegments = computed(() => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: reachChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Охват конкурентов'
|
||||
}
|
||||
: null,
|
||||
@@ -3009,7 +3347,7 @@ const chartsDataSegments = computed(() => {
|
||||
? {
|
||||
chartType: 'bar',
|
||||
data: followersChartData,
|
||||
options: competitorsBarChartOptions,
|
||||
options: horizontalBarChartOptions,
|
||||
title: 'Подписчики конкурентов'
|
||||
}
|
||||
: null
|
||||
@@ -3058,13 +3396,24 @@ const chartsDataSegments = computed(() => {
|
||||
const parsedContent = computed(() => {
|
||||
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 (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
|
||||
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)
|
||||
else if (report.value?.fullAnalysis) {
|
||||
@@ -3128,8 +3477,32 @@ const parsedContent = computed(() => {
|
||||
return segments.filter((segment) => {
|
||||
// If segment doesn't have a type, try to process it
|
||||
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 (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 = {
|
||||
type: 'competitors',
|
||||
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
|
||||
onMounted(async () => {
|
||||
const queryAnalysisId = route.query.analysisId;
|
||||
@@ -3741,4 +4187,81 @@ onBeforeUnmount(() => {
|
||||
.comparison-table-tr:hover {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user