UI Bitrix Framework: личный кабинет на системных компонентах
Собираем личный кабинет — профиль, заказы, уведомления — на системных UI-компонентах ядра: Extension::load, ui.system.*, sidepanel и notification-manager. Без Bootstrap, без сборщика. Vue-версия — для подписчиков.
Кирилл Новожилов
Автор
Содержание
Официальная документация описывает отдельные виджеты — поле ввода, меню, диалог, боковую панель. Но на практике разработчику нужен ответ на другой вопрос: как связать эти куски в цельный интерфейс публичной части, не таща Bootstrap и не копируя вёрстку из Битрикс24.
В этом туториале мы соберём личный кабинет — профиль, заказы, настройки уведомлений — на UI-расширениях ядра. Публичная часть показывает подключение через \Bitrix\Main\UI\Extension::load() и BX.Runtime.loadExtension() без сборщика. Vue-версия того же интерфейса и продакшен-нюансы — в блоке для подписчиков.
Исходники:
- Полная версия исходников кабинета на PHP + JS —
tutorial-bitrix-ui-cabinet-js(также в первом комментарии к посту в Telegram-канале). В статье же описывается пошаговый подход к созданию кабинета. - Vue-версия —
tutorial-bitrix-ui-cabinet-vue.
Что вы получите
После прочтения вы сможете:
- Понимать архитектуру UI Bitrix: расширения,
rel, design tokens. - Подключать системные компоненты (
ui.system.*) на странице сайта через шаблон. - Инициализировать JS-классы через
BX.Runtime.loadExtension()без@bitrix/chef. - Связать UI с PHP через
ControllerableиBX.ajax.runComponentAction. - Собрать из этих приёмов раздел профиля с сохранением — и самостоятельно повторить паттерн для блока уведомлений.
Уровень: Middle (нужен опыт с компонентами и D7).
Окружение: 1С-Битрикс 25.x+, PHP 8.2+, модуль ui. Примеры проверены на актуальном ядре (main 26.700.0, ui 26.550.0).
Часть 1. Как устроен UI Bitrix
1.1. Три слоя, которые нельзя путать
| Слой | Что это | Пример |
|---|---|---|
| PHP | Загрузка ассетов на страницу | \Bitrix\Main\UI\Extension::load('ui.system.input') |
| JS-расширение | Бандл + зависимости (rel в config.php) |
/bitrix/js/ui/system/input/ |
| JS-класс / Vue | API виджета | new Input({...}), <BInput v-model="..." /> |
CJSCore. Вся отрисовка — в браузере.Класс Extension из main делает ровно две вещи: находит config.php по имени расширения и вызывает CJSCore::init():
// bitrix/modules/main/lib/ui/extension.php
public static function load($extNames): void
{
foreach ((array)$extNames as $extName) {
if (static::register($extName)) {
\CJSCore::init($extName);
}
}
}
Имя расширения ui.system.input превращается в путь /bitrix/js/ui/system/input/.
1.2. Карта системных расширений
Состав ui.system.* зависит от версии ядра, поэтому ориентируйтесь не на документацию, а на папку /bitrix/js/ui/system/ своей сборки. Карта ниже актуальна для main 26.700.0 / ui 26.550.0:
| Расширение | Назначение | Vue-обёртка |
|---|---|---|
ui.system.input |
Поля ввода, пароль, чипы в input | ui.system.input.vue → BInput |
ui.system.typography |
Заголовки и текст | ui.system.typography.vue → Headline, Text |
ui.system.menu |
Контекстное меню | ui.system.menu.vue |
ui.system.chip |
Чипы / теги | ui.system.chip.vue |
ui.system.alert |
Алерты-плашки статуса | ui.system.alert.vue |
ui.system.checkbox |
Чекбоксы | ui.system.checkbox.vue |
ui.system.radiobutton |
Радиокнопки | ui.system.radiobutton.vue |
ui.system.dialog |
Модальный диалог | — |
ui.system.skeleton |
Скелетон загрузки | ui.system.skeleton.vue |
ui.system.label |
Подписи | ui.system.label.vue |
ui.system.highlighter |
Подсветка текста | — |
На старых ядрах (main 25.x и раньше) части расширений может не быть: ui.system.alert, ui.system.checkbox и ui.system.radiobutton появились позже остальных. Для алертов там остаётся легаси-вариант ui.alerts — о миграции см. раздел 4.6.
1.3. «Старшие» UI-расширения вне system/
В документации их нет, но без них полноценный кабинет не собрать:
| Расширение | Зачем в кабинете |
|---|---|
ui.design-tokens |
CSS-переменные (--ui-color-*, --ui-font-*) — без них «серые» поля выглядят чужеродно |
ui.buttons |
Кнопки действий (сохранить, навигация) |
ui.dialogs.messagebox |
MessageBox.confirm() — подтверждение выхода |
ui.notification-manager |
Тосты после сохранения |
main.sidepanel |
Боковая панель с деталями заказа |
ui.icon-set.api.core + ui.icon-set.outline |
Иконки в меню и кнопках |
1.4. Design tokens — обязательная база
Почти все новые UI-компоненты завязаны на ui.design-tokens. Подключайте его первым:
\Bitrix\Main\UI\Extension::load([
'ui.design-tokens',
'ui.system.input',
]);
Без tokens поля и кнопки получат дефолтные цвета ядра, которые могут конфликтовать с шаблоном сайта.
Часть 2. Архитектура примера «Личный кабинет»
/local/
└── components/vendor/personal.cabinet/
├── class.php # Controllerable: saveProfile
└── templates/.default/
├── template.php # Extension::load + data-config
├── script.js # BX.Runtime.loadExtension → UI
└── style.css # Layout (сетка, карточки)
/personal/cabinet/index.php # Страница-обёртка (в корне сайта)
Поток данных:

