Компоненты админки: vesp-table

Продолжаем наше знакомство с админкой Vesp.
Опираясь на свой опыт работы в MODX я постарался повторить примерную логику работы и здесь, для чего были написаны основные компоненты админки. Под капотом они используют BootstrapVue, но в будущем могут быть переписаны и на другую систему, при этом основной код страниц менять не придётся.
Для примера давайте разберёмся как именно работает раздел с пользователями

Вывод списка моделей

На странице frontend/src/admin/pages/users.vue мы встречаем первый основной копонент - vesp-table, который расширяет таблицу из BootstrapVue, принимает её основные параметры и добавляет свои:
<template>
  <div>
    <vesp-table
      :url="адрес контроллера, например admin/users"
      :header-actions="массив с действиями в шапке таблицы"
      :table-actions="массив с действиями в строках таблицы"
      :fields="массив выводимых колонок"
      :filters="массив для фильтрации запросов, обычно там просто поиск"
      :limit=`количество результатов на странице`
      :sort="сортировка"
      :dir="направление сортировки"
      :row-class="метод для определения CSS классов строки"
      :on-load="метод для изменение полученных данных перед выводом"
    />
    <!--
    Если у страницы есть дочерние маршруты (создание/изменение моделей),
    то не забываем указать этот тег, для вставки содержимого дочерней страницы
    -->
    <nuxt-child />
  </div>
</template>
У копонента всего один обязательный параметр для работы - url. Если убрать все остальные параметры, то вы увидите вот такую таблицу:
Теперь смотрим на javascript методы страницы:
// Объявляем экспорт адреса контроллера, для дочерних страниц
export const url = 'admin/users'

export default {
  name: 'UsersPage', // имя страницы
  // Проверка прав доступа к странице
  validate({app}) {
    // Этот метод вызывается Nuxt перед отрисовкой страницы, и является статичным
    // В нём еще нет никакого this, но в него передаётся объект со всем нужным
    // Так что здесь мы используем app.$hasScope вместо обычного this.$hasScope

    // Эта страница требует право доступа users
    return app.$hasScope('users')
  },
  data() {
    // Основные данные компонента
    return {
      // возвращаем ранее объявленный url, чтобы его можно было использовать в template
      url,
      // массив фильтров таблицы
      filters: {
        // таблица знает только этот вариант - и выводит для него строку поиска
        query: '',
      },
      // сортировка
      sort: 'id',
      dir: 'asc',
    }
  },
  head() {
    // здесь возврщаются мета-теги страницы
    return {
      // в нашем случае только title
      title: [this.$t('models.user.title_many'), this.$t('project')].join(' / '),
    }
  },
  // следующие значения вычисляются автоматически и зависят от других значений
  computed: {
    // таблица с юзерами показывает если только мы не перешли на
    // дочерний адрес с выводом групп пользователей
    isActive() {
      return !this.$route.name.includes('roles')
    },
    // Кнопки в заголовке таблицы
    headerActions() {
      return [
        // маршрут кнопки, иконка из FontAwesome, название
        // и вариант оформления из Bootstrap
        {route: 'users-create', icon: 'plus', title: this.$t('actions.create')},
        {route: 'users-roles', icon: 'users', title: this.$t('models.user_role.title_many'), variant: 'info'},
      ]
    },
    // Кнопки в строке таблицы: может быть адрес или фунция
    // Функция onDelete является встроенной
    tableActions() {
      return [
        {route: 'users-edit-id', icon: 'edit', title: this.$t('actions.edit')},
        {function: 'onDelete', icon: 'times', title: this.$t('actions.delete'), variant: 'danger'},
      ]
    },
    // Колонки таблицы
    fields() {
      // Ключ в массиве полученных из контроллера данных, заголовок, 
      // возможность сортировки при клике и форматирование значения
      return [
        {key: 'id', label: this.$t('components.table.columns.id'), sortable: true},
        {key: 'username', label: this.$t('models.user.username'), sortable: true},
        {key: 'fullname', label: this.$t('models.user.fullname'), sortable: true},
        {key: 'role.title', label: this.$t('models.user.role')},
        {
          key: 'created_at',
          // запись из стандартного лексикона
          label: this.$t('components.table.columns.created_at'),
          // это встроенное форматирование даты
          formatter: this.$options.filters.datetime,
          sortable: true,
        },
      ]
    },
  },
  methods: {
    // Неактивные юзеры выводятся серым цветом
    rowClass(item) {
      return item && !item.active ? 'text-muted' : ''
    },
  },
}
Я использую computed значения для настроек таблицы, потому что админка у нас мультиязычная и при переключении языка внизу страницы всё оформление мгновенно сменит язык - потому что computed значения изменятся в соответствии с новым языком и таблица перерисуется. Магия VueJS!
Сами лексиконы находятся в src/admin/lexicons и загружаются через файл index.js, который соединяет стандартные записи из Vesp с вашими, пользовательскими. Для работы используется @nuxtjs/i18n.

Принцип работы

Тут всё просто:
  • Таблица рендерится на страницу: шапка с кнопками и поиском
  • Сразу же делается запрос на сервер по указанному url
  • В ответ ожидается массив с total и rows, ровно как и выдают контроллеры Vesp
  • Если указан параметр on-load, то весь ответ передаётся в него, и там вы можете пройтись по rows и что-то поменять. В овет обязательно нужно вернуть массив в том же формате.
  • Затем в таблице отрисовываются полученные данные, если они есть.
  • К ним добавляются кнопочки, если указаны
Дальше при любом изменении параметров данные снова будут запрошены с сервера и таблица перерисуется - так и работает поиск, например. Путём ввода буквы в строку поиска вы меняете внутренний параметр filters.query и это ведёт к новому запросу на сервер.
А там, в контроллере, вы принимаете этот параметр и фильтруете записи:
protected function beforeCount(Builder $c): Builder
{
    if ($query = $this->getProperty('query')) {
        $c->where(
            static function (Builder $c) use ($query) {
                $c->where('username', 'LIKE', "%{$query}%");
                $c->orWhere('fullname', 'LIKE', "%{$query}%");
            }
        );
    }

    return $c;
}

Оформление строк

Vesp-table позволяет изменить значения в таблице аж 3мя способами:
  1. Метод on-load который меняет сами данные
  2. Указание метода formatter при опреледении колонки в fields()
  3. Указание специального template в шаблоне таблицы
Таблица использует особое указание шаблонов с именем колонки в скобках, например #cell(username). Это не является стандартным в Vue, оно было придумано специально для таблицы.
Мы же можем использовать эти шаблоны для сложного индивидуального оформления, например вывести аватарку пользователя через свой компонент user-avatar:
<vesp-table :url="url" ...>
    <template #cell(username)="{item}">
      <user-avatar :user="item" />
      {{ item.username }}
  </template>
</vesp-table>
Так это сделано в админке на моём сайте: Подробнее про шаблоны таблицы советую почитать в документации.

Заключение

Vesp-table является мощным и гибким средством вывода списка моделей из контроллеров Vesp, мы будем постоянного его использовать.
Более подробно устройство vesp-table можно посмотреть в исходниках компонента.
На следующем уроке будем создавать и менять модели во всплывающих модальных окнах.

2 комментария

Александр Наумов
На странице frontend/src/admin/users.vue
Наверное, все-таки: frontend/src/admin/pages/users.vue ?
Василий Наумкин
Спасибо, поправил!
bezumkin.ru
Personal website of Vasily Naumkin
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Futuris
04.04.2024, 08:56:12
Я просто немного запутался. Когда в абзаце "Vesp/Core" ты пишешь про "новый trait FileModel", я поду...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!