Skip to content

Layouty i strony ​

Layouty ​

LayoutZastosowanieZawartość
layouts/MasterLayout.vueWszystkie ekrany paneluNagłówek, menu boczne i kontener strony
layouts/AuthLayout.vueLogowanie i reset hasłaEkran dwukolumnowy z tłem
layouts/BlankLayout.vueStrona 404 i ekrany bez nawigacjiMinimalna otoczka
vue
<!-- src/layouts/MasterLayout.vue -->
<script setup>
defineOptions({
  async preFetch() {
    const { init: initDisks } = useMediaDiskStore();
    const authStore = useAuthStore();

    if (authStore.can("media.disks.index")) {
      return await initDisks();
    }
  },
});

const { locale, contentLocale } = useLocale();
const menuItems = useMenuItems();
</script>

<template>
  <q-layout view="lHh Lpr lff" class="base-layout">
    <the-header :menu-items="menuItems" />
    <q-page-container class="base-layout__main">
      <router-view :key="`master-${locale}-${contentLocale}`" class="base-layout__content" />
    </q-page-container>
  </q-layout>
</template>

Dwa elementy tego layoutu mają nieoczywiste działanie:

  • preFetch — włączona globalnie opcją preFetch: true w quasar.config.js, ładuje dyski mediów jeszcze przed wyrenderowaniem panelu i tylko dla użytkownika z odpowiednim uprawnieniem.
  • :key na router-view — wymusza przemontowanie stron przy zmianie języka interfejsu lub treści. Dzięki temu formularze wczytują wartości początkowe w nowym języku; kosztem są utracone niezapisane zmiany.

Powłoki stron ​

Strony pozostają cienkie — cała logika mieści się w komponentach tabel i formularzy.

BasePage ​

Komponent stanowiący podstawę wszystkich stron.

PropTypDomyślnieOpis
titleString""Nagłówek strony
breadcrumbsArray[]Elementy w postaci { label, to? }
titleWidthString"auto"Szerokość bloku tytułu, od 768 px
titleEllipsisClassString"ellipsis"Klasa odpowiedzialna za przycinanie tytułu
useTitleEllipsisBooleanfalseWłącza przycinanie tytułu
backLinkString | ObjectnullCel przycisku powrotu umieszczonego obok tytułu
previousRouteBooleanfalseZamiast backLink wykonuje cofnięcie w historii

Komponent udostępnia slot top-right.

BaseCardPage ​

Połączenie BasePage z płaską kartą na treść, stosowane na stronach list:

vue
<!-- src/modules/blog/pages/articles/ArticleIndexPage.vue -->
<script setup>
const breadcrumbs = useCoreBreadcrumbs("blog.articles.index");
</script>

<template>
  <base-card-page :title="$t('blog.articles.title.index')" :breadcrumbs="breadcrumbs">
    <blog-articles-table />
  </base-card-page>
</template>

BaseFormPage ​

Połączenie BasePage z BaseQueryState, stosowane na stronach edycji:

vue
<!-- src/modules/blog/pages/articles/ArticleEditPage.vue -->
<script setup>
import { useArticleDetailQuery } from "modules/blog/api/queries/article.queries";

const route = useRoute();
const id = computed(() => Number(route.params.id));
const query = useArticleDetailQuery(id);
const breadcrumbs = useCoreBreadcrumbs("blog.articles.edit");
</script>

<template>
  <base-form-page
    :query="query"
    :title="$t('blog.articles.title.edit')"
    :breadcrumbs="breadcrumbs"
  >
    <template #default="{ data }">
      <blog-article-form :current-values="data" />
    </template>
  </base-form-page>
</template>

Prop query przyjmuje obiekt zwracany przez useQuery, natomiast slot domyślny otrzymuje scope { data, isFetching }. Za spinner, błędy oraz baner nieświeżych danych odpowiada BaseQueryState, opisany w sekcji Vue Query.

Strona tworzenia nie korzysta z query

Ekran tworzenia rekordu używa BaseCardPage i renderuje formularz bez przekazywania current-values:

vue
<!-- src/modules/blog/pages/articles/ArticleCreatePage.vue -->
<base-card-page :title="$t('blog.articles.title.create')" :breadcrumbs="breadcrumbs">
  <blog-article-form />
</base-card-page>

Wzorzec teleportów ​

W layoucie i na stronie znajdują się trzy miejsca docelowe, do których komponenty potomne wstrzykują treść bez przekazywania propsów w górę:

Miejsce doceloweDefinicjaŹródło treści
#base-page-top-rightBasePageBaseTable i BaseForm poprzez slot base-page-top-right
#header-breadcrumbsTheHeader, ekrany od md wzwyżBasePage na podstawie propa breadcrumbs
#mobile-breadcrumbsTheHeader, ekrany poniżej mdjak wyżej

Dzięki temu przycisk tworzenia rekordu deklarowany jest wewnątrz tabeli, a pojawia się w nagłówku strony:

vue
<!-- src/modules/blog/components/BlogArticlesTable.vue -->
<template #base-page-top-right>
  <base-btn :label="$t('buttons.create')" :to="{ name: 'blog.articles.create' }" icon="add" />
</template>

Uzasadnienie tego rozwiązania