Три раздела кабинета:
- Профиль —
ui.system.input+ сохранение через AJAX (шаги 0–4). - Уведомления — переключатели на
ui.system.checkbox+ui.notification-manager(шаг 5). - Заказы — список карточек +
main.sidepanelсcontentCallback(фрагменты в шаге 6, целиком — в полном примере).
Часть 3. Практика: разбираем приёмы по шагам
Формат этой части: каждый шаг добавляет один приём и объясняет его нюансы — ровно те, что заявлены в начале статьи. Код в шагах короткий, пишите его руками: цель — научиться и понять принцип. В конце каждого шага описано, что должно получиться на экране.
Если что-то не завелось — сверьтесь с полными исходниками.
Шаг 0. Каркас: страница, компонент и данные через D7
Страница-обёртка. NEED_AUTH берёт авторизацию на себя — неавторизованный увидит форму входа, и в компоненте можно не проверять права:
File: personal/cabinet/index.php
<?php
define('NEED_AUTH', true);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/header.php';
global $APPLICATION;
$APPLICATION->SetTitle('Личный кабинет');
$APPLICATION->IncludeComponent('vendor:personal.cabinet', '.default', [], false);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/footer.php';
Минимальный компонент. Данные берём по-современному: CurrentUser вместо global $USER, ORM-класс UserTable вместо CUser::GetByID().
File: local/components/vendor/personal.cabinet/class.php
<?php declare(strict_types=1);
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\UserTable;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
final class VendorPersonalCabinetComponent extends CBitrixComponent
{
public function executeComponent(): void
{
$user = UserTable::getRow([
'select' => ['ID', 'NAME', 'EMAIL', 'PERSONAL_PHONE'],
'filter' => ['=ID' => (int)CurrentUser::get()->getId()],
]);
$this->arResult['USER'] = [
'ID' => (int)$user['ID'],
'NAME' => (string)($user['NAME'] ?? ''),
'EMAIL' => (string)($user['EMAIL'] ?? ''),
'PERSONAL_PHONE' => (string)($user['PERSONAL_PHONE'] ?? ''),
];
$this->includeComponentTemplate();
}
}
И временный шаблон-заглушка — просто чтобы убедиться, что данные доехали:
File: local/components/vendor/personal.cabinet/templates/.default/template.php
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
?>
<pre><?= htmlspecialcharsbx(print_r($arResult['USER'], true)) ?></pre>
/personal/cabinet/ показывает массив с вашим ID, именем и e-mail.
Серверная часть работает — переходим к UI.Шаг 1. Extension::load: что на самом деле попадает на страницу
Заменяем заглушку. Подключаем пока всего два расширения и рисуем один пустой контейнер:
File: local/components/vendor/personal.cabinet/templates/.default/template.php
<?php
use Bitrix\Main\UI\Extension;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
Extension::load([
'ui.design-tokens',
'ui.system.typography',
]);
?>
<div id="vpc-app"></div>
Страница осталась пустой — и это правильный момент, чтобы понять, что именно сделал Extension::load(). Откройте исходный код страницы и найдите:
/bitrix/js/ui/system/typography/dist/typography.bundle.js— имяui.system.typographyпревратилось в путь к папке расширения;- CSS-файл design tokens и бандлы зависимостей — их притянул
relизconfig.phpрасширения. Откройте/bitrix/js/ui/system/typography/config.phpи сверьте списокrelс тем, что появилось на странице.

