Tabler: админ-панель и KPI-дашборд на практике
Создаём связанную админ-панель на Tabler: общие фильтры для показателей, графика и таблицы, редактор заявок, темы и настройки. Живая мастерская и готовый проект с PHP API.

Хорошая админ-панель помогает ответить на конкретный вопрос: что происходит с заказами, где накопилась работа и какую запись нужно открыть следующей. Четыре красивые карточки ещё не решают эту задачу. Если график считает одно, таблица показывает другое, а фильтр влияет только на заголовок, интерфейс создаёт больше вопросов, чем ответов.
Tabler даёт готовую основу на Bootstrap: сетку, навигацию, карточки, таблицы, формы и состояния интерфейса. В этой статье соберём из них связанную панель: один набор заказов, общие фильтры, показатели, график, таблица и редактор. На странице можно попробовать работу панели, изменить её оформление и забрать исходники.
Где Tabler экономит время
Представьте внутренний сервис заявок. Менеджеру нужны список, поиск, статус, сумма и возможность исправить данные. Руководителю — общее состояние и сравнение с прошлым периодом. Tabler помогает быстро собрать знакомую оболочку, в которой эти действия выглядят согласованно: одинаковые поля, отступы, кнопки и карточки.
Подход подходит для кабинета поддержки, небольшой CRM, учёта заказов, управления контентом и внутреннего инструмента команды. Когда у каждого экрана свой набор случайных стилей, даже простое добавление поля становится отдельной дизайнерской задачей. Готовые компоненты позволяют тратить больше времени на правила сервиса и понятный маршрут пользователя.
| Часть панели | Что предоставляет Tabler | Что пишет приложение |
|---|---|---|
| Карточки показателей | Внешний вид, типографику, сетку | Формулы, периоды, единицы измерения |
| Таблица заказов | Оформление строк, ячеек и статусов | Поиск, сортировку, пагинацию, загрузку данных |
| Форма редактора | Поля, подписи, состояния ошибок | Проверку значений и сохранение |
| Навигация и диалоги | Компоненты интерфейса | Маршруты, права пользователя, сценарии работы |
| Тёмная тема | Цветовую систему | Выбор, хранение настройки и обновление графика |
Tabler не создаёт базу данных, авторизацию и бизнес-правила. Макет страницы входа не проверяет пароль, а кнопка «Сохранить» не означает, что запись уже дошла до сервера. Эти границы особенно важно понимать при переносе готового шаблона в настоящий сервис.
Подробнее: назначение Tabler UI и состав компонентов.
Попробуйте связанную панель заказов
В мастерской используются 72 вымышленных заказа и фиксированная дата 6 ноября 2026 года. Благодаря этому упражнение завтра покажет тот же исходный результат. Период, поиск, статус и канал применяются к показателям, графику и таблице одновременно.
Начните с обзора: смените период, найдите заказ по названию или клиенту, затем откройте редактор строки. Обратите внимание, как изменение суммы или статуса влияет на остальные блоки. Мини-мастерская позволяет переключить светлую и тёмную тему, плотность и состав карточек — без изменения самих заказов.
Полезно пройти один маршрут целиком: от общего состояния к проблемной группе, затем к конкретной записи и обратно к показателям. Если после редактирования запись исчезла из текущего фильтра, это не обязательно ошибка: например, заказ перестал соответствовать выбранному статусу. Интерфейс должен объяснить такой результат, а не оставлять пользователя искать «пропавшие» данные.
Отдельно попробуйте пустую выборку и учебную ошибку загрузки. Пустой результат означает, что подходящих строк нет; ошибка означает, что данные не удалось получить. Эти состояния нельзя заменять одним одинаковым нулём в карточке.
Для первого знакомства используйте готовые маршруты панели:
- «Все за 30 дней». Исходный текущий период содержит 42 заказа на 381 800 ₽. Предыдущий — 30 заказов на 255 900 ₽. Средняя стоимость округляется для отображения до 9 090 ₽ и 8 530 ₽ соответственно. Завершены 21 из 42 текущих заказов, то есть 50%.
- «Новые с сайта». В исходном наборе останутся три заказа на 33 500 ₽. При сортировке по дате от новых к старым сверху будет
DEMO-063. Переведите его в статус «В работе»: он перестанет соответствовать фильтру новых заказов, и срез пересчитается до двух заказов на 24 100 ₽. - Создание записи. После возврата к общему обзору создайте заказ на 4 500 ₽ с датой 06.11.2026. Если начальный набор ещё не меняли, текущая выборка вырастет до 43 записей, сумма — до 386 300 ₽. Найдите новую запись через поиск и откройте её редактор.
- «Пустой результат» и «Ошибка и повтор». Сравните сообщения и предложенные действия. Затем восстановите обзор. Локальная имитация ошибки предназначена для изучения состояния интерфейса; она не отправляет запрос к серверу сайта.
Контрольные числа относятся к исходным данным. Если уже меняли заказы, сначала выполните сброс демонстрационного набора. Изменение оформления и скрытие карточки не должны менять эти данные.
Сначала договоримся, что означают KPI
Перед выбором цвета карточки нужно определить её смысл. В панели заказов сумма — это сумма стоимости выбранных заказов. Она не становится выручкой только потому, что рядом нарисован знак рубля: в модели нет факта оплаты, возвратов, налогов или затрат. Название показателя должно соответствовать данным.
То же относится к завершённым заказам. Если фильтр выбирает записи по дате создания, а статус хранится текущий, доля завершённых описывает именно эту группу заказов. Она не показывает число завершений, произошедших за период. Для второго вопроса потребуется отдельное поле даты завершения.
function summarizeOrders(rows) {
const count = rows.length;
const total = rows.reduce((sum, row) => sum + row.cost, 0);
const completed = rows.filter(row => row.status === 'done').length;
return {
count,
total,
average: count ? total / count : null,
completionRate: count ? completed / count * 100 : null
};
}
Это самостоятельный пример расчёта для уже проверенных строк. При пустой выборке количество и сумма равны нулю, а среднее и долю разумно показывать как «нет данных»: делить на ноль нельзя. Такой выбор делает смысл карточки явным и не смешивает отсутствие наблюдений с измеренным нулевым результатом.
В учебном наборе стоимость задаётся целым числом рублей. Для денежных расчётов с копейками определите единицу хранения заранее: например, целое число копеек. Форматирование «1 500 ₽» относится к отображению; возвращать такую строку обратно в арифметику не нужно.
const money = new Intl.NumberFormat('ru-RU', {
style: 'currency', currency: 'RUB', maximumFractionDigits: 0
});
const summary = summarizeOrders(filteredRows);
totalElement.textContent = money.format(summary.total);
averageElement.textContent = summary.average === null
? 'Нет данных' : money.format(summary.average);
Подключение: HTML, локальные файлы или npm
Материал использует Tabler 1.7.3. Бесплатный основной пакет распространяется по MIT; при включении в проект сохраняйте лицензионные уведомления. Платные продукты Tabler и сторонние библиотеки имеют собственные условия. Для этой мастерской платные компоненты не требуются.
Самый быстрый старт — один HTML-файл. Следующий пример целиком можно сохранить как index.html и открыть в браузере при доступе к CDN. Кнопка меняет число собственной логикой JavaScript; оформление карточки и кнопки даёт Tabler.
<!doctype html>
<html lang="ru" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Первая карточка Tabler</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/core@1.7.3/dist/css/tabler.min.css">
</head>
<body>
<main class="container py-4">
<h1>Учебные заказы</h1>
<div class="card">
<div class="card-body">
<h2 class="card-title">Заказов в работе</h2>
<p class="display-6" id="count" aria-live="polite">0</p>
<button class="btn btn-primary" type="button" id="add">
Добавить учебный заказ
</button>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.7.3/dist/js/tabler.min.js"></script>
<script>
let count = 0;
document.getElementById('add').addEventListener('click', () => {
document.getElementById('count').textContent = String(++count);
});
</script>
</body>
</html>
В tabler.min.js уже включён JavaScript Bootstrap. Не подключайте рядом второй bootstrap.bundle.js: повторные обработчики способны создать два слоя затемнения или дважды открыть компонент. В обычном script-подключении программный API доступен через объект tabler.
Подробнее: подключение и включённый Bootstrap JavaScript.
При наличии сборщика можно установить закреплённую версию и импортировать нужный компонент. Этот вариант предполагает настроенный проект сборки; команда npm сама не создаёт страницу и сервер.
npm install @tabler/core@1.7.3
import '@tabler/core/dist/css/tabler.min.css';
import { Tooltip } from '@tabler/core';
const instances = [...document.querySelectorAll('[data-bs-toggle="tooltip"]')]
.map(element => new Tooltip(element));
// При удалении этого экрана из приложения:
function disposeTooltips() {
instances.forEach(instance => instance.dispose());
}
CDN удобен для первого знакомства. В скачиваемом проекте библиотеки лежат локально, поэтому работа интерфейса не зависит от стороннего CDN. При обновлении меняйте версию осознанно и проверяйте экран: сохранение названий классов не гарантирует неизменность каждой детали оформления.
Подробнее: лицензия основного пакета.
Собираем страницу вокруг действий пользователя
На первом экране нужны название рабочего пространства, понятный период и основные действия. Затем — показатели, график и список, который объясняет цифры. Настройки вида можно раскрывать отдельно: они нужны, но не должны постоянно отодвигать работу с заказами вниз.
Базовая сетка строится знакомыми классами Bootstrap. Например, две карточки занимают половину строки на широком экране, а на телефоне становятся друг под другом:
<div class="row g-3">
<div class="col-12 col-md-6">
<section class="card" aria-labelledby="orders-title">
<div class="card-body">
<h2 class="card-title" id="orders-title">Заказы</h2>
<p class="h1 mb-0" id="orders-value">—</p>
<p class="text-secondary mb-0">За выбранный период</p>
</div>
</section>
</div>
<div class="col-12 col-md-6">
<section class="card" aria-labelledby="amount-title">
<div class="card-body">
<h2 class="card-title" id="amount-title">Сумма заказов</h2>
<p class="h1 mb-0" id="amount-value">—</p>
<p class="text-secondary mb-0">Без учёта факта оплаты</p>
</div>
</section>
</div>
</div>
Сетка решает расположение, но не информационный приоритет. Если экран используют для обработки очереди, таблица и кнопка создания важнее декоративного графика. Если для анализа — период и сравнение должны быть заметны раньше списка. Один комплект компонентов допускает оба варианта без полной смены технологии.
Подробнее: структура страницы Tabler.
Один набор данных для всех представлений
Самая частая ошибка дашборда — отдельные массивы для карточек, графика и таблицы. Сначала они совпадают, а после редактирования расходятся. Проще хранить исходные записи и настройки, а остальные значения вычислять из них.
const state = {
orders: [],
filters: { days: 14, query: '', status: 'all', channel: 'all' },
table: { sort: 'date', direction: 'desc', page: 1, pageSize: 10 },
view: { theme: 'light', density: 'comfortable' }
};
// Порядок обработки:
// исходные записи → фильтры → KPI/график → сортировка → страница таблицы
// Пагинация таблицы не должна ограничивать набор для KPI.
В каждой записи нужны стабильный ID, дата, название, клиент, канал, статус и стоимость. ID не следует брать из номера строки на экране: после сортировки этот номер изменится. В редактор передавайте ID, а нужную запись находите в текущем наборе.
const order = {
id: 'DEMO-001',
date: '2026-11-06',
title: 'Настройка почты',
customer: 'Учебная компания',
channel: 'web',
status: 'new',
cost: 1500
};
Это иллюстрация структуры, а не обещание значений конкретной строки мастерской. Строки из API нужно проверять до расчёта: неизвестный статус, строка вместо числа или некорректная дата способны дать красивый, но неверный результат. Полезно отделить загрузку и проверку модели от функций отображения.
Фильтр должен менять весь контекст
Поиск по ID, названию и клиенту помогает быстро найти запись. Статус и канал сужают рабочую группу. Все условия объединяются: «В работе» и «Сайт» означают заказы, удовлетворяющие обоим фильтрам, а не объединение двух независимых списков.
function matchesFilters(row, filters) {
const query = filters.query.trim().toLocaleLowerCase('ru-RU');
const searchable = [row.id, row.title, row.customer]
.join(' ').toLocaleLowerCase('ru-RU');
return (!query || searchable.includes(query))
&& (filters.status === 'all' || row.status === filters.status)
&& (filters.channel === 'all' || row.channel === filters.channel);
}
После смены фильтра верните таблицу на первую страницу. Иначе пользователь может остаться на пятой странице выборки, в которой теперь всего три строки. Счётчик должен описывать весь найденный набор; подпись пагинации — текущий диапазон строк.
У поиска и фильтров должны быть видимые подписи. Placeholder исчезает после ввода и не заменяет название поля. Для частого сброса полезна одна понятная кнопка, возвращающая условия к исходному состоянию.
<div class="mb-3">
<label class="form-label" for="search">Поиск по заказам</label>
<input class="form-control" id="search" type="search"
placeholder="ID, название или клиент" aria-describedby="search-help">
<div class="form-hint" id="search-help">
Поиск применяется к показателям, графику и таблице.
</div>
</div>
Периоды и сравнение без случайных процентов
Сравнивать последние семь дней с предыдущими тридцатью обычно бессмысленно. Для изменения показателя выбирайте соседний период той же длины и применяйте одинаковые условия поиска, статуса и канала. В интерфейсе полезно показывать обе пары дат: слово «раньше» слишком неопределённо.
Для учебных календарных дат используем ISO-формат и UTC-арифметику. Это не универсальная замена часовому поясу бизнеса, но понятный способ избежать сдвига даты на день при разборе строк в браузерах.
function periodBounds(referenceDate, days) {
const day = 86400000;
const end = Date.parse(referenceDate + 'T00:00:00Z');
if (!Number.isFinite(end) || ![7, 14, 30].includes(days)) {
throw new Error('Неверный период');
}
const iso = time => new Date(time).toISOString().slice(0, 10);
const start = end - (days - 1) * day;
return {
current: { from: iso(start), to: iso(end) },
previous: { from: iso(start - days * day), to: iso(start - day) }
};
}
function inPeriod(row, period) {
return row.date >= period.from && row.date <= period.to;
}
Сравнение ISO-строк здесь работает для проверенных дат одинакового формата YYYY-MM-DD. Для событий с часами и минутами сначала определите часовой пояс, границы суток и поле времени. В реальной аналитике это часть определения показателя.
function relativeChange(current, previous) {
if (previous === 0) return null;
return (current - previous) / previous * 100;
}
const change = relativeChange(currentTotal, previousTotal);
changeElement.textContent = change === null
? 'Нет базы для сравнения'
: new Intl.NumberFormat('ru-RU', {
style: 'percent', maximumFractionDigits: 1,
signDisplay: 'always'
}).format(change / 100);
Когда предыдущая сумма равна нулю, нельзя показывать бесконечный рост или придумывать «+100%». Для изменения доли есть ещё одно различие: рост с 40% до 50% — это 10 процентных пунктов, а относительный рост составляет 25%. Подпись рядом с числом должна говорить, какой вариант рассчитан.
График объясняет показатели, а не живёт отдельно
В этой панели график строит Chart.js 4.6.1, подключённый отдельно. Tabler отвечает за карточку вокруг него. В официальных примерах Tabler встречается ApexCharts: это другая библиотека со своей лицензией, её наличие в примере не делает её частью MIT-лицензии Tabler.
В мастерской показано количество заказов по дням: сплошная линия — выбранный период, пунктирная — предыдущий. Сопоставляются соответствующие дни двух равных окон; точные даты есть в подсказке и раскрываемой таблице графика. Это позволяет сравнить поток обращений, сохранив связь с общим счётчиком.
Для временного ряда сначала создайте все дни выбранного периода, включая дни без заказов. Иначе график склеит разрывы и визуально исказит ритм работы. Ниже показан отдельный вариант для своего проекта — группировка суммы по дням. Она использует тот же отфильтрованный набор, но отвечает на другой вопрос, чем график количества в мастерской.
function totalsByDay(rows, from, to) {
const sums = new Map();
const step = 86400000;
for (let t = Date.parse(from + 'T00:00:00Z');
t <= Date.parse(to + 'T00:00:00Z'); t += step) {
sums.set(new Date(t).toISOString().slice(0, 10), 0);
}
rows.forEach(row => {
if (sums.has(row.date)) sums.set(row.date, sums.get(row.date) + row.cost);
});
return [...sums].map(([date, total]) => ({ date, total }));
}
<div style="position:relative;height:280px">
<canvas id="orders-chart" role="img"
aria-label="Сумма выбранных заказов по дням"></canvas>
</div>
<p>Точные записи доступны в таблице заказов ниже.</p>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.6.1/dist/chart.umd.min.js"></script>
// series — результат totalsByDay для текущей выборки.
const chart = new Chart(document.getElementById('orders-chart'), {
type: 'bar',
data: {
labels: series.map(item => item.date),
datasets: [{ label: 'Сумма, ₽', data: series.map(item => item.total),
backgroundColor: '#0f766e' }]
},
options: { responsive: true, maintainAspectRatio: false,
scales: { y: { beginAtZero: true } } }
});
function updateSeries(nextSeries) {
chart.data.labels = nextSeries.map(item => item.date);
chart.data.datasets[0].data = nextSeries.map(item => item.total);
chart.update('none');
}
Обновляйте существующий экземпляр вместо создания нового на каждом вводе символа. Если экран действительно удаляется, вызовите chart.destroy() перед повторным использованием canvas. Для тёмной темы отдельно обновляйте цвет подписей и сетки: уже нарисованные пиксели canvas не перекрашиваются только от CSS-класса страницы.
Таблица и текстовые подписи остаются необходимыми: одного aria-label у canvas недостаточно, чтобы передать все значения. Подробные варианты графиков, обновление данных и экспорт разобраны в мастерской Chart.js.
Подробнее: update и destroy в Chart.js.
Подробнее: отдельная графическая библиотека в примерах Tabler.
Таблица: от общей цифры к конкретной записи
Таблица — место, где пользователь проверяет происхождение показателя и действует. Статус нужен текстом, стоимость — с единицей, а редактор — по понятной кнопке. Цвет можно добавить как вторую подсказку, но «зелёная точка» без подписи не объясняет состояние.
Порядок обработки важен: сначала отбор, затем сортировка, затем страница. Если сортировать только десять отображённых строк, пользователь получит частичную сортировку, которая не соответствует ожиданию от заголовка таблицы.
function sortedPage(filteredRows, { page, pageSize, direction }) {
const sign = direction === 'asc' ? 1 : -1;
const sorted = [...filteredRows].sort((a, b) =>
sign * (a.cost - b.cost) || a.id.localeCompare(b.id)
);
const pages = Math.max(1, Math.ceil(sorted.length / pageSize));
const safePage = Math.min(Math.max(1, page), pages);
return {
rows: sorted.slice((safePage - 1) * pageSize, safePage * pageSize),
page: safePage, pages, total: sorted.length
};
}
Выводите пользовательские строки через textContent, а не собирайте HTML из названия заказа. Даже внутренний сервис может получать данные из формы, файла или внешней системы.
const cell = document.createElement('td');
cell.textContent = order.title;
const edit = document.createElement('button');
edit.type = 'button';
edit.className = 'btn btn-sm btn-outline-primary';
edit.textContent = 'Изменить';
edit.setAttribute('aria-label', 'Изменить заказ ' + order.id);
edit.addEventListener('click', () => openEditor(order.id));
Для небольшого набора собственного кода достаточно. Если нужны виртуализация большого списка, группировки и развитое редактирование ячеек, имеет смысл выделить таблицу в специализированный компонент. В статье о Tabulator можно сравнить такой подход с обычной таблицей внутри Tabler.
Подробнее: табличные компоненты и оформление.
Создание и редактирование без потери контекста
В мастерской редактор расположен внутри страницы. Это удобно для длинной формы на телефоне: поле, пояснение и ошибка остаются в обычном потоке. После открытия пользователь должен попасть к названию формы или первому полю, а после сохранения — получить сообщение о результате.
Не меняйте исходную запись после каждого нажатия клавиши, если рядом есть кнопки «Сохранить» и «Отмена». Работайте с черновиком и применяйте его только после проверки. Иначе отмена будет закрывать форму, но не отменять уже сделанные изменения.
let draft = null;
function beginEdit(order) {
draft = { ...order };
// Заполнить поля значениями draft.
}
function cancelEdit() {
draft = null;
// Закрыть редактор, исходный массив orders не менялся.
}
Пустое числовое поле нельзя бездумно преобразовывать через Number(): Number('') даёт ноль. Сначала отделите отсутствие значения от настоящего нуля, затем проверяйте диапазон и формат.
function parseCost(raw) {
const text = String(raw).trim();
if (!/^\d+$/.test(text)) return { error: 'Введите сумму целыми рублями' };
const value = Number(text);
if (!Number.isSafeInteger(value) || value < 0 || value > 10000000) {
return { error: 'Сумма должна быть от 0 до 10 000 000 ₽' };
}
return { value };
}
Этот диапазон соответствует учебному API: целые рубли от 0 до 10 000 000. Название содержит 3–120 символов, клиент — 2–80; канал и статус выбираются из разрешённых значений. Ошибку связывайте с полем и показывайте словами, а не только красной рамкой. Сервер повторяет проверку независимо от браузера.
<label class="form-label" for="order-title">Название заказа</label>
<input class="form-control is-invalid" id="order-title"
aria-invalid="true" aria-describedby="title-error">
<div class="invalid-feedback" id="title-error">
Укажите понятное название заказа.
</div>
Подробнее: оформление состояния ошибки формы.
Загрузка, пустая выборка и ошибка — три разных состояния
Пока идёт запрос, действие сохранения должно быть защищено от повторного запуска. После неудачи введённые данные стоит сохранить в форме, чтобы человек мог исправить причину или повторить попытку. Сброс заполненных полей из-за временного сбоя — одна из самых неприятных ошибок интерфейса.
Пустая выборка должна подсказывать следующий шаг: сбросить фильтры или создать заказ. Ошибка загрузки должна предлагать повторить запрос. Старые данные можно оставить только с явной отметкой, что они не обновлены; показывать их как свежие после сбоя нельзя.
<section class="empty" aria-labelledby="empty-title">
<h2 class="empty-title" id="empty-title">Заказы не найдены</h2>
<p class="empty-subtitle text-secondary">
Попробуйте другой период или сбросьте условия поиска.
</p>
<div class="empty-action">
<button type="button" class="btn btn-primary" id="reset-filters">
Сбросить фильтры
</button>
</div>
</section>
Учебная кнопка ошибки в публичном дашборде управляемо показывает состояние интерфейса. Она не доказывает работу сети или PHP. Настоящее поведение API проверяется в скачанном проекте, где запросы действительно проходят через сервер.
Подробнее: компонент пустого состояния.
Мини-мастерская: тема, плотность и карточки
Настройки полезны, когда меняют способ работы. Плотный список подходит для просмотра множества строк, свободный — для чтения и редактирования. Скрытие ненужной карточки уменьшает шум, но не должно менять формулы оставшихся показателей.
Базовое переключение цветовой схемы использует атрибут на корневом элементе. В этом простом примере приложение само управляет настройкой; отдельный tabler-theme.js параллельно не нужен.
function applyTheme(theme) {
const safeTheme = theme === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.bsTheme = safeTheme;
try { localStorage.setItem('my-dashboard-theme', safeTheme); } catch {}
}
let storedTheme = 'light';
try { storedTheme = localStorage.getItem('my-dashboard-theme') || 'light'; } catch {}
applyTheme(storedTheme);
Если используете штатный скрипт темы Tabler, следуйте его порядку подключения и API. Не запускайте два независимых механизма, которые спорят за один атрибут. Хранилище браузера также может быть недоступно: невозможность сохранить тему не должна ломать весь сервис.
/* Свои настройки поверх Tabler, без изменения vendor-файла. */
:root {
--tblr-primary: #0f766e;
--tblr-primary-fg: #fff;
}
.dashboard[data-density="compact"] .orders-table td {
padding-top: .45rem;
padding-bottom: .45rem;
}
@media (max-width: 600px) {
.dashboard input, .dashboard select, .dashboard textarea { font-size: 16px; }
}
Тёмная тема требует проверки всего экрана: текста в полях, пустых состояний, ошибок, границ таблицы и графика. Замена фона без обновления вспомогательных цветов обычно оставляет часть интерфейса нечитаемой. Плотность не должна уменьшать сенсорные действия до неудобных маленьких целей.
Подробнее: цветовые режимы Tabler.
Подробнее: CSS-переменные и настройка оформления.
Экспорт: выгружайте именно понятный набор
Перед кнопкой CSV нужно определить область выгрузки. Пользователь обычно ожидает все строки, соответствующие текущим фильтрам, даже если таблица показывает только первую десятку. Выгрузку одной страницы следует называть явно. Сортировку разумно сохранить, чтобы файл соответствовал экрану.
CSV требует двух разных действий: экранирования разделителей и защиты текстовых ячеек от интерпретации как формул. Одни кавычки вокруг значения вторую задачу не решают. Следующий пример для текстовых колонок добавляет апостроф перед потенциальной формулой, затем экранирует кавычки.
function csvTextCell(value) {
let text = String(value ?? '');
if (/^[\s\uFEFF]*[=+@-]/.test(text) || /^[\t\r\n]/.test(text)) {
text = "'" + text;
}
return '"' + text.replaceAll('"', '""') + '"';
}
const csv = '\uFEFF' + [
['ID', 'Название', 'Клиент', 'Стоимость, ₽'],
...filteredRows.map(row => [row.id, row.title, row.customer, row.cost])
].map(row => row.map(csvTextCell).join(';')).join('\r\n');
Для конкретного табличного редактора проверяйте результат открытия файла: обработка типов и формул может отличаться. JSON удобен для структуры настроек и данных, а CSV — для плоского списка. Это разные форматы с разным назначением.
Экспорт исходного JavaScript помогает разобрать реализацию, но не является обещанием, что один скрипт заменяет весь проект. Для воспроизведения панели нужны HTML, стили, библиотеки и данные. Поэтому полный набор собран в ZIP.
Подключение PHP: интерфейс не должен угадывать результат
Когда данные приходят из сервера, браузер должен различать успешный ответ, ошибки полей и недоступность запроса. Положительный HTTP-ответ сам по себе ещё не гарантирует ожидаемую структуру JSON. Начинайте с явного контракта: список полей, типы, идентификаторы и формат ошибок.
Ниже — самостоятельный фрагмент чтения для API, возвращающего объект с массивом orders. Он показывает проверку HTTP-статуса и защиту от запоздавшего ответа. Это приём интеграции; точный контракт готового проекта находится в его README.
let requestNumber = 0;
let controller = null;
async function loadOrders(url) {
const current = ++requestNumber;
controller?.abort();
controller = new AbortController();
try {
const response = await fetch(url, {
signal: controller.signal,
credentials: 'same-origin',
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const body = await response.json();
if (!Array.isArray(body.orders)) throw new Error('Некорректный ответ API');
if (current !== requestNumber) return;
return body.orders; // Далее — проверка структуры каждой строки.
} catch (error) {
if (error.name !== 'AbortError' && current === requestNumber) throw error;
}
}
Отмена чтения и отмена записи — разные вещи. Если браузер перестал ждать ответ сохранения, сервер мог уже выполнить действие. Для повторов нужны согласованные идентификаторы, версии или ключ идемпотентности. Нельзя создавать ещё один заказ автоматически при каждой сетевой ошибке.
PHP может отдавать данные отдельным JSON-маршрутом или безопасно встраивать начальные данные в HTML. Во втором случае пользовательский текст не должен закрыть тег script.
<?php
declare(strict_types=1);
$initial = ['orders' => [
['id' => 'DEMO-001', 'title' => 'Проверка формы', 'cost' => 1500]
]];
?>
<script type="application/json" id="initial-data"><?=
json_encode($initial, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
| JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)
?></script>
<script>
const initial = JSON.parse(document.getElementById('initial-data').textContent);
</script>
Экранирование вывода не заменяет проверку данных и прав доступа. Сервис должен отдельно решать, кто может видеть заказ, менять сумму или выгружать список. Tabler оформляет эти действия; ограничения выполняет сервер.
Точный запрос к API из архива
Локальный GET ./api.php возвращает заказы, текущую revision и CSRF-токен. Создание передаётся POST-запросом с operation: create, обновление — с operation: update, ID и объектом changes. Ниже полный пример создания для консоли локальной страницы; на сайте этот API не развёрнут.
async function createDemoOrder() {
const initialResponse = await fetch('./api.php', { credentials: 'same-origin' });
if (!initialResponse.ok) throw new Error('Не удалось прочитать состояние');
const initial = await initialResponse.json();
const response = await fetch('./api.php', {
method: 'POST', credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': initial.csrf
},
body: JSON.stringify({
operation: 'create', revision: initial.revision,
order: { date: '2026-11-06', title: 'Настройка формы',
customer: 'Команда Маяк', channel: 'web', status: 'new', cost: 4500 }
})
});
const result = await response.json();
if (!response.ok) {
const error = new Error(result.error?.message || 'Сохранение не выполнено');
error.status = response.status;
error.fields = result.error?.fields || {};
throw error;
}
return result;
}
createDemoOrder().then(console.log).catch(console.error);
Стоимость передаётся JSON-числом: строка "4500" не принимается. При HTTP 422 API возвращает ошибки полей; при устаревшей ревизии — HTTP 409 и актуальное состояние. Не повторяйте запись автоматически поверх этого состояния: сохраните черновик, объясните конфликт и дайте пользователю решить, какие изменения применить.
Данные локальной cookie-сессии лежат в var/sessions вне публичного каталога. Разные сессии независимы, а обновление страницы в той же сессии позволяет проверить сохранение. Это файловый учебный стенд с ограничением доступа localhost, без пользователей и ролей. CSRF-токен и проверка ревизии решают свои задачи, но не создают авторизацию рабочего сервиса.
Исходники и запуск настоящего локального проекта
Архив содержит панель, данные, локальные библиотеки и PHP-часть. Публичный пример работает в браузере; локальный проект нужен для проверки реального обмена с API. Все записи учебные, поэтому можно спокойно пройти создание, изменение и восстановление начального набора.
Скачать исходники Tabler-дашборда и PHP-проект.
Нужен PHP 8.2+. Composer, Node.js и сборка для готового архива не требуются. Распакуйте его и в корне проекта выполните:
php -d display_errors=0 -S 127.0.0.1:8080 -t public
Откройте http://127.0.0.1:8080/. Встроенный сервер запускается только локально; остановка — Ctrl+C. Параметр -t public оставляет данные и PHP-классы вне веб-корня. Файлы библиотек уже включены, поэтому обычная работа проекта не требует CDN.
В отдельном терминале можно проверить PHP-классы, затем запустить пример создания и изменения записи. Последняя команда проверяет настоящий HTTP API при работающем сервере и требует Python 3 без дополнительных пакетов:
php tests/run.php
php bin/demo.php
python tests/http_test.py http://127.0.0.1:8080/api.php
CLI-пример создаёт отдельный каталог var/cli-demo-*, добавляет заказ и меняет его статус. Он помогает изучить серверные вызовы без интерфейса. HTTP-тест использует собственные cookie-сессии. Подробный формат запросов находится в API.md, а устройство проекта и границы — в README.md.
Успех локальной демонстрации не означает, что готова рабочая CRM: подключение пользователей, базы, резервного копирования и правил доступа остаётся частью внедрения.
Полезная первая доработка — заменить синтетический источник одним реальным списком из вашего сервиса, сохранив контракт интерфейса. Проверьте одну запись, пустой набор, ошибку полей и сбой запроса. После этого переносите остальные действия, не меняя одновременно все уровни приложения.
Что проверить на телефоне и с клавиатуры
Адаптивная сетка — начало проверки. На узком экране важно, чтобы оставалось понятно, к какой записи относится сумма, что делает кнопка и куда исчез редактор после сохранения. Длинное название, крупная сумма и сообщение об ошибке часто обнаруживают проблемы, которых нет на аккуратных коротких демоданных.
Для небольшой рабочей таблицы удобны карточки строк с подписями полей; для широкой сравнительной таблицы может быть оправдана горизонтальная прокрутка. Не скрывайте важный столбец без другого способа получить его значение. Выбранный подход должен соответствовать действию, ради которого человек открыл экран.
Проверьте порядок Tab, Enter на кнопках, переход к ошибке поля, возврат после закрытия редактора и видимую рамку фокуса. Состояние должно обозначаться текстом, а график иметь доступную альтернативу. Bootstrap и Tabler предоставляют компоненты, но итоговая доступность зависит от разметки и поведения приложения.
Подробнее: ответственность приложения за доступность.
Ошибки, которые делают дашборд недостоверным
| Симптом | Что произошло | Как исправить |
|---|---|---|
| KPI меняется при переходе на страницу 2 | Итог посчитан только по видимым строкам | Считать по всей отфильтрованной выборке |
| Поиск обновляет таблицу, график прежний | У блоков разные источники состояния | Вычислять представления из одного отбора |
| Рост показывает Infinity | Прошлый показатель равен нулю | Показать отсутствие базы сравнения |
| Отмена редактора всё же меняет строку | Поля редактируют исходный объект | Использовать отдельный черновик |
| Два затемнения при открытии диалога | Bootstrap JS загружен дважды | Оставить один механизм компонентов |
| После сбоя всё выглядит свежим | Старая выборка не отмечена | Показать ошибку и состояние актуальности |
| Тёмная тема оставила светлый график | Canvas не обновлён вместе с темой | Обновить его цвета и экземпляр |
| CSV содержит только 10 строк | Экспортирована текущая страница | Явно выбрать область экспорта |
Развивать такую панель удобно от сценария, а не от количества виджетов. Сначала сделайте достоверный общий срез, переход к записи и безопасное сохранение. Затем добавляйте уведомления, сложные таблицы и дополнительные страницы. Для собственных меню и всплывающих панелей пригодится практикум Floating UI, а основу сетки можно исследовать в конструкторе Bootstrap 5.
Документация и материалы для продолжения
- Tabler UI — компоненты, структура страниц и установка.
- Релиз Tabler 1.7.3 — версия, использованная в материале.
- Цветовые режимы и настройка оформления.
- Таблицы, ошибки форм и пустые состояния.
- Chart.js — графики и работа с их данными.
- Tabulator — более сложные сценарии таблиц.
- Подборка KPI-дашбордов ThemeSelection — ориентир по композиции и типам панелей; технические примеры этой статьи основаны на документации используемых библиотек.