Przycisk tworzenia rekordu należy logicznie do tabeli, ponieważ to ona zna trasę i uprawnienia, natomiast wizualnie przynależy do nagłówka strony. Teleport pozwala pogodzić oba wymagania bez przekazywania slotów przez stronę.

Cała nawigacja opisana jest w jednym pliku src/menu-items.js, eksportującym composable, który zwraca computed z dwiema grupami:

js
// src/menu-items.js
export default function () {
  const { t } = useLocale();

  return computed(() => ({
    modules: [ /* ... */ ],
    settings: [ /* ... */ ],
  }));
}

Kształt pozycji ​

js
// src/menu-items.js
{
  icon: "article",                              // ikona Quasara lub feather_*
  name: t("menu-items.blog"),                   // etykieta
  to: { name: "blog.articles.index" },          // cel nawigacji
  access: "blog.articles.index",                // uprawnienie
  children: [                                   // opcjonalne podpozycje
    {
      name: t("blog.articles.title.index"),
      to: { name: "blog.articles.index" },
      access: "blog.articles.index",
    },
  ],
}
PoleWymaganeOpis
nametakEtykieta, zwykle pochodząca z menu-items.*
tonieCel nawigacji; pominięcie czyni z pozycji nagłówek grupy
iconnieWyłącznie na najwyższym poziomie
accessniePominięcie oznacza pozycję zawsze widoczną
childrenniePodpozycje

Etykiety najwyższego poziomu pochodzą z plików src/i18n/{en,pl}/menu-items.js i zapisane są w liczbie mnogiej, stąd wywołania t("menu-items.blog", 2) w większości wpisów.

Komponenty korzystające z konfiguracji ​

KomponentRola
TheNavigationDrawerSpłaszcza [...modules, ...settings] i renderuje listę
NavigationDrawerItemFiltruje po uprawnieniach, obsługuje rozwijanie i stan aktywny
DashboardMenuTilesPrezentuje tę samą konfigurację w formie kafelków na pulpicie

Stan aktywny wylicza useNestedRoutes z pliku src/composables/useNestedRoutes.js, porównując dwa pierwsze segmenty nazwy trasy. Z tego powodu trasa blog.articles.edit podświetla pozycję wskazującą na blog.articles.index.

Marker generatora w pliku menu-items.js

Komentarz // --place where generator will inject new menu items umieszczony jest w grupie modules, co opisują Moduły i generatory.

Dialogi ​

Dialogi otwierane są programistycznie, przy użyciu pluginu Dialog z Quasara — nie montuje się ich w szablonie.

Potwierdzenie usunięcia ​

js
// dowolny komponent wywołujący usunięcie rekordu
const { confirmDeletionDialog } = useDestroyableDialog();

confirmDeletionDialog(
  () => { /* potwierdzone */ },
  () => { /* anulowane — opcjonalne */ },
);

Tabele nie wymagają ręcznego wywołania

Wbudowana akcja delete w useCoreTable wyświetla potwierdzenie samodzielnie, co opisują Tabele.

BaseConfirmDialog ​

PropTypDomyślnie
titleStringwymagany
messageStringwymagany
iconString""
hideIconBooleantrue
confirmTextStringbuttons.confirm
cancelTextStringbuttons.cancel
confirmDangerBooleanfalse; wartość true nadaje przyciskowi potwierdzenia kolor czerwony
js
import { useQuasar } from "quasar";
import BaseConfirmDialog from "modules/core/components/ui/BaseConfirmDialog.vue";

const { dialog } = useQuasar();

dialog({
  component: BaseConfirmDialog,
  componentProps: {
    title: t("dialogs.deleting.title"),
    message: t("dialogs.deleting.message"),
    confirmDanger: true,
  },
}).onOk(() => { /* ... */ });

Własny dialog ​

js
const openDialog = useFormDialog(MyDialogComponent, onOk, onCancel, onDismiss);

openDialog({ componentProps: { itemId: 5 } });

Komponent dialogu musi korzystać z useDialogPluginComponent() z Quasara.

Globalny spinner ​

js
const loading = useLoading();

loading.show();
loading.hide();
loading.toggle();

Zapis formularza steruje spinnerem samodzielnie

Composable useFormSubmit obsługuje spinner we własnym zakresie — nie wywołuj loading równolegle podczas zapisu. Opisują to Formularze.

Komponenty pomocnicze ​

KomponentZastosowanie
BaseBtnPrzycisk z ustawieniami przyjętymi w projekcie (unelevated, no-caps)
BaseBreadcrumbsBreadcrumbs, uzupełniane o pozycję „Home"
BaseFormSection, BaseFormSectionHeader, BaseSubSectionHeaderPodział formularza na sekcje
BaseEmptyState, BaseErrorState, BaseStateOverlayStany puste oraz błędy
BaseStatusChip, BaseVisibilityBadges, BasePublishedAtIcon, BaseIconsTdElementy dekoracyjne komórek tabeli
BaseGallery, BaseMediaPreview, BaseFilePreviewTilePodglądy mediów
BaseDragHandleUchwyt przeciągania
BaseScrollArea, BaseFeatherIcon, BasePersonCardsPozostałe

Made by Moonbite