ui.design-tokens ставим в списке первым — почти все системные компоненты завязаны на его CSS-переменные, без него виджеты выглядят чужеродно.Шаг 2. BX.Runtime.loadExtension: получаем классы в JS
Создайте рядом с template.php файлы script.js и style.css. Битрикс подключает их из папки шаблона автоматически — никаких addExternalJS() / addExternalCss() не нужно.
File: local/components/vendor/personal.cabinet/templates/.default/script.js
BX.ready(() => {
BX.Runtime.loadExtension('ui.system.typography').then((exports) => {
console.log(exports);
const app = document.getElementById('vpc-app');
app.append(exports.Headline.render('Личный кабинет', { size: 'md', tag: 'h1' }));
});
});
Откройте консоль браузера: в exports лежат все ES-экспорты расширения — Headline, Text, Vue-обёртки. Это тот же результат, что дал бы import { Headline } from 'ui.system.typography' в собранном расширении, только без @bitrix/chef и этапа сборки.

Два нюанса, на которых спотыкаются чаще всего:
BX.Runtime.loadExtensionработает только в паре сExtension::loadв PHP: если расширение не зарегистрировано на странице, экспортов вы не получите. Уберите строку изtemplate.phpи убедитесь сами.- Создавать виджеты в инлайновом
<script>посреди страницы нельзя — ядроmain.coreещё не готово. Точка входа — всегдаBX.ready.

