PrimeVue: рабочий кабинет на Vue — мастерская и 8 живых интерфейсов
Связанный кабинет на Vue и PrimeVue: восемь рабочих экранов, настоящая загрузка JSON, таблицы и формы, понятные ошибки и исходники для Windows/Linux.

PrimeVue: из компонентов получается рабочий кабинет
В административной панели повторяются десятки небольших операций: найти заявку, отметить несколько строк, исправить клиента, назначить дату, выбрать услуги и увидеть результат сохранения. Если каждый такой элемент писать заново, значительная часть времени уходит на календарь, выпадающий список, состояния кнопок и поведение всплывающих панелей. При этом пользователю нужен понятный рабочий процесс, а не демонстрация набора красивых прямоугольников.
PrimeVue — библиотека компонентов для Vue, из которой можно собрать такой интерфейс. В ней есть DataTable, Drawer, DatePicker, MultiSelect, Tree, Stepper, сообщения, индикаторы и многие другие элементы. В этой статье они работают вместе: восемь экранов используют одни заявки, изменения отражаются в таблице и сводке, а новый заказ появляется в общей очереди.
Результат можно попробовать прямо здесь. Найдите компанию, измените статус, откройте карточку, создайте заказ, загрузите неправильный ответ и восстановите работу. Затем скачайте весь проект: в нём лежат реальные Vue-компоненты, набор данных, проверяемые функции, тесты и инструкции для Windows, Linux и размещения на VPS.
Где применять: сервисная компания, поддержка, учёт и каталог
Первый подходящий сценарий — внутренний кабинет небольшой команды. Менеджер видит очередь заявок, оператор меняет статусы, руководитель смотрит сводку по офисам. Здесь особенно полезны таблица, массовый выбор, небольшая форма и боковая карточка. Внешний вид можно привести к стилю компании, сохранив знакомые элементы управления на разных страницах.
Второй сценарий — пользовательский кабинет сервиса: создание обращения, выбор пакета работ, запись на дату, просмотр истории. В каталоге товаров те же приёмы превращаются в фильтры, выбор категории и редактирование позиции. В системе учёта оборудования дерево представляет площадки и подразделения, а таблица — связанные объекты.
Начинать лучше с одной операции, которую человек выполняет часто. Например, сократить путь от списка обращений до исправления контактного имени. Сначала определите нужные данные и итог действия, затем выберите компоненты. Большое количество доступных элементов не означает, что на первом экране нужно показать их все. Хороший кабинет помогает принять решение и завершить задачу без лишних переходов.
Версии и лицензия: почему в проекте PrimeVue 4.5.5
На момент подготовки материала актуальная npm-ветка latest уже указывает на PrimeVue 5.0.5. У неё изменилась модель лицензирования: официальное руководство описывает Community/Commercial, а установка предусматривает ключ лицензии. Поэтому утверждение «любая последняя PrimeVue распространяется по MIT» здесь было бы неверным.
Наша мастерская сознательно закрепляет PrimeVue 4.5.5 из ветки v4-stable и тему @primeuix/themes 2.0.3. У этих конкретных версий лицензия MIT проверена по пакетам. Vue закреплён на 3.5.45, Bootstrap — на 5.3.9. Прямые версии записаны без диапазонов, транзитивные зависимости — в package-lock.json. Платные шаблоны, PrimeBlocks и PRO-компоненты в проект не входят.
Это полезный пример выбора зависимости по условиям проекта. Мы сохраняем готовый свободно распространяемый учебный кабинет, а не предлагаем автоматически обновлять его командой npm update. Перед переходом на другую основную версию отдельно проверьте лицензию, API и поведение компонентов. Ссылки: установка v4, MIT для 4.5.5 и официальная миграция на v5.
Фрагмент файла package.json
{
"name": "aen-primevue-workshop",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=24.15"
},
"scripts": {
"build": "vite build && node tools/finish-build.mjs",
"start": "node tools/serve.mjs",
"test": "node --test tests/*.test.mjs"
},
"dependencies": {
"primevue": "4.5.5",
"@primeuix/themes": "2.0.3",
"vue": "3.5.45",
"bootstrap": "5.3.9"
},
"devDependencies": {
"vite": "8.3.8",
"@vitejs/plugin-vue": "6.0.11",
"jsdom": "31.0.1"
}
}
Мини-мастерская: восемь живых экранов и общие данные
Выберите сценарий в верхнем списке. Под заголовком есть практическое задание и объяснение, в каком интерфейсе пригодится этот приём. Все восемь экранов используют общий массив заявок. Поэтому сохранение карточки видно в таблице, новый заказ меняет число записей и сумму, а завершение работы увеличивает показатель прогресса.
Начальный набор состоит из двадцати четырёх вымышленных заявок трёх офисов. Он действительно загружается по HTTP из JSON на этом же сайте. После загрузки редактирование выполняется в памяти браузерной вкладки: мастерская не делает POST/PUT, не сохраняет изменения в базе, не принимает оплату и не отправляет сообщения клиентам. Эта граница обозначена непосредственно в интерфейсе.
Для длительного изучения откройте отдельную вкладку: всплывающие панели внутри iframe ограничены документом мастерской. Внизу можно выбрать исходный файл, скопировать его, скачать отдельно или получить весь ZIP. Экспорт текущих заявок создаёт JSON-файл с вашим учебным состоянием. Перезагрузка страницы вернёт исходный набор.
Быстрый запуск в Windows: готовая сборка и разработка
В архив уже включена готовая папка public. Чтобы просто открыть мастерскую, установите Node.js 24 LTS, распакуйте проект и выполните npm start из его корневой папки. Откройте адрес http://127.0.0.1:8530. Сервер привязан к loopback и предназначен для локального просмотра. Для остановки нажмите Ctrl+C в той же консоли.
Двойной щелчок по index.html не заменяет HTTP-сервер: браузер ограничивает загрузку модулей и fetch через file://. Для просмотра готового public установка npm-зависимостей не нужна. Для изменения Vue-кода сначала выполните npm ci –ignore-scripts, затем сборку и тесты. npm ci читает lock-файл и не выбирает новые версии самостоятельно.
Если порт занят другим приложением, задайте другой PORT в PowerShell и открывайте соответствующий адрес. Не останавливайте незнакомый процесс ради учебного примера. В production этот встроенный сервер не требуется: готовые файлы раздаёт обычный веб-сервер, а Node остаётся инструментом сборки.
Фрагмент файла README.md
npm ci --ignore-scripts
npm run build
npm test
npm start
Linux и воспроизводимая сборка в контейнере
На Linux последовательность такая же: Node 24 LTS, npm ci, build, test. Все пути внутри исходников относительные, команды проверки не зависят от букв Windows-дисков. Для локального сервера переменная задаётся перед командой: PORT=8530 npm start. Привязка к 127.0.0.1 остаётся полезной: учебный стенд не становится общедоступным случайно.
Изолированный контейнер удобен, когда на сервере или рабочей машине установлена другая версия Node. В README приведён запуск node:24.22.2-bookworm-slim. Исходники монтируются только для чтения, копируются во временную папку контейнера без node_modules и public, после чего зависимости устанавливаются заново и выполняется чистая сборка. Опция –rm удаляет только этот завершённый контейнер.
В проверке проекта Windows и Linux прошли двенадцать тестов. Это проверка исполнения и сборки в двух средах; она не означает, что внешний VPS, домен или сертификат уже настроены. Для каждого реального размещения нужны отдельные команды проверки HTTP и осмотр страницы на конечном адресе.
Фрагмент файла README.md
node --version
npm ci --ignore-scripts
npm run build
npm test
PORT=8530 npm start
Как устроено приложение: компоненты, модель и API отдельно
В src/App.vue находится связь интерфейсов и общего состояния Vue. Модель в src/model.mjs умеет проверять данные, фильтровать заявки, считать показатели и создавать заказ. src/api.mjs отвечает только за загрузку разрешённого JSON и проверку ответа. Такое разделение позволяет проверить правила без браузера и не прятать вычисление суммы внутрь шаблона кнопки.
В src/main.js подключаются PrimeVue, тема Aura, сервисы подтверждения и уведомлений, русские названия календаря и доступные подписи. Bootstrap 5 используется для оболочки и базовых стилей. Его управляющий JavaScript не подключается: один Drawer должен иметь один механизм управления, а не одновременно PrimeVue и Bootstrap Modal.
Готовый проект не является обязательной архитектурой большой CRM. В рабочем приложении отдельные экраны разумно вынести в компоненты, состояние сервера — в подходящий слой запросов, а повторяющиеся поля — в собственную обвязку. Здесь общая структура специально остаётся обозримой, чтобы было понятно, откуда появляется каждое значение и какая операция его меняет.
Фрагмент файла vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
base: "./",
publicDir: false,
build: {
outDir: "public",
emptyOutDir: true,
target: "es2022",
chunkSizeWarningLimit: 1100,
},
});
Экран 1. Таблица заявок: найти, сузить и отсортировать
Начните с поиска «Маяк»: в исходном наборе совпадут четыре записи. Поиск учитывает ID, название клиента и задачу, убирает крайние пробелы и не зависит от регистра. Затем выберите статус. Эти условия соединяются, поэтому пользователь видит именно пересечение запроса и статуса, а не случайную смесь двух разных списков.
Сортировка и пагинация выполняются настоящим DataTable. Заголовок суммы позволяет менять порядок, а нижние кнопки показывают по пять строк. Если совпадений нет, появляется объяснение и действие «Показать все заявки». Это важнее красивого пустого блока: человек понимает, что данные не исчезли и как вернуть привычный список.
В примере всего двадцать четыре записи, поэтому поиск выполняется локальной функцией. На большом каталоге фильтрация, сортировка и пагинация должны переехать на сервер. Компонент таблицы умеет показывать данные, но сам по себе не делает скачивание всей клиентской базы эффективным. Возможности таблицы перечислены в документации DataTable v4.
Фрагмент файла src/model.mjs
export function filterRequests(
rows,
{ search = "", status = "all", department = "all" } = {},
) {
const q = search.trim().toLocaleLowerCase("ru");
return rows.filter(
(r) =>
(status === "all" || r.status === status) &&
(department === "all" || r.department === department) &&
(!q ||
`${r.id} ${r.client} ${r.title}`.toLocaleLowerCase("ru").includes(q)),
);
}
Мобильная таблица: локальная прокрутка и сохранённый смысл
Таблица часто содержит больше данных, чем помещается на телефоне. Если просто уменьшить весь текст до очень мелкого размера, читать и выбирать строки будет неудобно. Если скрыть половину полей без объяснения, человек может не увидеть сумму или статус, на основании которых принимает решение.
В мастерской таблица сохраняет колонки и прокручивается по горизонтали внутри собственной рамки. Вся страница при этом не должна расширяться. Рядом есть понятная подсказка, а область получает tabindex и доступное имя, чтобы к ней можно было перейти клавиатурой. Пагинация ограничивает вертикальную длину списка и остаётся рядом с данными.
Для другого продукта может лучше подойти карточное представление, как на третьем экране. Выбирайте форму по задаче: таблица удобна для сравнения значений между строками, карточка — для просмотра и редактирования одной записи. Проверять нужно реальное встроенное место, включая узкий iframe, а не только полноэкранную страницу на широком мониторе.
Экран 2. Массовое действие: сначала выбор, затем подтверждение
Отметьте несколько строк checkbox и посмотрите на счётчик выбранных заявок. Checkbox заголовка относится к текущей странице, а не ко всей базе сервиса. Затем выберите новый статус и нажмите «Изменить выбранные». ConfirmDialog покажет конкретные ID и итоговый статус до применения операции.
Такой порядок нужен там, где ошибка массового действия дороже одного дополнительного нажатия. Подтверждение должно сообщать предмет операции: сколько заявок, какие записи и что изменится. Универсальное «Вы уверены?» без контекста почти не помогает. Отмена закрывает окно и оставляет данные прежними, подтверждение обновляет выбранные записи и очищает выбор.
При изменении фильтра выбор также сбрасывается. Это осознанная политика демо, исключающая неожиданное изменение скрытых строк. В настоящем продукте можно сохранять межстраничный выбор, но тогда его охват должен быть явно обозначен, а сервер — проверять разрешения для каждого переданного ID. Клиентский checkbox не является проверкой права изменять запись.
Фрагмент файла src/App.vue
function bulkApply() {
const ids = selection.value.map((r) => r.id);
if (!ids.length) return;
confirm.require({
header: "Изменить выбранные заявки?",
message: `${ids.join(", ")} → «${labelStatus(bulkStatus.value)}». Только память вкладки.`,
acceptLabel: "Применить статус",
rejectLabel: "Отмена",
accept: () => {
rows.value = changeStatus(rows.value, ids, bulkStatus.value);
selection.value = [];
announce(
`Статус «${labelStatus(bulkStatus.value)}» применён к ${ids.length} заявкам.`,
);
},
});
}
Стабильный ID важнее позиции в массиве
После сортировки первая строка может стать последней. После фильтрации индексы вообще относятся к другому списку. Поэтому выбор, редактирование и массовое действие привязываются к стабильному id, а не к номеру строки. В DataTable для этого задан dataKey, а функция изменения статусов получает именно список ID.
Функция создаёт новый массив и новые объекты только для затронутых заявок. Исходный набор не изменяется неожиданно внутри вычисления. Такой код удобно проверить: выбранные ID поменяли статус, остальные записи сохранились, неизвестное значение статуса отклонено. Ошибку можно воспроизвести без кликов по всей странице.
Учебный генератор выдаёт последовательные REQ-1025 и далее. Это удобная видимая идентичность в одной вкладке, а не распределённый генератор для нескольких серверов. В рабочей системе ID выдаёт сервер или согласованная схема идентификаторов. После успешного POST интерфейс должен использовать ID из ответа, а не считать количество загруженных строк.
Фрагмент файла src/model.mjs
export function changeStatus(rows, ids, status) {
if (!STATUSES.some((s) => s.value === status))
throw new Error("Неизвестный статус");
const chosen = new Set(ids);
return rows.map((r) => (chosen.has(r.id) ? { ...r, status } : r));
}
Экран 3. Drawer: редактирование без потери списка
На третьем экране заявки представлены карточками. Кнопка «Редактировать REQ-1001» содержит идентификатор, поэтому человек заранее понимает, какую запись откроет. Боковая панель показывает клиента, задачу и статус. При открытии значения копируются в отдельный черновик; набор текста ещё не меняет карточку за панелью.
Очистите название клиента и попробуйте сохранить. Панель останется открытой, рядом с полем появится конкретная ошибка, фокус вернётся в поле. Введите корректное название: карточка и общая таблица обновятся после сохранения. Затем откройте её снова, измените текст и нажмите «Отмена» — прежнее значение сохранится.
Drawer подходит для небольших изменений с опорой на контекст списка. Если форма занимает много разделов и требует длительного согласования, отдельная страница может быть удобнее. На телефоне ширина панели ограничена размером окна. Escape закрывает её; возврат фокуса к открывшей кнопке позволяет продолжить работу клавиатурой. Базовое поведение описано в руководстве Drawer.
Фрагмент файла src/App.vue
async function saveCard() {
cardErrors.value = validateCard(draft.value);
if (Object.keys(cardErrors.value).length) {
await nextTick();
document
.getElementById(`card-${Object.keys(cardErrors.value)[0]}`)
?.focus();
return;
}
rows.value = rows.value.map((r) =>
r.id === draft.value.id
? {
...r,
client: draft.value.client.trim(),
title: draft.value.title.trim(),
status: draft.value.status,
}
: r,
);
drawer.value = false;
announce(`${draft.value.id}: карточка сохранена в памяти вкладки.`);
}
Черновик, локальное сохранение и подтверждение сервера
В мастерской «Сохранить карточку» означает запись в память вкладки. Это сказано прямо внутри панели. Такой сценарий хорошо показывает механику компонента, но его нельзя выдавать за готовое серверное редактирование. Кнопка не отправляет HTTP-запрос на изменение и не переживает перезагрузку страницы.
При подключении настоящего API появятся дополнительные состояния: запрос отправлен, сервер подтвердил, сервер вернул ошибки полей, соединение потеряно, версия записи устарела. Закрывать панель сразу после отправки обычно неудобно: при отказе человек теряет контекст. Сначала получите подтверждённый объект, обновите основное состояние и только затем закройте форму.
На время запроса защититесь от повторной отправки. Для конфликта версии предложите понятный выбор, не затирайте чужую работу молча. Проверку прав и допустимых статусов обязательно повторяет сервер. Если такие сценарии становятся центральными для проекта, полезно отдельно изучить управление данными API через TanStack Query и состояния форм с TanStack Form.
Фрагмент файла src/model.mjs
export function validateCard(draft) {
const errors = {};
if (
!draft.client?.trim() ||
draft.client.trim().length < 2 ||
draft.client.trim().length > 80
)
errors.client = "Название клиента: от 2 до 80 символов.";
if (
!draft.title?.trim() ||
draft.title.trim().length < 2 ||
draft.title.trim().length > 120
)
errors.title = "Название задачи: от 2 до 120 символов.";
if (!STATUSES.some((s) => s.value === draft.status))
errors.status = "Выберите известный статус.";
return errors;
}
Экран 4. Форма: DatePicker, MultiSelect и живой расчёт
Форма собирает клиента, дату выполнения, набор услуг и офис. Выберите две услуги: итоговая стоимость пересчитается сразу, а в правой карточке появятся составляющие заказа. Это помогает проверить выбор до отправки. После «Добавить заявку» создаётся новая запись, которая видна в таблице, дереве и общей сводке.
MultiSelect хранит стабильные ID услуг. Названия и цены берутся из отдельного справочника, поэтому переименование подписи не ломает вычисления. Выбранный набор проходит проверку: нельзя передать неизвестную услугу, повторить один ID дважды или отправить пустой список. На реальном сервере цены следует заново получить из доверенного каталога, а не принимать итоговую сумму от браузера.
DatePicker выбирает календарный день. Минимальная дата относится к явно показанному учебному срезу 13.11.2026, поэтому пример воспроизводим и не зависит от сегодняшних часов читателя. Для реальной записи на услуги доступные даты определяются расписанием и часовым поясом организации. Документация: DatePicker и MultiSelect.
Фрагмент файла src/model.mjs
export function orderTotal(ids) {
return SERVICES.filter((s) => ids.includes(s.id)).reduce(
(sum, s) => sum + s.price,
0,
);
}
Календарный день, деньги и ошибки формы
Дата визита и момент времени — разные данные. Если требуется «16 ноября», бездумное преобразование через toISOString может перенести день при переходе в UTC. В модели берутся локальные год, месяц и число выбранного календаря и формируется строка YYYY-MM-DD. Это сохраняет смысл выбранного дня; время визита и зона здесь вообще не моделируются.
Стоимость услуг хранится целыми копейками. Суммирование поэтому не зависит от двоичного представления десятичных дробей. Форматирование в рубли выполняется только для отображения. Все учебные цены целорублёвые, поэтому форматтер скрывает копейки; при ценах с дробной частью измените настройки отображения, сохранив целочисленную внутреннюю модель.
Ошибки привязаны к конкретным полям и объясняют исправление. Общая красная плашка «данные неверны» заставила бы человека угадывать причину. Клиентская проверка сокращает количество неудачных попыток, но не заменяет server validation. Ту же форму можно отправить без браузера, поэтому сервер обязан самостоятельно проверить дату, доступные услуги, офис и права пользователя.
Фрагмент файла src/model.mjs
export function dateKey(date) {
if (!(date instanceof Date) || !Number.isFinite(date.getTime())) return "";
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}
Экран 5. Tree: подразделение меняет связанные данные
Дерево содержит сервисную компанию и три офиса. Выбор северного офиса показывает его восемь исходных заявок, связанные названия и общую сумму. Корневой узел возвращает обзор всех подразделений. При создании нового заказа в выбранном офисе результат изменится, потому что дерево работает с общей моделью, а не с отдельными демонстрационными числами.
Такой приём подходит для площадок, складов, категорий товаров и групп оборудования. Важно различать выбранный узел и раскрытые узлы: раскрытие помогает исследовать структуру, выбор меняет контекст рабочей области. В компоненте для них используются разные состояния, поэтому одно действие не должно случайно означать другое.
В большом дереве дочерние узлы можно загружать по требованию, показывать ожидание и обрабатывать отказ. Здесь структура мала и находится в коде. Это фильтр интерфейса, а не ограничение доступа: сведения скрываются визуально, но остаются в загруженном JSON. Если сотрудник не имеет права видеть другой офис, сервер не должен отправлять ему эти записи.
Фрагмент файла src/model.mjs
export const DEPARTMENTS = [
{
key: "all",
label: "Сервисная компания",
children: [
{ key: "north", label: "Северный офис" },
{ key: "center", label: "Центральный офис" },
{ key: "south", label: "Южный офис" },
],
},
];
Экран 6. Stepper: заказ по шагам без пропуска проверки
Мастер разделяет заказ на три этапа: клиент, состав работ и дата, итоговая проверка. Нажмите «Дальше» с пустым клиентом: переход остановится и появится понятное сообщение. На следующем шаге нельзя продолжить без услуг и даты. На последнем экране собраны выбранные значения, чтобы человек ещё раз оценил заказ перед подтверждением.
Stepper отвечает за представление шагов, а бизнес-правила остаются приложению. Свойство linear само по себе не знает, какой клиент допустим и какие услуги совместимы. Поэтому переход вызывает validateOrder и выбирает ошибки, относящиеся к текущему этапу. Финальное подтверждение проверяет весь объект повторно.
После подтверждения мастер показывает ID созданного заказа и предлагает отдельное действие для следующего. Это предотвращает случайное повторное создание той же заявки обычным повторным нажатием кнопки. Для настоящего API дополнительно нужна серверная защита повторов, например ключ идемпотентности. Вертикальная компоновка шагов сохраняет читаемые подписи на узком экране.
Фрагмент файла src/App.vue
function nextStep() {
const errors = validateOrder(wizard.value, asOf.value);
if (step.value === "1" && errors.client) {
wizardError.value = errors.client;
document.getElementById("wizard-client")?.focus();
return;
}
if (
step.value === "2" &&
(errors.date || errors.services || errors.department)
) {
wizardError.value = errors.date || errors.services || errors.department;
return;
}
wizardError.value = "";
step.value = step.value === "1" ? "2" : "3";
}
Когда пошаговая форма помогает, а когда мешает
Шаги удобны, когда одно решение подготавливает следующее: сначала тип услуги, затем доступные параметры, потом согласование. Человек не видит сразу все возможные поля и лучше понимает последовательность. Но короткую форму из трёх независимых полей часто удобнее оставить на одном экране, чтобы не добавлять лишние нажатия.
Полезно показывать прогресс словами и сохранять введённые значения при возврате. В нашем мастере кнопка «Назад» меняет активный шаг, но не очищает объект заказа. Пользователь может поправить клиента или дату, затем снова увидеть обновлённый итог. Очистка происходит только при явном начале нового заказа.
Если мастер должен переживать перезагрузку, сохранение черновика становится отдельной продуктовой функцией: нужен срок хранения, защита данных, связь с пользователем и объяснение статуса. localStorage не превращает черновик в серверный заказ. В этой мастерской такого сохранения нет, и интерфейс обещает только изменения внутри текущей вкладки.
Фрагмент файла src/App.vue
function finishWizard() {
const errors = validateOrder(wizard.value, asOf.value);
if (Object.keys(errors).length) {
wizardError.value = Object.values(errors).join(" ");
return;
}
const order = createOrder(rows.value, wizard.value, asOf.value);
rows.value = [order, ...rows.value];
wizardResult.value = order;
announce(`${order.id}: мастер завершён, заявка видна в общем кабинете.`);
}
Экран 7. Toast, Message и пустое состояние
Успешное действие вызывает короткое уведомление Toast. Рядом остаётся постоянная строка результата, а на седьмом экране доступен журнал последних восьми действий. Человек, который отвлёкся на несколько секунд, всё равно сможет понять, завершилась ли операция. Важный результат не должен существовать только в исчезающем всплывающем сообщении.
Кнопка «Учебный отказ» намеренно включает локальное состояние ошибки. Интерфейс так его и называет; это не запись настоящего сбоя API. Плашка Message объясняет, что данные заявки не изменились, и предлагает повторить действие успешно. Для реального отказа текст должен отражать фактическую причину и безопасное следующее действие.
«Пустой результат» подставляет поисковый запрос без совпадений. Заявки при этом не удаляются: виден фильтр, объяснение и кнопка очистки. Отличайте отсутствие данных от загрузки и ошибки. Пустой экран во всех трёх случаях выглядит одинаково, но требует совершенно разных действий пользователя.
Фрагмент файла src/App.vue
function announce(text, severity = "success") {
notice.value = text;
log.value.unshift({ text, severity });
log.value = log.value.slice(0, 8);
toast.add({
severity,
summary: severity === "error" ? "Действие не выполнено" : "Готово",
detail: text,
life: 4500,
});
}
Экран 8. Кабинет, который действительно меняется от данных
Последний экран объединяет показатели, прогресс очереди и последние заявки. Значения не написаны вручную в карточках: количество, сумма и доля завершения вычисляются из общего массива. Измените статус на втором экране и вернитесь сюда — показатели пересчитаются. Создайте заказ через форму — он появится среди свежих заявок.
Три кнопки загрузки демонстрируют нормальный ответ, пустой ответ и ответ неправильной структуры. Перед заменой непустого набора показывается подтверждение, потому что локальная работа будет потеряна при успешной загрузке. Если новый JSON не проходит проверку, прежние данные остаются доступными и интерфейс предлагает восстановление.
Это уже полезный каркас для небольшого кабинета: список, редактирование и сводка реагируют на одну модель. При подключении настоящего сервиса не следует копировать состояние отдельно для каждого виджета. Иначе карточка, таблица и счётчик быстро начинают показывать разные версии одной заявки. Для сложной аналитики можно дополнительно использовать графики Unovis во Vue.
Фрагмент файла src/model.mjs
export function summarize(rows) {
return {
total: rows.length,
open: rows.filter((r) => r.status !== "done").length,
done: rows.filter((r) => r.status === "done").length,
amount: rows.reduce((n, r) => n + r.amount, 0),
};
}
Учебный API: настоящий GET и проверка структуры ответа
Термин «учебный API» здесь означает три статических JSON-файла с известной схемой. Запрос идёт через fetch на тот же origin; в браузере можно увидеть URL, HTTP-статус и тело ответа. В public лежит только вымышленный набор. Эта демонстрация не создаёт базу данных и не выдаёт локальное изменение массива за сохранение на сервере.
Loader выбирает путь из фиксированной таблицы. Сначала проверяет response.ok, затем разбирает JSON и передаёт результат в validateDataset. Модель проверяет версию схемы, массив, разумный размер, уникальные ID, строки, статусы, офисы и целочисленные суммы. Это защищает интерфейс от явно неверного ответа, но остаётся ограниченной учебной проверкой, а не универсальной JSON Schema.
Файлы раздаются без credentials, потому что данные публичные. В рабочем кабинете политика авторизации будет другой: cookies или заголовки, срок сессии, обработка 401/403 и права на отдельные действия. Эти решения должен принимать серверный контракт. Не добавляйте секрет API в исходники Vue или переменные сборки, которые попадут в публичный JavaScript.
Фрагмент файла src/api.mjs
import { validateDataset } from "./model.mjs";
const FILES = {
normal: "cabinet.json",
empty: "empty.json",
invalid: "invalid.json",
};
export async function loadDataset(
mode = "normal",
{ signal, fetcher = fetch } = {},
) {
if (!Object.hasOwn(FILES, mode)) throw new Error("Неизвестный набор данных.");
const response = await fetcher(
new URL(`data/${FILES[mode]}`, document.baseURI),
{ signal, cache: "no-store", credentials: "omit" },
);
if (!response.ok)
throw new Error(`Не удалось загрузить данные: HTTP ${response.status}.`);
return validateDataset(await response.json());
}
Отмена запроса и последний успешный набор
Два запроса могут завершиться в обратном порядке. Если пользователь быстро переключил набор, поздний ответ первого запроса способен затереть более свежие данные. В примере предыдущий AbortController отменяется, а каждый запуск получает последовательный номер. Результат применяется только если этот номер всё ещё актуален.
Проверка номера дополняет отмену: не все источники прекращают работу мгновенно, а обработчик уже полученного ответа мог успеть продолжить выполнение. В finally тот же номер определяет, разрешено ли снять индикатор загрузки. Иначе старый запрос мог бы скрыть Skeleton, пока новый ещё выполняется.
При ошибке rows не заменяется пустым массивом. Сохраняется последний успешный набор и показывается отдельное сообщение. Пользователь видит прежние данные и понимает, почему новые не приняты. Для рабочего сервиса полезно также отмечать их актуальность, время последней синхронизации и ограничения дальнейших действий с устаревшей версией.
Фрагмент файла src/App.vue
async function load(mode = "normal") {
controller?.abort();
controller = new AbortController();
const seq = ++sequence;
busy.value = true;
apiError.value = "";
try {
const data = await loadDataset(mode, { signal: controller.signal });
if (seq !== sequence) return;
rows.value = data.requests;
asOf.value = data.asOf;
loaded.value = true;
selection.value = [];
loadedAt.value = new Date().toLocaleTimeString("ru-RU");
announce(
`Загружено ${rows.value.length} заявок из ${mode === "empty" ? "пустого" : "учебного"} JSON. Локальные изменения заменены.`,
);
} catch (error) {
if (seq === sequence && error.name !== "AbortError") {
apiError.value = error.message;
announce(
"Новые данные не приняты. Последний успешный набор сохранён.",
"error",
);
}
} finally {
if (seq === sequence) busy.value = false;
}
}
Тема, Bootstrap и понятная клавиатура
PrimeVue в styled mode даёт готовую тему, которую можно адаптировать через токены. В проекте используется Aura с тёмно-бирюзовой основной палитрой и светлой оболочкой. Цвет помогает различать состояние, но подпись статуса остаётся текстом: зелёный кружок без слова «Завершена» был бы неоднозначным.
Компоненты предоставляют часть клавиатурного поведения, однако подписи, порядок фокуса и реакция приложения остаются ответственностью автора страницы. Кнопка Drawer сообщает aria-expanded и aria-controls, поля связаны с label, сообщения об ошибках — с соответствующими полями. Есть заметный focus-visible и ссылка перехода к мастерской.
Проверьте Tab и Shift+Tab, открытие календаря и списка, Escape у панели, возврат к исходной кнопке и сохранение текста при ошибке. Не объявляйте интерфейс полностью доступным только по имени библиотеки. Если требуется другое визуальное оформление и более низкоуровневое управление, сравните этот подход с Reka UI и её примитивами.
Исходники, экспорт и отсутствие внешней CDN-зависимости
Vite собирает Vue, PrimeVue и тему в локальные файлы. Bootstrap CSS также входит в сборку. При открытии опубликованной мастерской браузеру не требуется скачать библиотеку с внешнего CDN. Это упрощает воспроизводимость: проверяется конкретная версия комплекта, а не меняющийся удалённый адрес latest.
Просмотр исходников формируется автоматически при сборке из настоящих файлов проекта. Текст помещается в textarea и не исполняется. Кнопка копирования обрабатывает отказ Clipboard API и предлагает скачать файл или выделить текст вручную. Полный ZIP содержит исходники, lock-файл, лицензии и готовый public; установка зависимостей потребуется только для разработки.
Экспорт заявок создаёт JSON с текущим состоянием вкладки. Это удобный способ забрать результат опыта и посмотреть, как изменился объект после действий. Экспорт не является резервной копией сервера и не подтверждает сохранение в CRM. Для больших или конфиденциальных данных скачивание потребует отдельной политики доступа и объёма.
Фрагмент файла src/App.vue
async function copySource() {
try {
await navigator.clipboard.writeText(sourceCode.value);
announce("Выбранный исходник скопирован.");
} catch {
sourceError.value =
"Браузер запретил буфер обмена. Выделите текст в поле ниже или скачайте файл.";
}
}
Размещение на Linux/VPS: статические файлы через Nginx
На VPS достаточно раздавать содержимое public. Не нужно держать npm start как production-службу и открывать наружу порт 8530. Соберите и проверьте проект на рабочей машине или в CI, загрузите результат в отдельный release-каталог, настройте новый server block и выполните nginx -t. Только после успешной проверки применяйте reload.
Конфигурация в архиве использует try_files с явным 404. Это важно для JSON и JavaScript: автоматический возврат index.html по любому URL может замаскировать отсутствующий файл ответом HTTP 200. Проверяйте не только статус страницы, но и content-type модулей, наличие dataset и фактический путь из index.html.
Для HTTPS используйте принятый механизм сервера: существующий reverse proxy, Caddy или Certbot для корректно настроенного Nginx. Перед выдачей сертификата нужны DNS и доступность соответствующих портов. Сохраните предыдущий release для отката. В архиве отдельно описаны права пользователя deploy, относительные URL и завершающий slash при размещении в подпапке.
Фрагмент файла docs/nginx.conf
# Replace hostname/path; add to an existing Nginx installation carefully.
server {
listen 80;
server_name workshop.example.org;
root /var/www/primevue-workshop/releases/20261113;
index index.html;
add_header X-Content-Type-Options nosniff always;
location / { try_files $uri $uri/ =404; }
location ~* \.(json|html)$ { add_header Cache-Control "no-cache"; }
location ~* \.(js|css)$ { expires 1h; }
location ~ /\. { deny all; }
}
Что проверяют тесты и чего они не доказывают
Одиннадцать тестов модели и loader проверяют независимость данных, повторные ID, неизвестные статусы, целые денежные суммы, комбинированные фильтры, ошибки формы, календарный день и создание заказа. Есть настоящий временный HTTP-сервер на loopback: loader получает JSON и обрабатывает реальный ответ 503. Сервер закрывается после проверки.
Отдельный тест импортирует готовый JavaScript bundle в JSDOM. Он проходит все восемь экранов настоящей PrimeVue: выбирает строки, открывает ConfirmDialog, проверяет Drawer, заполняет MultiSelect и календарь, выбирает офис, проходит Stepper, обрабатывает пустоту и неправильный JSON. Ошибки console.error также считаются неуспехом, а не скрываются ради зелёного результата.
Всего на Windows и Linux прошли двенадцать тестов. В проверке bundle fetch заменяется чтением файлов, а браузерная геометрия не воспроизводится. Поэтому это не доказательство мобильного отображения, touch или работы экранного диктора. Реальный браузер и публичный адрес проверяются отдельно, после сборки и размещения той же версии файлов.
Фрагмент файла README.md
npm run build
npm test
Размер сборки и разумное развитие проекта
Проверенный JavaScript всей мастерской занимает 1041917 байт, gzip — 227258 байт. CSS с Bootstrap занимает 238727 байт, gzip — 33211 байт. Это восемь экранов, таблица, календарь, тема и вспомогательные компоненты вместе, а не размер одного InputText. Точные показатели записаны в public/build-info.json и меняются при изменении состава проекта.
Для внедрения в своё приложение берите необходимые компоненты и измеряйте фактический bundle. Редко используемые разделы можно загрузить отдельно, справочники — ограничить разумным объёмом, таблицы — перевести на серверную пагинацию. Но оптимизация не должна скрывать важные состояния или превращать нормальное действие в серию непонятных загрузок.
Дальнейшее развитие определяет продуктовая задача: API с правами доступа, серверное сохранение, история изменений, обработка конфликтов, поиск по большой базе. Сначала перенесите один целостный сценарий и проверьте успех, отмену, отказ и пустой результат. Затем расширяйте кабинет, сохраняя единый источник данных и понятные обещания интерфейса.
Источники и следующий практический шаг
Скачайте проект, запустите готовый public и повторите действия с собственным вымышленным набором. Поменяйте один статус и убедитесь, что таблица, карточка и сводка остаются согласованными. Добавьте новое поле в данные, модель и форму, затем проверьте, что ошибка ввода не теряет черновик. Такой небольшой законченный шаг полезнее механического переноса всех компонентов сразу.
Официальная документация помогает уточнить свойства и события установленной версии: DataTable, Drawer, Tree, Stepper и Toast. Для API сверяйтесь именно с веткой v4, использованной в package-lock. При переходе на v5 отдельно прочитайте руководство миграции и условия распространения.
Авторский код распространяется по MIT, тексты лицензий зависимостей включены в архив. Готовый результат — не production CRM, а доступный для изучения и переноса каркас рабочего кабинета: настоящие компоненты, общая модель, загрузка данных, проверяемые действия и объяснённые границы.


