Files
marketing/src/components/marketing/dashboard-v3/v4/V4Competitors.vue
T
2026-04-10 12:50:08 +05:00

349 lines
9.9 KiB
Vue

<script setup>
import SectionWrapper from './SectionWrapper.vue';
import { computed, ref } from 'vue';
const props = defineProps({ id: String, data: [Array, Object] });
const normalizedData = computed(() => {
const arr = Array.isArray(props.data) ? props.data : (props.data?.competitors || []);
return arr.map(c => ({
...c,
erVal: parseFloat(c.engagementRate ?? c.er) || 0,
ratingVal: parseFloat(c.averageRating ?? c.rating) || 0,
followersVal: parseInt(c.followers) || 0,
postsVal: parseInt(c.postsPerMonth) || 0
}));
});
const expanded = ref(null);
const toggleRow = (name) => { expanded.value = expanded.value === name ? null : name; };
const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—';
const fmtPct = (n) => n != null ? parseFloat(n).toFixed(2) + '%' : '—';
const erColor = (er) => parseFloat(er) >= 6 ? 'var(--kai-green)' : parseFloat(er) >= 3 ? 'var(--kai-yellow)' : 'var(--kai-red)';
const PLAT_COLORS = { Instagram:'#E1306C', TikTok:'#69C9D0', Telegram:'#2AABEE', '2GIS':'#00B140', YouTube:'#FF0000' };
const platColor = (p) => PLAT_COLORS[p] || '#808080';
const sorted = computed(() => [...normalizedData.value].sort((a, b) => b.followersVal - a.followersVal));
// Scatter data
const scatter = computed(() => normalizedData.value.map((c, i) => ({
x: c.postsVal || 0,
y: c.erVal || 0,
r: Math.max(Math.sqrt((c.followersVal || 0) / 1000) * 5, 4),
name: c.name,
color: platColor(c.platform)
})));
const maxX = computed(() => Math.max(Math.max(...scatter.value.map(d => d.x)) * 1.1, 10));
const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) * 1.2, 5));
</script>
<template>
<SectionWrapper :id="id" title="Карта конкурентов" subtitle="Позиционирование игроков: Активность vs Вовлечённость" num="3">
<!-- Scatter plot -->
<div class="competitor-plot-card">
<div class="plot-header">
<span class="plot-axis-label">Активность (посты/мес) </span>
<span class="plot-axis-label vertical">Вовлечённость (ER %) </span>
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Лидеры (справа вверху) делают много постов и получают высокий отклик.'"></i>
</div>
<div class="plot-leader-zone">ЛИДЕРЫ</div>
<div class="plot-container">
<svg class="plot-svg" :viewBox="`0 0 500 220`" preserveAspectRatio="none">
<!-- Grid -->
<line x1="50" y1="10" x2="50" y2="200" class="plot-grid-line"/>
<line x1="50" y1="200" x2="490" y2="200" class="plot-grid-line"/>
<!-- Leader quadrant -->
<rect x="270" y="10" width="220" height="100" class="plot-leader-rect" rx="8"/>
<!-- Data points -->
<g v-for="(pt, i) in scatter" :key="i" class="plot-point-group">
<circle
:cx="50 + (pt.x / maxX) * 440"
:cy="195 - (pt.y / maxY) * 185"
:r="Math.max(pt.r, 5)"
:fill="pt.color"
class="plot-point"
/>
<text
:x="50 + (pt.x / maxX) * 440"
:y="195 - (pt.y / maxY) * 185 - Math.max(pt.r, 5) - 6"
class="plot-point-text"
>{{ pt.name }}</text>
</g>
</svg>
</div>
</div>
<!-- Table -->
<div class="table-container">
<table class="kai-table">
<thead>
<tr>
<th width="40">#</th>
<th>Бренд</th>
<th>Платформа</th>
<th>Подписчики</th>
<th>ER</th>
<th> Рейтинг</th>
<th>Контент</th>
<th width="40"></th>
</tr>
</thead>
<tbody>
<template v-for="(c, i) in sorted" :key="c.name">
<tr :class="{ 'top-row': i < 3 }" :style="i < 3 ? { '--top-color': ['#F59E0B','#939393','#CD7F32'][i] } : {}">
<td class="row-num">{{ String(i+1).padStart(2,'0') }}</td>
<td>
<div class="brand-cell">
<div class="brand-avatar" :style="{ background: platColor(c.platform) + '20', color: platColor(c.platform), borderColor: platColor(c.platform) + '40' }">
<i class="pi pi-building text-[11px]" />
</div>
<span class="brand-name">{{ c.name }}</span>
</div>
</td>
<td><span class="kai-plat-badge" :style="{ background: platColor(c.platform)+'20', color: platColor(c.platform), border: `1px solid ${platColor(c.platform)}20` }">{{ c.platform }}</span></td>
<td class="font-bold">{{ fmtNum(c.followersVal) }}</td>
<td class="font-bold" :style="{ color: erColor(c.erVal) }">{{ fmtPct(c.erVal) }}</td>
<td class="font-bold text-yellow-500"> {{ (c.ratingVal).toFixed(1) }}</td>
<td class="font-bold">{{ c.postsVal }} <small class="text-surface-500 ml-1">/ мес</small></td>
<td>
<button class="expand-btn" :class="{ active: expanded === c.name }" @click="toggleRow(c.name)">
<i class="pi pi-chevron-down" />
</button>
</td>
</tr>
<!-- Expanded content -->
<tr v-if="expanded === c.name" class="expanded-row">
<td colspan="8">
<div class="expand-content">
<div class="sw-grid">
<div class="sw-col">
<div class="sw-title green">Сильные стороны</div>
<ul class="sw-list">
<li v-for="(s, si) in (c.strengths||[])" :key="si">{{ s }}</li>
<li v-if="!c.strengths?.length" class="empty-sw">Нет данных</li>
</ul>
</div>
<div class="sw-col">
<div class="sw-title red">Слабые стороны</div>
<ul class="sw-list">
<li v-for="(w, wi) in (c.weaknesses||[])" :key="wi">{{ w }}</li>
<li v-if="!c.weaknesses?.length" class="empty-sw">Нет данных</li>
</ul>
</div>
</div>
</div>
</td>
</tr>
</template>
<tr v-if="!sorted.length"><td colspan="8" class="empty-table">Нет данных</td></tr>
</tbody>
</table>
</div>
</SectionWrapper>
</template>
<style scoped>
.competitor-plot-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 20px;
padding: 24px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.plot-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.plot-axis-label {
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-txt3);
}
.plot-leader-zone {
position: absolute;
top: 60px;
right: 40px;
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
color: var(--kai-blue);
opacity: 0.4;
letter-spacing: 0.2em;
pointer-events: none;
}
.plot-container {
padding-left: 20px;
}
.plot-grid-line {
stroke: var(--kai-border);
stroke-width: 1;
}
.plot-leader-rect {
fill: rgba(var(--kai-blue-rgb), 0.05);
}
.plot-point {
fill-opacity: 0.8;
stroke: rgba(255,255,255,0.2);
stroke-width: 1;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.plot-point-group:hover .plot-point {
fill-opacity: 1;
r: 10;
stroke-width: 2;
filter: drop-shadow(0 0 8px currentColor);
}
.plot-point-text {
font-size: 8px;
font-weight: 700;
fill: var(--kai-txt2);
text-anchor: middle;
pointer-events: none;
}
/* Table Stylings */
.table-container {
border-radius: 16px;
overflow: hidden;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
}
.top-row {
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.03), transparent);
border-left: 3px solid var(--top-color);
}
.row-num {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
color: var(--kai-txt3);
opacity: 0.6;
}
.brand-cell {
display: flex;
align-items: center;
gap: 12px;
}
.brand-avatar {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.brand-name {
font-size: 12px;
font-weight: 700;
color: var(--kai-txt);
}
.expand-btn {
background: transparent;
border: none;
color: var(--kai-txt3);
cursor: pointer;
padding: 4px;
transition: all 0.3s;
}
.expand-btn.active {
transform: rotate(180deg);
color: var(--kai-blue);
}
.expanded-row td {
padding: 0 !important;
background: rgba(45,123,255,0.04);
border-bottom: 1px solid var(--kai-border);
}
.expand-content {
padding: 24px 32px;
}
.sw-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.sw-title {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 16px;
}
.sw-title.green { color: var(--kai-green); }
.sw-title.red { color: var(--kai-red); }
.sw-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.sw-list li {
font-size: 11.5px;
color: var(--kai-txt2);
display: flex;
align-items: flex-start;
gap: 10px;
}
.sw-list li::before {
content: '→';
color: var(--kai-txt3);
font-size: 10px;
margin-top: 1px;
}
.empty-sw {
color: var(--kai-txt3) !important;
font-style: italic;
font-size: 11px !important;
}
.empty-table {
text-align: center;
color: var(--kai-txt3);
padding: 40px;
font-size: 12px;
}
</style>