Шаг 3. data-config: передаём данные из PHP и рисуем поле
Данные компонента отдаём в JS через data--атрибут с JSON. Так PHP не смешивается с JS: шаблон не генерирует код, а script.js остаётся чистым статичным файлом:
File: local/components/vendor/personal.cabinet/templates/.default/template.php
<?php
use Bitrix\Main\UI\Extension;
use Bitrix\Main\Web\Json;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
Extension::load([
'ui.design-tokens',
'ui.system.typography',
'ui.system.input',
]);
$config = [
'user' => $arResult['USER'],
];
?>
<div id="vpc-app" data-config="<?= htmlspecialcharsbx(Json::encode($config)) ?>"></div>
В script.js читаем конфиг и создаём системное поле:
BX.ready(() => {
const app = document.getElementById('vpc-app');
const config = JSON.parse(app.dataset.config);
Promise.all([
BX.Runtime.loadExtension('ui.system.typography'),
BX.Runtime.loadExtension('ui.system.input'),
]).then(([typography, inputModule]) => {
app.append(typography.Headline.render('Профиль', { size: 'md', tag: 'h1' }));
const nameInput = new inputModule.Input({
label: 'Имя',
value: config.user.NAME,
size: inputModule.InputSize.Md,
design: inputModule.InputDesign.Grey,
required: true,
withClear: true,
onInput: () => nameInput.setError(''),
});
app.append(nameInput.render());
});
});
Обратите внимание на связку setError('') в onInput: поле умеет подсвечивать ошибку, но сбрасывать её при новом вводе — ваша задача. Этот приём заработает в полную силу на следующем шаге, когда ошибки начнёт присылать сервер.
data-config.
Шаг 4. Controllerable: связываем UI с PHP
Теперь сохранение. Компонент объявляет AJAX-действия через контракт Controllerable — отдельный контроллер или REST для этого не нужны. Дополните class.php: добавьте к use новые классы, объявите интерфейсы и допишите методы:
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Engine\Contract\Controllerable;
use Bitrix\Main\Error;
use Bitrix\Main\Errorable;
use Bitrix\Main\ErrorCollection;
use Bitrix\Main\Mail\Address;
final class VendorPersonalCabinetComponent extends CBitrixComponent implements Controllerable, Errorable
{
private ErrorCollection $errors;
public function onPrepareComponentParams($arParams)
{
$this->errors = new ErrorCollection();
return parent::onPrepareComponentParams($arParams);
}
public function configureActions(): array
{
return [
'saveProfile' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
new ActionFilter\Csrf(),
],
],
];
}
public function getErrors(): array
{
return $this->errors->toArray();
}
public function getErrorByCode($code): ?Error
{
return $this->errors->getErrorByCode($code);
}
public function saveProfileAction(string $name, string $email): ?array
{
$name = trim($name);
if ($name === '')
{
$this->errors->setError(new Error('Укажите имя.'));
return null;
}
if (!(new Address($email))->validate())
{
$this->errors->setError(new Error('Укажите корректный e-mail.'));
return null;
}
$user = new CUser();
$updated = $user->Update((int)CurrentUser::get()->getId(), [
'NAME' => $name,
'EMAIL' => $email,
]);
if (!$updated)
{
$this->errors->setError(new Error((string)$user->LAST_ERROR));
return null;
}
return ['user' => ['NAME' => $name, 'EMAIL' => $email]];
}
// executeComponent() — без изменений
}
Что здесь важно понять:
- Имя метода. Действие
saveProfile= методsaveProfileAction. Аргументы придут изdataAJAX-запроса по именам — приведение и типизацию ядро сделает само. Параметр без значения по умолчанию при этом обязателен: не пришлёт клиент поле — действие ответит ошибкойCould not find value for parameter {…}. Необязательным полям давайте дефолт (string $phone = '') — биндер подставит его сам. - Префильтры — защита действия:
Authenticationне пустит гостя,HttpMethodотсечёт GET,Csrfпотребует валидныйsessid(клиентский хелпер подставит его сам; дёрните URL действия руками без токена — получите ошибку безопасности). - Ошибки. У
Engine\Controllerесть готовыйaddError(), а у компонента — нет: движок после выполнения действия сам спрашивает ошибки у того, кто реализует интерфейсErrorable. Поэтому держимErrorCollection, инициализируем её вonPrepareComponentParams()(его ядро вызывает и при AJAX-запросе) и складываем ошибки черезsetError+return null— клиент получит их массивомerrors. Исключения на границе компонента не нужны.
Для вызова действия JS должен знать имя компонента и его подписанные параметры. Добавьте их в $config в template.php (а в Extension::load — ui.buttons и ui.notification-manager):
$config = [
'componentName' => $this->getComponent()->getName(),
'signedParameters' => $this->getComponent()->getSignedParameters(),
'user' => $arResult['USER'],
];
signedParameters — параметры компонента, подписанные ключом сайта: при AJAX-вызове сервер доверяет им, потому что подпись нельзя подделать на клиенте.
Остался клиент. В script.js расширяем список загружаемых расширений — в Promise.all появляются два новых, и у колбэка then соответственно два новых аргумента:
Promise.all([
BX.Runtime.loadExtension('ui.system.typography'),
BX.Runtime.loadExtension('ui.system.input'),
BX.Runtime.loadExtension('ui.buttons'),
BX.Runtime.loadExtension('ui.notification-manager'),
]).then(([typography, inputModule, buttons, notificationManager]) => {
// здесь — Headline и nameInput из шага 3,
// рядом добавьте emailInput по образцу nameInput
});
Порядок важен: аргументы в then приходят в том же порядке, что и промисы в массиве. Внутрь колбэка добавьте кнопку с вызовом действия:
const saveButton = new buttons.Button({
text: 'Сохранить',
color: buttons.ButtonColor.SUCCESS,
round: true,
noCaps: true,
onclick: () => {
BX.ajax.runComponentAction(config.componentName, 'saveProfile', {
mode: 'class',
signedParameters: config.signedParameters,
data: {
name: nameInput.getValue().trim(),
email: emailInput.getValue().trim(),
},
}).then(() => {
notificationManager.Notifier.notifyViaBrowserProvider({
id: 'profile-saved',
title: 'Профиль сохранён',
text: 'Данные профиля обновлены.',
});
}).catch((errorResponse) => {
const message = errorResponse.errors?.[0]?.message || 'Не удалось сохранить профиль.';
notificationManager.Notifier.notifyViaBrowserProvider({
id: 'profile-save-error',
text: message,
});
});
},
});
app.append(saveButton.render());
mode: 'class' говорит ядру искать действие в классе компонента (а не в файле ajax.php рядом с ним).
- меняете имя → «Сохранить» → тост в углу, данные в админке (
Настройки → Пользователи) обновились; - вводите некорректный e-mail → тост с текстом ошибки с сервера — та самая ошибка из
ErrorCollectionдействия; - во вкладке Network виден запрос на
/bitrix/services/main/ajax.phpс action вашего компонента иsessidв теле.

