Быстрый старт фронтенда
Фреймворк VST utils использует экосистему Vue для отображения фронтенда. Руководство по быстрому старту проведет вас через наиболее важные шаги по настройке функций фронтенда. Установка приложения и настройка описаны в - разделе Backend этой документации.
Есть несколько этапов в приложении VST utils:
Перед запуском приложения:
checkCacheVersions() проверяет, была ли изменена версия приложения с последнего посещения и очищает все кэшированные данные, если это так;
загрузка схемы OpenAPI с бэкенда. Генерирует сигнал „openapi.loaded“;
загрузка всех статических файлов из SPA_STATIC в setting.py;
устанавливает AppConfiguration из схемы OpenAPI;
Приложение запущено:
если в settings.py есть centrifugoClient, подключается к нему. Дополнительные сведения о конфигурации centrifugo можно найти в разделе «Настройки клиента Centrifugo»;
загрузка списка доступных языков и переводов;
api.loadUser() возвращает данные пользователя;
ModelsResolver создает модели из схемы, генерирует сигнал models[${modelName}].created для каждой созданной модели и allModels.created, когда все модели созданы;
ViewConstructor.generateViews() инициализирует View fieldClasses и modelClasses;
QuerySetsResolver находит соответствующий queryset по имени модели и пути представления;
global_components.registerAll() регистрирует Vue global_components;
prepare() генерирует сигнал app.beforeInit с { app: this };
инициализация модели с LocalSettings. Узнайте больше об этом в разделе Локальные настройки;
создание routerConstructor из this.views, генерация сигнала „app.beforeInitRouter“ с { routerConstructor } и получение нового VueRouter({this.routes});
инициализация приложения Vue() из schema.info, хранилища pinia и генерация сигнала „app.afterInit“ с {app: this};
Приложение смонтировано.
Есть схема, представляющая процесс инициализации приложения (названия сигналов красным шрифтом):
Настройка фронтенда
Для настройки фронтенда переименуйте файл vite.config.ts.default в vite.config.ts. Каждый проект, основанный на vst-utils, содержит index.ts в каталоге /frontend_src/. Этот файл предназначен для вашего кода. Запустите команду yarn для установки всех зависимостей. Затем выполните yarn devBuild из корневого каталога вашего проекта для сборки статических файлов.
Выходные файлы будут собраны в каталоге {AppName}/static/spa. При установке vstutils через pip фронтенд код собирается автоматически, поэтому вам может потребоваться добавить каталог spa в gitignore.
Пример точки входа фронтенда:
import { initApp } from '@vstconsulting/vstutils';
initApp({
api: {
url: new URL('/api/', window.location.origin).toString(),
},
});
Хуки операций страниц
Функция hookViewOperation может быть использована для выполнения произвольного кода перед выполнением действия. Действие может быть прервано, если prevent на возвращаемом объекте установлено на true.
import { hookViewOperation, showConfirmationModal } from '@vstconsulting/vstutils';
hookViewOperation({
path: '/category/{id}/change_parent/',
operation: 'execute',
onBefore: async () => {
const isConfirmed = await showConfirmationModal({
title: 'Are you sure?',
text: 'Changing category parent is irreversible',
confirmButtonText: 'Change',
cancelButtonText: 'Cancel',
});
return {
prevent: !isConfirmed,
};
},
});
Настройка поля
В main.js создайте новое поле, расширив его от BaseField (или любого другого подходящего поля)
Например, создадим поле, которое отображает элемент HTML h1 с текстом „Привет, мир!“
class CustomField extends spa.fields.base.BaseField {
static get mixins() {
return super.mixins.concat({
render(createElement) {
return createElement('h1', {}, 'Hello World!');
},
});
}
}
Или отобразите имя человека с некоторым префиксом
class CustomField extends spa.fields.base.BaseField {
static get mixins() {
return super.mixins.concat({
render(h) {
return h("h1", {}, `Mr ${this.$props.data.name}`);
},
});
}
}
Зарегистрируйте это поле в app.fieldsResolver, чтобы предоставить соответствующий формат и тип поля для нового поля
const customFieldFormat = 'customField';
app.fieldsResolver.registerField('string', customFieldFormat, CustomField);
Слушайте подходящий сигнал models[ModelWithFieldToChange].fields.beforeInit для изменения формата поля
spa.signals.connect(`models[ModelWithFieldToChange].fields.beforeInit`, (fields) => {
fields.fieldToChange.format = customFieldFormat;
});
Список моделей и их полей доступен во время выполнения в консоли по адресу app.modelsClasses
Чтобы изменить поведение поля, создайте новый класс поля с необходимой логикой. Допустим, вам нужно отправить API количество миллисекунд, но пользователь хочет вводить количество секунд. Решением будет переопределить методы toInner и toRepresent поля.
class MilliSecondsField extends spa.fields.numbers.integer.IntegerField {
toInner(data) {
return super.toInner(data) * 1000;
}
toRepresent(data) {
return super.toRepresent(data)/1000;
}
}
const milliSecondsFieldFormat = 'milliSeconds'
app.fieldsResolver.registerField('integer', milliSecondsFieldFormat, MilliSecondsField);
spa.signals.connect(`models[OneAllFields].fields.beforeInit`, (fields) => {
fields.integer.format = milliSecondsFieldFormat;
});
Теперь у вас есть поле, которое отображает секунды, но сохраняет/получает данные в миллисекундах в виде детализированного представления модели AllFieldsModel.
Примечание
Если вам нужно показать какое-то предупреждение или ошибку в консоли разработчика, вы можете использовать методы warn и error поля. Вы можете передать сообщение, и оно будет выведено с типом поля, именем модели и именем поля.
Изменение пути к полю FkField
Иногда вам может потребоваться запросить другой набор объектов для FkField. Например, чтобы выбрать только известных авторов, создайте конечную точку famous_author на бэкенде и установите путь запроса FkField в famous_author. Слушайте сигнал app.beforeInit.
spa.signals.connect('app.beforeInit', ({ app }) => {
app.modelsResolver.get('OnePost').fields.get('author').querysets.get('/post/new/')[0].url = '/famous_author/'
});
Теперь, когда мы создаем новый пост на конечной точке /post/, Author FkField выполняет GET-запрос к /famous_author/ вместо /author/. Это полезно для получения другого набора авторов (которые могли быть ранее отфильтрованы на бэкенде).
Стилизация CSS
Как скрипты, файлы CSS могут быть добавлены в index.ts
import { initApp } from '@vstconsulting/vstutils';
import './style.css';
initApp({
api: {
url: new URL('/api/', window.location.origin).toString(),
},
});
Давайте проанализируем страницу и найдем класс CSS для нашего пользовательского поля. Это column-format-customField и создается с использованием шаблона column-format-{Field.format}.
Используйте обычные стили CSS для изменения внешнего вида поля.
.column-format-customField:hover {
background-color: orangered;
color: white;
}
Другие элементы страницы также доступны для стилизации: например, чтобы скрыть определенный столбец, установите соответствующее поле в значение none.
.column-format-customField {
display: none;
}
Показать столбец первичных ключей в списке
Каждый столбец первичного ключа имеет класс CSS pk-column и по умолчанию скрыт (с использованием display: none;).
Например, этот стиль покажет столбец первичных ключей во всех представлениях списка модели Order.
.list-Order .pk-column {
display: table-cell;
}
Настройка представления
Слушайте сигнал «allViews.created» и добавьте новый пользовательский миксин в представление.
В следующем фрагменте кода показано отображение нового представления вместо представления по умолчанию.
spa.signals.once('allViews.created', ({ views }) => {
const AuthorListView = views.get('/author/');
AuthorListView.mixins.push({
render(h) {
return h('h1', {}, `Custom view`);
},
});
});
Узнайте больше о функции render() Vue в документации Vue.
Также можно настроить представление, переопределив вычисляемые свойства и методы по умолчанию существующих миксинов. Например, переопределите вычисляемое свойство breadcrumbs для отключения хлебных крошек в представлении списка авторов.
import { ref } from 'vue';
spa.signals.once("allViews.created", ({ views }) => {
const AuthorListView = views.get("/author/");
AuthorListView.extendStore((store) => {
return {
...store,
breadcrumbs: ref([]),
};
});
});
Иногда вам может потребоваться скрыть страницу с деталями по какой-то причине, но при этом сохранить доступ ко всем действиям и подссылкам с страницы списка. Чтобы сделать это, также следует слушать сигнал «allViews.created» и изменить параметр hidden с значения false по умолчанию на true, например:
spa.signals.once('allViews.created', ({ views }) => {
const authorView = views.get('/author/{id}/');
authorView.hidden = true;
});
Изменение заголовка представления
Чтобы изменить заголовок и строку, отображаемую в хлебных крошках, измените свойство title представления или метод getTitle для более сложной логики.
spa.signals.once('allViews.created', ({ views }) => {
const usersList = views.get('/user/');
usersList.title = 'Users list';
const userDetail = views.get('/user/{id}/');
userDetail.getTitle = (state) => (state?.instance ? `User: ${state.instance.id}` : 'User');
});
Хранилище страницы
У каждой страницы есть хранилище, которое можно получить глобально app.store.page или из компонента страницы с использованием this.store.
Метод представления extendStore можно использовать для добавления пользовательской логики в хранилище страницы.
import { computed } from 'vue';
spa.signals.once('allViews.created', ({ views }) => {
views.get('/user/{id}/').extendStore((store) => {
// Override title of current page using computed value
const title = computed(() => `Current page has ${store.instances.hength} instances`);
async function fetchData() {
await store.fetchData(); // Call original fetchData
await callSomeExternalApi(store.instances.value);
}
return {
...store,
title,
fetchData,
};
});
});
Переопределение корневого компонента
Корневой компонент приложения можно переопределить с использованием сигнала app.beforeInit. Это может быть полезно, например, для изменения классов CSS макета, поведения кнопки назад или основных компонентов макета.
Пример настройки компонента боковой панели:
const CustomAppRoot = {
components: { Sidebar: CustomSidebar },
mixins: [spa.AppRoot],
};
spa.signals.once('app.beforeInit', ({ app }) => {
app.appRootComponent = CustomAppRoot;
});
Перевод значений полей
Значения, отображаемые с использованием FKField или ChoicesField, могут быть переведены с использованием стандартных файлов перевода.
Ключ перевода должен быть определен как :model:<ModelName>:<fieldName>:<value>. Например:
TRANSLATION = {
':model:Category:name:Category 1': 'Категория 1',
}
Перевод значений может быть трудоемким, поскольку каждая модель на бэкенде обычно генерирует более одной модели на фронтенде. Для избежания этого добавьте атрибут _translate_model = „Category“ к модели на бэкенде. Это сокращает
':model:Category:name:Category 1': 'Категория 1',
':model:OneCategory:name:Category 1': 'Категория 1',
':model:CategoryCreate:name:Category 1': 'Категория 1',
в
':model:Category:name:Category 1': 'Категория 1',
Для FKField используется имя связанной модели. И fieldName должно быть равно viewField.
Изменение действий или подссылок
Иногда использование только схемы для определения действий или подссылок недостаточно.
Например, у нас есть действие, которое делает пользователя суперпользователем (/user/{id}/make_superuser/), и мы хотим скрыть это действие, если пользователь уже является суперпользователем (is_superuser равно true). Сигнал <${PATH}>filterActions может быть использован для достижения такого результата.
spa.signals.connect('</user/{id}/make_superuser/>filterActions', (obj) => {
if (obj.data.is_superuser) {
obj.actions = obj.actions.filter((action) => action.name !== 'make_superuser');
}
});
<${PATH}>filterActions получает {actions, data}
<${PATH}>filterSublinks получает {sublinks, data}
Свойство data будет содержать данные экземпляра. Свойства actions и sublinks будут содержать массивы с элементами по умолчанию (не скрытыми действиями или подссылками), их можно изменить или полностью заменить.
Локальные настройки
Поля этой модели отображаются в левой боковой панели. Все данные из этой модели сохраняются в локальном хранилище браузера. Если вы хотите добавить другие варианты, вы можете сделать это, используя сигнал beforeInit, например:
spa.signals.once('models[_LocalSettings].fields.beforeInit', (fields) => {
const cameraField = new spa.fields.base.BaseField({ name: 'camera' });
// You can add some logic here
fields.camera = cameraField;
})
Хранилище
Есть три способа сохранения данных:
userSettingsStore - сохраняет данные на сервере. По умолчанию есть варианты изменения языка и кнопка включения/выключения темного режима. Данные для userSettingsStore поступают из схемы.
localSettingsStore - сохраняет данные в локальном хранилище браузера. Здесь вы можете хранить свои собственные поля, как описано в Локальные настройки.
store - хранит данные текущей страницы.
Чтобы использовать любое из этих хранилищ, вам нужно выполнить следующую команду: app.[storeName], например: app.userSettingsStore.
Примечание
Если вы обращаетесь к userSettingsStore изнутри компонента, тогда вам нужно использовать this.$app вместо app.
Из app.store вам может потребоваться:
vewsItems и viewItemsMap - хранят информацию о родительских представлениях для этой страницы. Они используются, например, в хлебных крошках. Различие между ними заключается только в способе хранения информации: viewItems - это массив объектов, а viewItemsMap - это карта.
page - сохраняет всю информацию о текущей странице.
title - заголовок текущей страницы.