154 lines
4.4 KiB
Vue
154 lines
4.4 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Воронка распределения лидов по 5 статусам воронки.
|
||
*
|
||
* Источник дизайна: liderra_v8_handoff/concepts/v8_dashboard.html секция .panel
|
||
* с #funnel-title (segmented bar + funnel-list).
|
||
* Источник правды для статусов: db/schema.sql:2130 → composables/leadStatuses.ts.
|
||
*
|
||
* Props:
|
||
* - counts: Record<slug, number> — количество лидов в каждом статусе.
|
||
* Если не передано — используется mock с равномерным распределением.
|
||
*
|
||
* Рендер:
|
||
* 1. Segmented horizontal bar — каждый сегмент пропорционален count'у статуса
|
||
* и закрашен colorHex из lead_statuses.
|
||
* 2. funnel-list — 5 строк с цветным dot + name + count, отсортированы по
|
||
* убыванию count'а (как в handoff).
|
||
*/
|
||
import { computed } from 'vue';
|
||
import { LEAD_STATUSES } from '../../composables/leadStatuses';
|
||
|
||
interface Props {
|
||
counts?: Record<string, number>;
|
||
title?: string;
|
||
}
|
||
|
||
// Default counts инлайнятся в withDefaults — Vue SFC compiler требует чтобы
|
||
// factory-функция в withDefaults не реферировала модуль-уровневые const'ы
|
||
// (checkInvalidScopeReference). Mock-распределение ~190 лидов по 5 статусам.
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
counts: () => ({
|
||
new: 24,
|
||
viewed: 18,
|
||
in_progress: 96,
|
||
won: 41,
|
||
lost: 11,
|
||
}),
|
||
title: 'Воронка',
|
||
});
|
||
|
||
const items = computed(() =>
|
||
LEAD_STATUSES.map((s) => ({
|
||
...s,
|
||
count: props.counts[s.slug] ?? 0,
|
||
})),
|
||
);
|
||
|
||
const total = computed(() => items.value.reduce((sum, i) => sum + i.count, 0));
|
||
|
||
// Список под сегментированной полосой — отсортирован по убыванию count'а
|
||
// (как в handoff — наглядность распределения).
|
||
const itemsByCount = computed(() => [...items.value].sort((a, b) => b.count - a.count));
|
||
</script>
|
||
|
||
<template>
|
||
<v-card variant="outlined" class="funnel-chart pa-0">
|
||
<div class="panel-head pa-4">
|
||
<h2 class="text-h6 panel-title ma-0">{{ props.title }}</h2>
|
||
<div class="text-body-2 text-medium-emphasis">{{ items.length }} статусов · {{ total }} лидов · сейчас</div>
|
||
</div>
|
||
|
||
<div
|
||
class="funnel-bar mx-4"
|
||
role="img"
|
||
:aria-label="`Распределение ${total} лидов по ${items.length} статусам`"
|
||
>
|
||
<div
|
||
v-for="item in items"
|
||
:key="item.slug"
|
||
class="funnel-seg"
|
||
:style="{ flex: item.count, background: item.colorHex }"
|
||
:title="`${item.nameRu} · ${item.count}`"
|
||
/>
|
||
</div>
|
||
|
||
<div class="funnel-list pa-4">
|
||
<span v-for="item in itemsByCount" :key="item.slug" class="funnel-list-item">
|
||
<span class="dot" :style="{ background: item.colorHex }" />
|
||
<span class="name">{{ item.nameRu }}</span>
|
||
<span class="qty">{{ item.count }}</span>
|
||
</span>
|
||
</div>
|
||
</v-card>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.funnel-chart {
|
||
background: #fff;
|
||
}
|
||
|
||
.panel-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-variation-settings: 'opsz' 18;
|
||
letter-spacing: -0.01em;
|
||
color: #081319;
|
||
}
|
||
|
||
.funnel-bar {
|
||
display: flex;
|
||
height: 12px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: #f0ede4;
|
||
}
|
||
|
||
.funnel-seg {
|
||
height: 100%;
|
||
transition: filter 0.15s;
|
||
}
|
||
.funnel-seg:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.funnel-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 8px 16px;
|
||
border-top: 1px solid #f0ede4;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.funnel-list-item {
|
||
display: grid;
|
||
grid-template-columns: 12px 1fr auto;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.name {
|
||
color: #343c41;
|
||
}
|
||
|
||
.qty {
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-feature-settings: 'tnum';
|
||
font-weight: 500;
|
||
color: #081319;
|
||
}
|
||
</style>
|