diff --git a/src/assets/kai-v4.css b/src/assets/kai-v4.css index 1f7b843..ba25e39 100644 --- a/src/assets/kai-v4.css +++ b/src/assets/kai-v4.css @@ -1,33 +1,48 @@ -/* ========================================================= +/* ========================================================= KAI v4.0 Dashboard — Dark Theme Globals Loaded by MarketingAnalysisV3.vue via diff --git a/src/components/marketing/dashboard-v3/v4/V4Behavioral.vue b/src/components/marketing/dashboard-v3/v4/V4Behavioral.vue index 3ecee9e..acf3109 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Behavioral.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Behavioral.vue @@ -6,58 +6,297 @@ const props = defineProps({ id: String, data: Object }); const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—'; const behaviors = computed(() => [ - { icon:'🏷', label:'Акции и скидки', val: props.data?.promoUsagePercent || 0, desc:'Конкурентов используют акции', color:'#FF7A2D', tip: 'Процент успешных конкурентов, активно использующих промо-предложения' }, - { icon:'💰', label:'Открытые цены', val: props.data?.priceVisibilityPercent || 0, desc:'Указывают цены открыто (остальные — в Direct)', color:'#A855F7', tip: 'Процент компаний, которые пишут цены прямо в постах, а не просят писать в Директ' }, - { icon:'📅', label:'Прямое бронирование', val: props.data?.directBookingPercent || 0, desc:'Постов заканчиваются призывом записаться', color:'#2D7BFF', tip: 'Наличие прямой ссылки на бронирование, заявку или номер телефона' }, + { icon:'pi-ticket', label:'Акции и скидки', val: props.data?.promoUsagePercent || 0, desc:'Конкурентов используют акции', color:'var(--kai-orange)', tip: 'Процент успешных конкурентов с промо-предложениями' }, + { icon:'pi-dollar', label:'Открытые цены', val: props.data?.priceVisibilityPercent || 0, desc:'Указывают цены открыто', color:'var(--kai-purple)', tip: 'Процент компаний, пишущих цены в постах' }, + { icon:'pi-calendar-plus', label:'Прямое бронирование', val: props.data?.directBookingPercent || 0, desc:'Призывы к записи/брони', color:'var(--kai-blue)', tip: 'Наличие прямой ссылки на бронирование/заявку' }, ]); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Cii.vue b/src/components/marketing/dashboard-v3/v4/V4Cii.vue index af96ad0..f48b7d2 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Cii.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Cii.vue @@ -10,20 +10,21 @@ const factors = computed(() => props.data?.contributingFactors || []); const ciiColor = computed(() => { const v = cii.value; - if (v <= 25) return '#00D48B'; - if (v <= 50) return '#F59E0B'; - if (v <= 75) return '#FF7A2D'; - return '#EF4444'; -}); -const ciiDesc = computed(() => { - const v = cii.value; - if (v <= 25) return '✅ Хорошие условия для старта. Рынок не перегружен.'; - if (v <= 50) return '⚡ Конкуренция есть, но место найдётся. Важен контент.'; - if (v <= 75) return '🔥 Рынок активный. Нужна чёткая стратегия отличия.'; - return '⚠️ Высокая конкуренция. Требуется серьёзное SMM-присутствие.'; + if (v <= 25) return 'var(--kai-green)'; + if (v <= 50) return 'var(--kai-yellow)'; + if (v <= 75) return 'var(--kai-orange)'; + return 'var(--kai-red)'; }); -const FACTOR_ICONS = ['👥','📅','❤️','⭐']; +const ciiDesc = computed(() => { + const v = cii.value; + if (v <= 25) return 'Хорошие условия для старта. Рынок не перегружен.'; + if (v <= 50) return 'Конкуренция есть, но место найдётся. Важен контент.'; + if (v <= 75) return 'Рынок активный. Нужна чёткая стратегия отличия.'; + return 'Высокая интенсивность. Требуется сильное присутствие.'; +}); + +const FACTOR_ICONS = ['pi-users', 'pi-calendar', 'pi-percentage', 'pi-star']; // SVG half-gauge const arcD = computed(() => { @@ -33,49 +34,220 @@ const arcD = computed(() => { const r = 80; const x = 100 + r * Math.cos(rad); const y = 100 + r * Math.sin(rad); - // A half-circle never exceeds 180 degrees, so the large-arc-flag should always be 0 const lg = 0; return `M 20 100 A 80 80 0 ${lg} 1 ${x} ${y}`; }); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Competitors.vue b/src/components/marketing/dashboard-v3/v4/V4Competitors.vue index 9c5317d..6e98866 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Competitors.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Competitors.vue @@ -20,7 +20,7 @@ const toggleRow = (name) => { expanded.value = expanded.value === name ? null : 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 ? '#00D48B' : parseFloat(er) >= 3 ? '#F59E0B' : '#EF4444'; +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'; @@ -39,100 +39,310 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) * + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Conclusions.vue b/src/components/marketing/dashboard-v3/v4/V4Conclusions.vue index 3ca5124..2aa3960 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Conclusions.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Conclusions.vue @@ -4,14 +4,13 @@ import { computed } from 'vue'; const props = defineProps({ id: String, data: Array }); -const ICONS = ['👥','🗂','⭐','📈','💲','🎬','⚡']; +const ICONS = ['pi-users', 'pi-folder', 'pi-star', 'pi-chart-line', 'pi-dollar', 'pi-video', 'pi-bolt']; const conclusions = computed(() => { const arr = props.data || []; return arr.map((text, i) => { - // try to guess the theme based on text const t = text.toLowerCase(); - let themeIdx = 1; // default layout/content + let themeIdx = 1; if (t.includes('конкурент') || t.includes('игрок')) themeIdx = 0; else if (t.includes('репутац') || t.includes('отзыв') || t.includes('рейтинг')) themeIdx = 2; else if (t.includes('спрос') || t.includes('пик')) themeIdx = 3; @@ -25,23 +24,139 @@ const conclusions = computed(() => { + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Content.vue b/src/components/marketing/dashboard-v3/v4/V4Content.vue index e79b2dc..8fae3d4 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Content.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Content.vue @@ -7,11 +7,11 @@ const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—'; const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—'; const SEGMENTS = [ - { key: 'demoContentPercent', label: 'Демонстрация', color: '#2D7BFF' }, - { key: 'expertContentPercent', label: 'Экспертный', color: '#00D48B' }, - { key: 'salesContentPercent', label: 'Продающий', color: '#FF7A2D' }, - { key: 'reviewsContentPercent', label: 'Отзывы', color: '#A855F7' }, - { key: 'engagementContentPercent', label: 'Вовлекающий', color: '#F59E0B' }, + { key: 'demoContentPercent', label: 'Демонстрация', color: 'var(--kai-blue)' }, + { key: 'expertContentPercent', label: 'Экспертный', color: 'var(--kai-green)' }, + { key: 'salesContentPercent', label: 'Продающий', color: 'var(--kai-orange)' }, + { key: 'reviewsContentPercent', label: 'Отзывы', color: 'var(--kai-purple)' }, + { key: 'engagementContentPercent', label: 'Вовлекающий', color: 'var(--kai-yellow)' }, ]; const segments = computed(() => SEGMENTS.map(s => ({ ...s, value: props.data?.[s.key] || 0 })).filter(s => s.value > 0)); @@ -19,7 +19,7 @@ const segments = computed(() => SEGMENTS.map(s => ({ ...s, value: props.data?.[s // SVG donut chart const donut = computed(() => { const total = segments.value.reduce((a, s) => a + s.value, 0) || 1; - const r = 70, cx = 90, cy = 90, stroke = 28; + const r = 70, cx = 90, cy = 90, stroke = 22; let cum = 0; return segments.value.map(s => { const pct = s.value / total; @@ -34,13 +34,13 @@ const donut = computed(() => { + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Demand.vue b/src/components/marketing/dashboard-v3/v4/V4Demand.vue index 921c6ab..c8282e9 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Demand.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Demand.vue @@ -12,17 +12,18 @@ const demandTrend = computed(() => { const last = seasonality.value[seasonality.value.length - 1].value; return first ? Math.round(((last - first) / first) * 100) : 0; }); + const trendDir = computed(() => demandTrend.value >= 0 ? '▲ +' + demandTrend.value + '%' : '▼ ' + demandTrend.value + '%'); -const trendColor = computed(() => demandTrend.value >= 0 ? '#00D48B' : '#EF4444'); +const trendColor = computed(() => demandTrend.value >= 0 ? 'var(--kai-green)' : 'var(--kai-red)'); // Area chart (SVG) const chartPath = computed(() => { - if (seasonality.value.length === 0) return ''; + if (seasonality.value.length === 0) return { stroke: '', fill: '', pts: [] }; const pts = seasonality.value.map(s => s.value); - const max = Math.max(...pts, 100); - const min = Math.min(...pts, 0); + const max = Math.max(...pts, 10); + const min = 0; const range = max - min || 1; - const w = 400, h = 180; + const w = 500, h = 180; const step = w / (pts.length - 1 || 1); const coords = pts.map((val, i) => { @@ -42,83 +43,289 @@ const chartPath = computed(() => { + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Executive.vue b/src/components/marketing/dashboard-v3/v4/V4Executive.vue index e143f80..cc50690 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Executive.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Executive.vue @@ -11,7 +11,7 @@ const lvlClass = (lvl) => { if (l.includes('низк') || l.includes('low')) return 'kai-level-low'; return 'kai-level-high'; }; -const erColor = (v) => parseFloat(v) >= 4 ? '#00D48B' : parseFloat(v) >= 2 ? '#F59E0B' : '#EF4444'; +const erColor = (v) => parseFloat(v) >= 4 ? 'var(--kai-green)' : parseFloat(v) >= 2 ? 'var(--kai-yellow)' : 'var(--kai-red)'; const stars = (r) => { const n = parseFloat(r) || 0; return '★'.repeat(Math.round(n)) + '☆'.repeat(5 - Math.round(n)); @@ -21,46 +21,111 @@ const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—'; const kpis = computed(() => { const d = props.data || {}; return [ - { icon: '👥', num: d.activeCompetitors ?? '—', label: 'активных конкурентов', sub: d.competitionLevel, subClass: lvlClass(d.competitionLevel), color: '#2D7BFF' }, - { icon: '📈', num: fmtPct(d.averageNicheEr), label: 'средний ER в нише', sub: 'Уровень вовлечённости аудитории', numColor: erColor(d.averageNicheEr), color: '#00D48B', term: 'er' }, - { icon: '⭐', num: (parseFloat(d.averageRating) || 0).toFixed(1), label: 'средний рейтинг на картах', sub: stars(d.averageRating), color: '#F59E0B', term: 'rating' }, - { icon: '📊', num: d.demandTrend || '—', label: 'тренд спроса', sub: 'Изменение за период', color: '#A855F7', small: true, term: 'trend' }, - { icon: '🎯', num: d.businessStage || '—', label: 'этап бизнеса', sub: d.geography || '', color: '#FF7A2D', small: true }, - { icon: '⚡', num: null, label: 'главный инсайт', sub: (d.keyFigures || [])[0] || '—', special: true, color: '#2D7BFF' }, + { icon: 'pi-users', num: d.activeCompetitors ?? '—', label: 'конкурентов', sub: d.competitionLevel, subClass: lvlClass(d.competitionLevel), color: 'var(--kai-blue)' }, + { icon: 'pi-chart-line', num: fmtPct(d.averageNicheEr), label: 'средний ER', sub: 'Вовлечённость', numColor: erColor(d.averageNicheEr), color: 'var(--kai-green)', term: 'er' }, + { icon: 'pi-star-fill', num: (parseFloat(d.averageRating) || 0).toFixed(1), label: 'рейтинг', sub: stars(d.averageRating), color: 'var(--kai-yellow)', term: 'rating' }, + { icon: 'pi-sort-alt', num: d.demandTrend || '—', label: 'тренд', sub: 'За период', color: 'var(--kai-purple)', term: 'trend' }, + { icon: 'pi-map-marker', num: d.businessStage || '—', label: 'этап', sub: d.geography || '', color: 'var(--kai-orange)' }, + { icon: 'pi-bolt', num: null, label: 'инсайт', sub: (d.keyFigures || [])[0] || '—', special: true, color: 'var(--kai-blue)' }, ]; }); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Geo.vue b/src/components/marketing/dashboard-v3/v4/V4Geo.vue index 8ccf743..82a439e 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Geo.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Geo.vue @@ -4,27 +4,30 @@ import { computed } from 'vue'; const props = defineProps({ id: String, data: Object }); -const GAUGE_COLORS = ['#00D48B','#F59E0B','#FF7A2D','#EF4444']; const gaugeColor = (v) => { const n = parseFloat(v) || 0; - if (n <= 40) return GAUGE_COLORS[0]; - if (n <= 70) return GAUGE_COLORS[1]; - if (n <= 85) return GAUGE_COLORS[2]; - return GAUGE_COLORS[3]; + if (n <= 40) return 'var(--kai-green)'; + if (n <= 70) return 'var(--kai-yellow)'; + if (n <= 85) return 'var(--kai-orange)'; + return 'var(--kai-red)'; }; + const gaugeLabel = (v) => { const n = parseFloat(v) || 0; - if (n <= 30) return '🟢 Свободный рынок'; - if (n <= 60) return '🟡 Умеренная конкуренция'; - if (n <= 85) return '🟠 Высокая конкуренция'; - return '🔴 Перегретый рынок'; + if (n <= 30) return 'Свободный рынок'; + if (n <= 60) return 'Умеренная конкуренция'; + if (n <= 85) return 'Высокая конкуренция'; + return 'Перегретый рынок'; }; + const cityStatus = (row) => { - if ((row.activePlayers > 30 && row.avgEr > 4) || row.activePlayers > 25) return { text: 'Высококонкурентный', color: '#EF4444' }; - if (row.activePlayers > 15) return { text: 'Конкурентный', color: '#FF7A2D' }; - return { text: 'Свободный', color: '#00D48B' }; + if ((row.activePlayers > 30 && row.avgEr > 4) || row.activePlayers > 25) return { text: 'HIGH', color: 'var(--kai-red)' }; + if (row.activePlayers > 15) return { text: 'MODERATE', color: 'var(--kai-orange)' }; + return { text: 'OPEN', color: 'var(--kai-green)' }; }; + const fmtPct = (n) => n != null ? parseFloat(n).toFixed(2) + '%' : '—'; + const cities = computed(() => { const arr = props.data?.cityComparison || []; return arr.map(c => ({ @@ -35,9 +38,10 @@ const cities = computed(() => { avgPostsPerMonth: parseInt(c.averagePostsPerMonth ?? c.avgPostsPerMonth) || 0 })); }); + const density = computed(() => parseFloat(props.data?.densityIndex) || 0); -// SVG gauge arc calculation +// SVG gauge arc const arcPath = computed(() => { const r = 80, cx = 100, cy = 100; const startAngle = -180, sweepAngle = 180 * (density.value / 100); @@ -47,68 +51,201 @@ const arcPath = computed(() => { const x2 = cx + r * Math.cos(toRad(startAngle + sweepAngle)); const y2 = cy + r * Math.sin(toRad(startAngle + sweepAngle)); const lg = sweepAngle > 180 ? 1 : 0; - return `M ${x1} ${y1} A ${r} ${r} 0 ${lg} 1 ${x2} ${y2}`; + return `M 20 100 A ${r} ${r} 0 ${lg} 1 ${x2} ${y2}`; }); -const sortKey = { key: 'activePlayers', dir: -1 }; -const sortedCities = computed(() => [...cities.value].sort((a,b) => (b[sortKey.key] - a[sortKey.key]) * sortKey.dir)); +const sortedCities = computed(() => [...cities.value].sort((a,b) => b.activePlayers - a.activePlayers)); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Header.vue b/src/components/marketing/dashboard-v3/v4/V4Header.vue index ba8ef36..5cb8a63 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Header.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Header.vue @@ -1,26 +1,81 @@ - + + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Landscape.vue b/src/components/marketing/dashboard-v3/v4/V4Landscape.vue index fe119b8..dd36598 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Landscape.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Landscape.vue @@ -4,66 +4,229 @@ import { computed } from 'vue'; const props = defineProps({ id: String, data: Object, demand: Object }); -const PLATFORM_COLORS = { Instagram:'#E1306C', TikTok:'#69C9D0', Telegram:'#2AABEE', '2GIS':'#00B140', YouTube:'#FF0000', GoogleMaps:'#4285F4' }; +const PLATFORM_COLORS = { + Instagram: '#E1306C', + TikTok: '#69C9D0', + Telegram: '#2AABEE', + '2GIS': '#00B140', + YouTube: '#FF0000', + GoogleMaps: '#4285F4' +}; const platforms = computed(() => { const pl = props.data?.activePlayersByPlatform || {}; const max = Math.max(...Object.values(pl), 1); - return Object.entries(pl).map(([k, v]) => ({ name: k, count: v, pct: Math.round(v / max * 100), color: PLATFORM_COLORS[k] || '#2D7BFF' })).sort((a,b)=>b.count-a.count); + return Object.entries(pl).map(([k, v]) => ({ + name: k, + count: v, + pct: Math.round(v / max * 100), + color: PLATFORM_COLORS[k] || 'var(--kai-blue)' + })).sort((a,b) => b.count - a.count); }); const cities = computed(() => { const cd = props.data?.cityDistribution || {}; const max = Math.max(...Object.values(cd), 1); - return Object.entries(cd).map(([k, v]) => ({ city: k, count: v, pct: Math.round(v / max * 100) })).sort((a,b)=>b.count-a.count).slice(0, 7); + return Object.entries(cd).map(([k, v]) => ({ + city: k, + count: v, + pct: Math.round(v / max * 100) + })).sort((a,b) => b.count - a.count).slice(0, 7); }); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Positioning.vue b/src/components/marketing/dashboard-v3/v4/V4Positioning.vue index 6e7bfe5..43232a4 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Positioning.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Positioning.vue @@ -8,11 +8,11 @@ const mode = computed(() => props.data?.mode || 'COMPARISON'); const isBench = computed(() => mode.value === 'BENCHMARK'); const axes = [ - { key: 'activity', label: '📊 Активность', tip: 'Количество и регулярность публикаций' }, - { key: 'engagement', label: '💬 Вовлечённость', tip: 'Количество лайков, комментариев, репостов' }, - { key: 'video', label: '📹 Видео', tip: 'Доля современного формата Reels / TikTok в контенте' }, - { key: 'reputation', label: '⭐ Репутация', tip: 'Рейтинг на агрегаторах и картах (2ГИС, Яндекс)' }, - { key: 'frequency', label: '📅 Частота', tip: 'Сколько проходит времени между постами' } + { key: 'activity', label: 'Активность', tip: 'Количество и регулярность публикаций' }, + { key: 'engagement', label: 'Вовлечённость', tip: 'Лайки, комментарии, репосты' }, + { key: 'video', label: 'Видео', tip: 'Доля Reels / TikTok' }, + { key: 'reputation', label: 'Репутация', tip: 'Рейтинг на агрегаторах' }, + { key: 'frequency', label: 'Частота', tip: 'Интервал между постами' } ]; const uMet = computed(() => props.data?.userMetrics || {}); @@ -22,18 +22,18 @@ const gapData = computed(() => axes.map(a => { const m = parseFloat(mMet.value[a.key]) || 0; const u = parseFloat(uMet.value[a.key]) || 0; const diff = u - m; - let prio = 'NEED', prioText = '🟡 Нужно'; - if (diff < -60) { prio = 'CRIT'; prioText = '🔴 Критично'; } - else if (diff < -40) { prio = 'IMP'; prioText = '🟠 Важно'; } - else if (diff >= 0) { prio = 'AHEAD'; prioText = '🟢 Вы впереди'; } - return { label: a.label, m, u, diff, prio, prioText }; + let prio = 'NEED', prioText = 'НУЖНО'; + if (diff < -60) { prio = 'CRIT'; prioText = 'КРИТИЧНО'; } + else if (diff < -40) { prio = 'IMP'; prioText = 'ВАЖНО'; } + else if (diff >= 0) { prio = 'AHEAD'; prioText = 'ЛИДЕР'; } + return { label: a.label, m, u, diff, prio, prioText, tip: a.tip }; })); // Radar Chart Math const radarPath = (metrics, scale=0.8) => { const pts = axes.map((a, i) => { - const v = parseFloat(metrics[a.key]) || 0; - const r = (v / 100) * 100 * scale; // max radius 100 + const v = Math.min(parseFloat(metrics[a.key]) || 0, 100); + const r = (v / 100) * 100 * scale; const angle = (Math.PI * 2 * i) / axes.length - Math.PI / 2; return `${150 + r * Math.cos(angle)},${150 + r * Math.sin(angle)}`; }); @@ -45,73 +45,291 @@ const userPath = computed(() => isBench.value ? '' : radarPath(uMet.value)); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Rationale.vue b/src/components/marketing/dashboard-v3/v4/V4Rationale.vue index 35f128d..7bf1c53 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Rationale.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Rationale.vue @@ -11,37 +11,245 @@ const pars = computed(() => { + + diff --git a/src/components/marketing/dashboard-v3/v4/V4Reputation.vue b/src/components/marketing/dashboard-v3/v4/V4Reputation.vue index 99da3d8..7ada3c1 100644 --- a/src/components/marketing/dashboard-v3/v4/V4Reputation.vue +++ b/src/components/marketing/dashboard-v3/v4/V4Reputation.vue @@ -5,61 +5,311 @@ import { computed } from 'vue'; const props = defineProps({ id: String, data: Object }); const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—'; const fmtPct = (n) => n != null ? (parseFloat(n) * 100).toFixed(0) + '%' : '—'; -const stars = (r) => { const n = parseFloat(r)||0; return '★'.repeat(Math.round(n))+'☆'.repeat(5-Math.round(n)); }; +const stars = (r) => { + const n = Math.round(parseFloat(r) || 0); + return '★'.repeat(n) + '☆'.repeat(5 - n); +}; const starDist = computed(() => { const d = props.data?.starDistribution || {}; - const max = Math.max(...Object.values(d).map(v => parseFloat(v) || 0), 1); - return [5,4,3,2,1].map(n => ({ star: n, pct: parseFloat(d[String(n)]) || 0, fill: max, color: ['#22C55E','#86EFAC','#F59E0B','#F97316','#EF4444'][5-n] })); + return [5,4,3,2,1].map(n => ({ + star: n, + pct: parseFloat(d[String(n)]) || 0, + color: ['var(--kai-green)','var(--kai-green)','var(--kai-yellow)','var(--kai-orange)','var(--kai-red)'][5-n] + })); }); + + diff --git a/src/components/marketing/dashboard-v3/v4/V4SideNav.vue b/src/components/marketing/dashboard-v3/v4/V4SideNav.vue index c0e693b..c127e9c 100644 --- a/src/components/marketing/dashboard-v3/v4/V4SideNav.vue +++ b/src/components/marketing/dashboard-v3/v4/V4SideNav.vue @@ -1,26 +1,151 @@ + + + diff --git a/src/components/marketing/wizard/CheckboxCardsField.vue b/src/components/marketing/wizard/CheckboxCardsField.vue index d756975..6e41a69 100644 --- a/src/components/marketing/wizard/CheckboxCardsField.vue +++ b/src/components/marketing/wizard/CheckboxCardsField.vue @@ -37,22 +37,28 @@ const toggle = (value) => { diff --git a/src/components/marketing/wizard/RadioGroupField.vue b/src/components/marketing/wizard/RadioGroupField.vue index 75db88d..5f9ad6c 100644 --- a/src/components/marketing/wizard/RadioGroupField.vue +++ b/src/components/marketing/wizard/RadioGroupField.vue @@ -22,22 +22,29 @@ defineEmits(['update:modelValue']); diff --git a/src/components/marketing/wizard/StepSection.vue b/src/components/marketing/wizard/StepSection.vue index e78cff6..a5ec314 100644 --- a/src/components/marketing/wizard/StepSection.vue +++ b/src/components/marketing/wizard/StepSection.vue @@ -8,11 +8,12 @@ defineProps({