Сфера: CRM и админ-панель на Vue — живой проект и исходники
Собственная CRM на Vue 3: восемь связанных разделов, графики ECharts, заявки, клиенты, воронка продаж, задачи и календарь. Попробуйте полноэкранное демо и скачайте сборку с исходниками.

Админ-панель становится полезной, когда за красивыми карточками есть общие данные и рабочие действия. Менеджер открывает клиента, создаёт заявку, назначает исполнителя и видит, как меняются список, календарь и показатели. Руководитель выбирает период, изучает загрузку команды и переходит от графика к конкретным записям.
«Сфера» — собственная учебная CRM на Vue 3, которую можно исследовать прямо на сайте и скачать целиком. Здесь восемь связанных разделов, разные диаграммы Apache ECharts, создание и редактирование записей, воронка продаж, канбан задач, календарь и настройки интерфейса. Готовый платный шаблон для проекта не используется.
Откройте полноценную CRM и попробуйте сценарии
Открыть «Сферу» на весь экран ↗ Скачать сборку и исходники ZIP
Для первого знакомства удобнее полный экран: он даёт место боковой навигации, графикам и рабочим спискам. Встроенная версия ниже использует тот же проект. На телефоне навигация открывается отдельной панелью; таблицы и формы адаптируются к ширине экрана.
Начните с маршрута, который связывает несколько экранов:
- В верхней панели выберите набор данных и период. Посмотрите, как изменились показатели и графики.
- Откройте «Клиенты», найдите компанию и перейдите к её заявкам.
- Создайте или измените заявку: сумму, оплату, ответственного, статус и срок. Вернитесь в обзор и сравните результаты.
- В «Сделках» переведите сделку на другой этап. В «Задачах» измените состояние работы, затем проверьте календарь.
- В настройках смените тему и плотность, выключите анимации, измените цель и состав виджетов. Сохраните свою копию JSON.
Демоданные вымышлены. В начальных наборах дата отсчёта зафиксирована на 6 ноября 2026 года: это позволяет повторять упражнения независимо от сегодняшней даты.
Восемь разделов одного приложения
- Обзор. Основные показатели, динамика выбранной группы заявок, каналы обращений, услуги, выполнение цели, ближайшая работа и переходы к деталям.
- Заявки. Фильтры, поиск, сортировка, страницы списка, создание, редактирование и CSV. Стоимость и полученная сумма хранятся отдельно.
- Клиенты. Контакты, сегменты и связанные заявки. Переход к клиенту меняет рабочий контекст, а не создаёт копию независимого списка.
- Сделки. Воронка текущих этапов, ответственные и суммы предложений. Этап можно изменить перетаскиванием или обычным элементом управления.
- Задачи. Канбан по состоянию работы, приоритет, исполнитель и срок. Для управления с клавиатуры и телефона предусмотрена альтернатива drag-and-drop.
- Календарь. Сроки задач и заявок, переходы между месяцами и открытие связанной записи.
- Аналитика. Разные представления данных: ряды, столбцы, доли, текущая воронка, недельные группы и загрузка команды.
- Настройки. Светлая и тёмная тема, плотность, анимация, виджеты, цель, JSON-копии, восстановление и демонстрация сетевой ошибки.
Количество страниц само по себе ничего не гарантирует. Важнее, что у записи один ID, у клиента одни связанные данные, а сохранение проходит через общий слой состояния. Именно это позволяет развивать интерфейс без расхождения показателей.
Три серверных набора: панель меняется вместе с данными
В верхней панели можно загрузить три ситуации: обычный месяц, рост после рекламы и аврал в команде. Это целые согласованные наборы клиентов, заявок, сделок, задач и сотрудников. При переключении выполняется настоящий запрос к PHP API, а не простая подмена цифры в карточке.
В обычном сценарии удобно изучать базовые переходы. Сценарий роста показывает изменение объёма и каналов обращений. Перегрузка делает заметнее очередь работы и просроченные сроки. После загрузки одни и те же функции расчёта строят новые карточки, графики, доски и календарь.
GET api/demo.php?scenario=balanced
GET api/demo.php?scenario=growth
GET api/demo.php?scenario=overload
Ответ содержит признак успеха, имя сценария, время выдачи и snapshot — полный набор сущностей. Время выдачи означает момент HTTP-ответа, а не дату создания всех записей. Внутри снимка остаётся отдельная фиксированная дата для учебных периодов.
type LoadState = 'loading' | 'ready' | 'error';
type Scenario = 'balanced' | 'growth' | 'overload';
interface DemoResponse {
ok: true;
scenario: Scenario;
loadedAt: string;
snapshot: DataSnapshot;
}
Проверка TypeScript работает во время разработки, а сетевой JSON приходит во время исполнения. Поэтому ответ API обязательно проверяется отдельно: типы полей, допустимые статусы, реальные даты, суммы и ссылки между сущностями. Приложение заменяет текущий набор только после успешной проверки всего пакета.
Почему Vue, Bootstrap и ECharts
Vue отвечает за компоненты и реактивное отображение. Pinia хранит общее состояние. Vue Router связывает разделы с адресом страницы. Bootstrap 5 даёт сетку, поля и базовые элементы оформления; собственные стили формируют внешний вид «Сферы». Apache ECharts строит графики.
У компонентов разные роли. ECharts не знает, что такое заявка, а Bootstrap не решает, какие строки входят в выбранный период. Правила находятся в модели приложения. Такое разделение позволяет заменить график или форму, сохранив расчёты.
В проекте закреплены Vue 3.5.45, Bootstrap 5.3.8, ECharts 6.1.0, Pinia 4.1.0 и Vue Router 5.4.0. Для проверенной сборки используется TypeScript 6.0.3: выбор версии определяется совместимостью инструментов, а не только меткой latest.
Bootstrap подключён как CSS. Открытием меню, редактора и других интерактивных элементов управляет Vue. Два независимых механизма, одновременно меняющих один DOM-элемент, способны конфликтовать — этот нюанс описан в документации Bootstrap для JavaScript-фреймворков.
Модель: связываем сущности по ID
У заявки есть ссылка на клиента и исполнителя. У задачи — ссылки на клиента и, при необходимости, заявку. Сделка также принадлежит клиенту. Название компании используется для показа, а стабильный ID — для связи. Если пользователь переименует компанию, заявки сохранят связь с ней.
interface Order {
id: string;
clientId: string;
title: string;
service: string;
assigneeId: string;
channel: 'web' | 'email' | 'phone' | 'recommendation';
status: 'new' | 'working' | 'review' | 'done' | 'cancelled';
priority: 'low' | 'normal' | 'high';
createdAt: string;
dueDate: string;
amount: number;
paidAmount: number;
notes: string;
}
В учебной модели деньги хранятся целыми рублями. Полученная сумма должна быть неотрицательной и не превышать стоимость заявки. Для реального сервиса с копейками определите единицу хранения заранее, например целое число копеек. Строка «15 000 ₽» относится к отображению, а не к расчётам.
При импорте недостаточно проверить, что clientId — строка. Нужно убедиться, что такой клиент присутствует в том же снимке. Иначе интерфейс получит заявку без владельца, а переходы между разделами начнут ломаться.
Один источник состояния для всех экранов
Если обзор хранит собственный массив, таблица — другой, а календарь — третий, после первого редактирования появятся расхождения. В «Сфере» исходные записи хранятся в Pinia. Отфильтрованные списки и показатели вычисляются из них.
import { computed } from 'vue';
import { useCrmStore } from './store';
const crm = useCrmStore();
const workingOrders = computed(() =>
crm.periodOrders.filter(order => order.status === 'working')
);
const workingAmount = computed(() =>
workingOrders.value.reduce((sum, order) => sum + order.amount, 0)
);
Это небольшой пример использования общего store. После сохранения заявки зависимые вычисления обновляются автоматически. Компоненту не нужно вручную отправлять новую сумму в каждую карточку.
Настройки вида не должны менять данные. Скрытие графика убирает виджет с экрана, а не удаляет заявки. Смена плотности меняет отступы, а не размер выборки. Период, напротив, действительно меняет набор записей и должен быть заметен рядом с показателями.
Как читать KPI и не придумывать выручку
Сумма заявок — сумма поля amount у выбранных записей. Получено по заявкам — сумма paidAmount той же группы. Эти показатели отвечают на разные вопросы.
В модели нет отдельного журнала платежей. Поэтому «получено по заявкам за период создания» не означает «деньги, поступившие именно в эти даты». Для кассового отчёта понадобятся отдельные платежи с датой, возвратами и связью с заявкой. Аналогично текущий статус «Готово» не сообщает дату завершения.
function summarize(rows: Order[]) {
const count = rows.length;
const amount = rows.reduce((sum, row) => sum + row.amount, 0);
const paid = rows.reduce((sum, row) => sum + row.paidAmount, 0);
const done = rows.filter(row => row.status === 'done').length;
return {
count, amount, paid,
average: count ? amount / count : null,
completionRate: count ? done / count * 100 : null,
};
}
При пустой группе количество и сумма равны нулю, а среднее и доля не имеют наблюдений для расчёта. Для сравнения используются соседние периоды одинаковой длины. Если предыдущий показатель равен нулю, интерфейс должен объяснить отсутствие базы, а не показывать Infinity или случайные «+100%».
function changePercent(current: number, previous: number): number | null {
return previous === 0 ? null : (current - previous) / previous * 100;
}
Разные графики — разные вопросы
В проекте есть линейные ряды, столбцы по услугам, кольцевая диаграмма каналов, воронка текущих этапов, тепловая карта задач, индикатор цели, недельные группы и небольшие графики динамики. Они распределены между обзором и аналитикой, чтобы первый экран оставался читаемым.
- Ряд помогает увидеть распределение выбранных заявок по дням.
- Столбцы позволяют сравнить услуги.
- Кольцо показывает состав обращений по каналам.
- Воронка описывает текущие стадии сделок. Она не доказывает конверсию переходов: для этого нужна история стадий.
- Тепловая карта показывает задачи команды по срокам; число задач не заменяет оценку трудоёмкости в часах.
- Индикатор цели сравнивает выбранный показатель с пользовательским значением настройки.
Диапазон каждого блока обозначается явно. Например, задачи по срокам и сделки текущей воронки могут описывать другой набор, чем заявки по дате создания. Их нельзя молча выдавать за одну и ту же выборку.
// Экземпляр создаётся один раз после появления контейнера.
const chart = echarts.init(container);
chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: series.map(item => item.date) },
yAxis: { type: 'value' },
series: [{ id: 'orders', type: 'line', smooth: true,
data: series.map(item => item.value), areaStyle: {} }],
});
// При изменении данных обновляем существующий график.
chart.setOption({ series: [{ id: 'orders', data: nextValues }] });
// Перед удалением контейнера вызываем chart.dispose().
Это схема жизненного цикла, а не отдельная готовая страница. Полная Vue-обёртка находится в ChartPanel.vue: она учитывает изменение размеров, тему, настройку анимаций и освобождение ресурсов. У графиков есть текстовые представления данных; цвет и canvas не остаются единственным способом понять результат.
Загрузка и ошибка должны сохранять рабочий контекст
При выборе нового сценария пользователь видит состояние загрузки. Полученный JSON сначала проходит проверку, затем целиком становится текущим набором. Если запрос завершился ошибкой, прежние данные сохраняются и рядом появляется объяснение.
async function readScenario(name: string, signal: AbortSignal) {
const url = './api/demo.php?scenario=' + encodeURIComponent(name);
const response = await fetch(url, {
signal, headers: { Accept: 'application/json' }, cache: 'no-store',
});
const body = await response.json();
if (!response.ok || body.ok !== true) {
throw new Error(body.error?.message || 'Не удалось загрузить данные');
}
return body.snapshot; // Затем — полная проверка схемы и связей.
}
Если пользователь быстро переключил два набора, поздний ответ первого запроса не должен заменить результат второго. Для этого используются отмена чтения и проверка актуальности запроса. Отмена запроса чтения безопасно прекращает ожидание; с сохранением на сервер ситуация сложнее — сервер мог уже выполнить действие.
В настройках есть проверка учебной серверной ошибки. Она обращается к PHP endpoint и получает настоящий HTTP 503. После этого можно повторить загрузку. Это отдельный сценарий, поэтому ошибка не маскируется под пустой список заказов.
Редактор: черновик, проверка и понятное сохранение
Создание и редактирование используют общую выдвижную форму. Поля работают с черновиком: отмена закрывает форму без применения введённых значений. При ошибке данные остаются на месте, а сообщение объясняет, какое поле нужно исправить.
Пустую сумму нельзя молча превращать в ноль через Number(''). Сначала проверяется наличие и формат значения, затем диапазон. У Vue поле type="number" автоматически использует преобразование чисел, но пустое значение остаётся строкой. Поэтому обработчик принимает string | number; это поведение описано в документации v-model. Дата должна существовать в календаре, статус — входить в разрешённый набор, а исполнитель — присутствовать в справочнике.
function wholeRubles(raw: string | number): number {
const text = String(raw).trim(); // Vue number-input может вернуть число.
if (!/^\d+$/.test(text)) throw new Error('Введите сумму целыми рублями');
const value = Number(text);
if (!Number.isSafeInteger(value) || value < 0 || value > 100_000_000) throw new Error('Неверная сумма');
return value;
}
После сохранения редактор закрывается, запись обновляется в общем наборе, а действие появляется в уведомлениях. Если запись перестала соответствовать выбранным условиям, она может исчезнуть из текущего списка: это нормальный результат изменения статуса или периода. Пользователю нужен понятный переход к данным, а не ощущение, что запись потерялась.
Пользовательский текст выводится как текст, без исполнения HTML. Это правило важно даже в учебной CRM: JSON-импорт также является внешним источником данных.
Канбан и календарь как рабочие инструменты
Перетаскивание удобно мышью, но не должно быть единственным способом сменить состояние. У задач и сделок есть обычное управление статусом. Это полезно на сенсорном экране, с клавиатуры и для пользователей, которым точное перетаскивание неудобно.
Столбец доски — представление значения поля, а не отдельное хранилище карточек. Перемещение меняет этап или статус исходной сущности, после чего список и показатели пересчитываются. Календарь использует те же сроки: изменение даты в редакторе переносит событие.
// Примеры вызовов настоящего store проекта:
crm.moveDeal(dealId, 'negotiation');
crm.moveTask(taskId, 'done');
crm.openEditor('order', orderId);
Для собственного проекта определите допустимые переходы отдельно. Например, заявка может требовать проверки перед завершением. В этой демонстрации этапы помогают изучить интерфейс; сложный процесс согласования следует добавлять как явные бизнес-правила.
Настройки действительно меняют приложение
Тема меняет оболочку, поля и графики. Плотность влияет на расстояния в рабочих списках. Состав виджетов позволяет убрать ненужные блоки. Цель сразу отражается в соответствующем индикаторе.
Анимация используется для переходов, появления панелей и обновления графиков. Её можно выключить; системная настройка уменьшения движения также учитывается. Эффект должен помогать увидеть изменение, а не мешать нажать кнопку или прочитать результат.
crm.updateSettings({ theme: 'dark', density: 'compact' });
crm.updateSettings({ animations: false });
JSON-экспорт сохраняет структуру данных и настройки. Импорт сначала проверяет весь пакет и только затем заменяет текущий набор. При некорректном файле рабочие данные остаются прежними. CSV решает другую задачу: выгрузку плоского списка выбранных строк.
Хранилище браузера привязано к адресу сайта. Демо на сайте и копия на localhost имеют разные данные. Очистка браузера может удалить изменения, поэтому экспорт JSON — полезный способ сохранить собственный эксперимент.
Скачивание и запуск без сборки
Скачать полный проект «Сфера».
В архиве есть готовый каталог dist. Для запуска нужен PHP 8.2+; Composer и Node.js для уже собранной версии не требуются. Откройте терминал в корне распакованного проекта:
php -S 127.0.0.1:8080 -t dist
Откройте http://127.0.0.1:8080/. Остановка — Ctrl+C. PHP обслуживает приложение и действительно выполняет API загрузки наборов. Открытие HTML двойным щелчком или простой сервер статических файлов не заменяют этот запуск.
Готовую папку можно разместить на хостинге с PHP. Маршруты используют hash history, поэтому переходы между разделами не требуют отдельного rewrite. База данных, регистрация пользователей и настройка Composer для этой демоверсии не нужны.
Разработка: компоненты, тесты и сборка
Для изменения Vue-компонентов нужен Node.js 20.19+ или 22.12+. В архив включён lock-файл. В первом терминале установите зависимости и запустите Vite:
npm ci
npm run dev
Во втором терминале запустите PHP API для разработки:
php -S 127.0.0.1:8785 -t public
Приложение открывается на http://127.0.0.1:8783/. Vite перенаправляет запросы /api на PHP. Изменения компонентов обновляются автоматически.
npm test
npm run build
Сборка проверяет TypeScript и создаёт новый dist. В src/types.ts находятся типы, в domain.ts — проверки и вычисления, в store.ts — общие данные и операции. Экраны лежат в src/views, оформление — в src/styles. Наборы сервера расположены в public/data, PHP-маршрут — в public/api/demo.php.
После собственной доработки пройдите сквозной маршрут: загрузка набора → клиент → заявка → редактирование → обзор → экспорт → восстановление. Отдельно проверьте пустой поиск, неверный файл, сетевую ошибку, телефон и управление клавиатурой.
Как превратить основу в свой сервис
Начните с одной сущности и сохраните её контракт. Например, замените источник заявок на свой PHP API, затем добавьте сохранение и только после этого подключайте остальные разделы. Обновляйте store подтверждённым сервером результатом, а ошибки полей возвращайте к соответствующим элементам формы.
async function saveOrder(id: string, changes: unknown, revision: number) {
const response = await fetch('/api/orders/' + encodeURIComponent(id), {
method: 'PATCH', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changes, revision }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.message || 'Не удалось сохранить');
return result;
}
Это пример будущего маршрута вашего сервиса, а не существующий endpoint демо. В текущей версии PHP выдаёт исходные наборы, а редактирование выполняется в браузере. Для общей многопользовательской CRM нужны серверное хранение, проверка пользователя и прав, обработка конфликтов, журналирование и резервное копирование.
Знакомый макет входа сам по себе не создаёт авторизацию. Поэтому в учебном проекте нет декоративного «входа», который выдавал бы локальный интерфейс за защищённый рабочий сервис. Подключайте эти механизмы в том слое, который действительно управляет доступом к данным.
Документация и связанные материалы
- Vue: руководство и TypeScript в Vue.
- Pinia: состояние и действия.
- Vue Router: маршрутизация.
- Apache ECharts: начало работы, события графиков.
- Bootstrap 5.
- Tabler: связанная панель заказов — более компактный практикум по устройству дашборда.
- Chart.js: мастерская графиков — другой подход к визуализации.
Авторский код «Сферы» распространяется под MIT. Лицензии и уведомления зависимостей включены в архив. Собственный интерфейс можно развивать под поддержку, продажи, управление услугами или внутреннюю работу команды.