Красивее показывать серверные ошибки не тостом, а плашкой ui.system.alert — его API разобран в разделе 4.6, подключать loadExtension вы уже умеете.

Шаг 5. Закрепление: блок «Уведомления» своими руками
Все приёмы у вас в руках — соберите второй блок кабинета самостоятельно, без готового кода:
- В
class.phpдобавьте в$this->arResult['NOTIFICATIONS']демо-массив подписок (id,label,enabled) и прокиньте его в$configшаблона — какuserв шаге 3. - В
Extension::loadдобавьтеui.system.checkbox(и наборы иконокui.icon-set.api.core+ui.icon-set.outline— галочка чекбокса рисуется иконкой). - В
script.jsотрендерите по строке на подписку:Text+ чекбокс. Из нового — только API чекбокса:
const checkbox = new checkboxModule.Checkbox({
size: checkboxModule.CheckboxSize.Md,
checked: item.enabled,
onChange: ({ checked }) => {
// тост через Notifier — как в шаге 4
},
});
row.append(checkbox.render());
Колбэк onChange получает { checked, event }; состоянием можно управлять из кода — setChecked(), isChecked(), для «выбрана часть подписок» есть опция indeterminate. Сохранение настроек на сервер — ещё одно действие в configureActions() по образцу шага 4.

