Перейти к содержимому
Поиск
Разработка

Сфера: CRM и админ-панель на Vue — живой проект и исходники

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

Александр Некрасов
Vue CRM Сфера — админка на практике: графики, показатели и канбан в единой панели

Админ-панель становится полезной, когда за красивыми карточками есть общие данные и рабочие действия. Менеджер открывает клиента, создаёт заявку, назначает исполнителя и видит, как меняются список, календарь и показатели. Руководитель выбирает период, изучает загрузку команды и переходит от графика к конкретным записям.

«Сфера» — собственная учебная CRM на Vue 3, которую можно исследовать прямо на сайте и скачать целиком. Здесь восемь связанных разделов, разные диаграммы Apache ECharts, создание и редактирование записей, воронка продаж, канбан задач, календарь и настройки интерфейса. Готовый платный шаблон для проекта не используется.

Что можно забрать. В архиве находятся готовая сборка, Vue-компоненты, TypeScript, CSS, PHP API, демоданные, тесты и инструкция. Сервер действительно отдаёт наборы данных по HTTP. Изменения посетителя сохраняются в его браузере, поэтому каждый может экспериментировать со своей копией.

Откройте полноценную CRM и попробуйте сценарии

Открыть «Сферу» на весь экран ↗ Скачать сборку и исходники ZIP

Для первого знакомства удобнее полный экран: он даёт место боковой навигации, графикам и рабочим спискам. Встроенная версия ниже использует тот же проект. На телефоне навигация открывается отдельной панелью; таблицы и формы адаптируются к ширине экрана.

Начните с маршрута, который связывает несколько экранов:

  1. В верхней панели выберите набор данных и период. Посмотрите, как изменились показатели и графики.
  2. Откройте «Клиенты», найдите компанию и перейдите к её заявкам.
  3. Создайте или измените заявку: сумму, оплату, ответственного, статус и срок. Вернитесь в обзор и сравните результаты.
  4. В «Сделках» переведите сделку на другой этап. В «Задачах» измените состояние работы, затем проверьте календарь.
  5. В настройках смените тему и плотность, выключите анимации, измените цель и состав виджетов. Сохраните свою копию 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 нужны серверное хранение, проверка пользователя и прав, обработка конфликтов, журналирование и резервное копирование.

Знакомый макет входа сам по себе не создаёт авторизацию. Поэтому в учебном проекте нет декоративного «входа», который выдавал бы локальный интерфейс за защищённый рабочий сервис. Подключайте эти механизмы в том слое, который действительно управляет доступом к данным.

Документация и связанные материалы

Авторский код «Сферы» распространяется под MIT. Лицензии и уведомления зависимостей включены в архив. Собственный интерфейс можно развивать под поддержку, продажи, управление услугами или внутреннюю работу команды.