Быстрый старт фронтенда

Фреймворк VST utils использует экосистему Vue для отображения фронтенда. Руководство по быстрому старту проведет вас через наиболее важные шаги по настройке функций фронтенда. Установка приложения и настройка описаны в - разделе Backend этой документации.

Есть несколько этапов в приложении VST utils:

  1. Перед запуском приложения:

    • checkCacheVersions() проверяет, была ли изменена версия приложения с последнего посещения и очищает все кэшированные данные, если это так;

    • загрузка схемы OpenAPI с бэкенда. Генерирует сигнал „openapi.loaded“;

    • загрузка всех статических файлов из SPA_STATIC в setting.py;

    • устанавливает AppConfiguration из схемы OpenAPI;

  2. Приложение запущено:

    • если в 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};

  3. Приложение смонтировано.

Есть схема, представляющая процесс инициализации приложения (названия сигналов красным шрифтом):

graph TD
  Cached("checkCachedVersion()")--New App Version-->Clean("cleanAllCache()");
  Cached--Same App Version-->Schema(Load Schema);
  Clean-->Cached
  Schema--'openapi.loaded'-->AppConfiguration
  AppConfiguration--Has Centrifugo options-->Centrifugo(Connect Centrifugo)
  AppConfiguration--No Centrifugo-->Translation
  Centrifugo-->Translation(Load translation, <br/> load languages)
  Translation-->LoadUser("api.LoadUser()")-->ModelsResolver
  subgraph Models generation
  ModelsResolver--All Models Created-->B('allModels.created'):::classSignal
  ModelsResolver--Not All Models Created-->Create(Create Model)
  Create(Create Model)-->SignalBeforeInit("'models#91;modelName#93;.fields.beforeInit'"):::classSignal-->Fields(Create Fields)
  Fields(Create Fields)-->SignalAfterInit("'models#91;modelName#93;.fields.afterInit'"):::classSignal-->modelsmodelName("'models#91;modelName#93;.created'"):::classSignal
  modelsmodelName-->ModelsResolver
  end
  ViewConstructor("ViewConstructor.generateViews()")--'allViewsCreated'-->QuerySetResolver
  QuerySetResolver--finds approppriate querySet-->registerAll("global_components.registerAll()")
  registerAll--registers Vue global_components-->prepare
  prepare--'app.beforeInit'-->RouterConstuctor
  RouterConstuctor--'app.beforeInitRouter'-->D("new VueRouter()")
  D-->E(Vue Internationalization plugin - i18n)
  E-->F(Create new Vue Instance)

linkStyle 3 stroke:red,color:red
linkStyle 11 stroke:red,color:red
linkStyle 12 stroke:red,color:red
linkStyle 14 stroke:red,color:red
linkStyle 16 stroke:red,color:red
linkStyle 19 stroke:red,color:red
linkStyle 20 stroke:red,color:red

classDef classSignal stroke:#333,color:#f00;

Настройка фронтенда

Для настройки фронтенда переименуйте файл 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,
    };
  },
});

Настройка поля

  1. В 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}`);
      },
    });
  }
}
  1. Зарегистрируйте это поле в app.fieldsResolver, чтобы предоставить соответствующий формат и тип поля для нового поля

const customFieldFormat = 'customField';
app.fieldsResolver.registerField('string', customFieldFormat, CustomField);
  1. Слушайте подходящий сигнал 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

  1. Как скрипты, файлы 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}.

  1. Используйте обычные стили 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.

Локальные настройки

Поля этой модели отображаются в левой боковой панели. Все данные из этой модели сохраняются в локальном хранилище браузера. Если вы хотите добавить другие варианты, вы можете сделать это, используя сигнал 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 - заголовок текущей страницы.