Шаг 6. Куда расти: заказы, sidepanel, меню
Фрагменты ниже — из полного примера: там кабинет получает третий раздел «Заказы», навигацию-вкладки и скелетон загрузки. Они строятся поверх каркаса из шагов 0–5; каждому нужно своё расширение в Extension::load шаблона и в loadExtension.
Боковая панель заказа (main.sidepanel). Документация показывает открытие по URL, но для кабинета удобнее contentCallback — не нужна отдельная страница:
ui.SidePanel.Instance.open(`order-${order.id}`, {
width: 520,
cacheable: false,
allowChangeHistory: false,
title: `Заказ №${order.number}`,
contentCallback: () => {
const content = document.createElement('div');
content.style.padding = '24px';
// ... Headline + Text + кнопка «Закрыть»
return content;
},
});
allowChangeHistory: false критичен для публичной части — иначе слайдер перепишет URL.
Контекстное меню (ui.system.menu):
const menu = new ui.Menu({
bindElement: button.getContainer(),
items: [
{
id: 'logout',
title: 'Выйти',
design: ui.MenuItemDesign.Alert,
onClick: () => { /* ... */ },
},
],
});
menu.show(event.currentTarget);
Подтверждение (ui.dialogs.messagebox):
ui.MessageBox.confirm(
config.messages.logoutConfirm,
config.messages.logoutTitle,
() => { /* redirect logout */ },
);
Скелетон (ui.system.skeleton) — пока грузится первая секция:
sectionNode.append(
ui.Circle(48),
ui.Line(220, 16, 8),
ui.Line(360, 12, 8),
);
Часть 4. О чём не расскажут в официальной документации
4.1. BX.Runtime.loadExtension vs import в расширении
Документация показывает два пути:
// Путь A — модульное расширение (нужен @bitrix/chef)
import { Input } from 'ui.system.input';
// Путь B — шаблон компонента (без сборщика)
BX.Runtime.loadExtension('ui.system.input').then(({ Input }) => { ... });
Для публичной части сайта путь B часто практичнее: не нужен CI-сборщик в маленьком проекте.
4.2. Extension::getResourceList() — один запрос, все зависимости
Если строите SPA или AJAX-оболочку и хотите отдать ассеты одним JSON:
use Bitrix\Main\UI\Extension;
$resources = Extension::getResourceList([
'ui.system.input',
'ui.buttons',
], ['with_dependency' => true]);
// $resources['js'], $resources['css'], $resources['settings']
Метод разворачивает rel и post_rel из всех config.php. Полезно для кастомного шаблона, который не использует bitrix/header.php.
4.3. settings в config.php расширения
В config.php можно передать PHP-данные в JS:
return [
'js' => 'dist/app.bundle.js',
'settings' => [
'maxUploadSize' => 5_242_880,
],
];
На клиенте: BX.Extension.getSettings('vendor.myext'). Документация это не освещает, но паттерн используется в ядре (CRM, IM).
4.4. ui.system.input тянет Vue-зависимости
Даже если вы используете только JS-класс Input, в rel уже есть ui.system.chip.vue и ui.icon-set.api.vue. Это нормально — бандлер ядра собирает общий пакет. Не пугайтесь «лишних» .vue в Network: они не монтируют приложение сами.
4.5. Порядок загрузки на публичной странице
Типичная ошибка — вызвать new Input(...) в <script> до BX.ready. Правильная последовательность:
bitrix/header.php→ ядроmain.core.Extension::load(...)в шаблоне.BX.ready→BX.Runtime.loadExtension→ создание виджетов.
4.6. ui.system.alert вместо легаси ui.alerts
В актуальном ядре системный алерт уже есть, и API у него отличается от старого ui.alerts:
const alert = new ui.Alert({
design: ui.AlertDesign.tintedSuccess,
content: 'Профиль сохранён',
hasCloseButton: true,
events: {
closeButtonClick: () => alert.destroy(),
},
});
alertNode.append(alert.render());
Соответствие при миграции с ui.alerts:
Легаси ui.alerts |
Новый ui.system.alert |
|---|---|
color: Alert.Color.SUCCESS |
design: AlertDesign.tintedSuccess |
text: '...' |
content: '...' (строка или HTMLElement) |
closeBtn: true |
hasCloseButton: true + событие closeButtonClick |
getContainer() |
render() |
Доступные варианты AlertDesign: tinted (нейтральный), tintedSuccess, tintedWarning, tintedAlert.
Если Extension::load('ui.system.alert') молча не регистрирует расширение — ядро старое (main 25.x и раньше), и там остаётся ui.alerts:
\Bitrix\Main\UI\Extension::load('ui.alerts');
const alert = new ui.Alert({
text: 'Профиль сохранён',
color: ui.Alert.Color.SUCCESS,
closeBtn: true,
});
alertNode.append(alert.getContainer());
Часть 5. Антипаттерны
| Симптом | Причина | Решение |
|---|---|---|
| Поле без стилей, «голый» input | Не загружен ui.design-tokens |
Добавить в Extension::load первым |
BX.Runtime.loadExtension → undefined |
Бандл не на странице | Сначала Extension::load в PHP |
| Слайдер меняет URL сайта | allowChangeHistory по умолчанию true |
Передать false |
runComponentAction 403 |
Нет CSRF или не авторизован | Проверить ActionFilter\Csrf, сессию |
| Иконка не видна | Нет CSS-набора | Подключить ui.icon-set.outline |
| Двойная инициализация | script.js без guard |
Проверять root.__inited |
Часть 6. Полный пример
Полный пример — это кабинет из шагов 0–5 плюс раздел «Заказы» с боковой панелью, навигация-вкладки между разделами, контекстное меню с подтверждением выхода и скелетон загрузки. Код — в репозитории подписчиков:
- JS (шаблон):
tutorial-bitrix-ui-cabinet-js(также можно скачать в первом комментарии к посту в Telegram) - Vue:
tutorial-bitrix-ui-cabinet-vue
Скопируйте local/ и personal/ в корень сайта → откройте /personal/cabinet/.
Теги:
Комментарии (0)
Пожалуйста, войдите в аккаунт, чтобы оставить комментарий
Оставить комментарийЗагрузка...
Пока нет ни одного комментария. Будьте первым!
Похожие статьи
Тихие хуки Битрикса: вспомогательные файлы, которые ядро подключает за вас (и как ими пользоваться)