Motyw
Layouty i strony
Layouty
| Layout | Zastosowanie | Zawartość |
|---|---|---|
layouts/MasterLayout.vue | Wszystkie ekrany panelu | Nagłówek, menu boczne i kontener strony |
layouts/AuthLayout.vue | Logowanie i reset hasła | Ekran dwukolumnowy z tłem |
layouts/BlankLayout.vue | Strona 404 i ekrany bez nawigacji | Minimalna 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: truewquasar.config.js, ładuje dyski mediów jeszcze przed wyrenderowaniem panelu i tylko dla użytkownika z odpowiednim uprawnieniem.:keynarouter-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.
| Prop | Typ | Domyślnie | Opis |
|---|---|---|---|
title | String | "" | Nagłówek strony |
breadcrumbs | Array | [] | Elementy w postaci { label, to? } |
titleWidth | String | "auto" | Szerokość bloku tytułu, od 768 px |
titleEllipsisClass | String | "ellipsis" | Klasa odpowiedzialna za przycinanie tytułu |
useTitleEllipsis | Boolean | false | Włącza przycinanie tytułu |
backLink | String | Object | null | Cel przycisku powrotu umieszczonego obok tytułu |
previousRoute | Boolean | false | Zamiast 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 docelowe | Definicja | Źródło treści |
|---|---|---|
#base-page-top-right | BasePage | BaseTable i BaseForm poprzez slot base-page-top-right |
#header-breadcrumbs | TheHeader, ekrany od md wzwyż | BasePage na podstawie propa breadcrumbs |
#mobile-breadcrumbs | TheHeader, ekrany poniżej md | jak 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ę.
Menu nawigacyjne
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",
},
],
}| Pole | Wymagane | Opis |
|---|---|---|
name | tak | Etykieta, zwykle pochodząca z menu-items.* |
to | nie | Cel nawigacji; pominięcie czyni z pozycji nagłówek grupy |
icon | nie | Wyłącznie na najwyższym poziomie |
access | nie | Pominięcie oznacza pozycję zawsze widoczną |
children | nie | Podpozycje |
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
| Komponent | Rola |
|---|---|
TheNavigationDrawer | Spłaszcza [...modules, ...settings] i renderuje listę |
NavigationDrawerItem | Filtruje po uprawnieniach, obsługuje rozwijanie i stan aktywny |
DashboardMenuTiles | Prezentuje 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
| Prop | Typ | Domyślnie |
|---|---|---|
title | String | wymagany |
message | String | wymagany |
icon | String | "" |
hideIcon | Boolean | true |
confirmText | String | buttons.confirm |
cancelText | String | buttons.cancel |
confirmDanger | Boolean | false; 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
| Komponent | Zastosowanie |
|---|---|
BaseBtn | Przycisk z ustawieniami przyjętymi w projekcie (unelevated, no-caps) |
BaseBreadcrumbs | Breadcrumbs, uzupełniane o pozycję „Home" |
BaseFormSection, BaseFormSectionHeader, BaseSubSectionHeader | Podział formularza na sekcje |
BaseEmptyState, BaseErrorState, BaseStateOverlay | Stany puste oraz błędy |
BaseStatusChip, BaseVisibilityBadges, BasePublishedAtIcon, BaseIconsTd | Elementy dekoracyjne komórek tabeli |
BaseGallery, BaseMediaPreview, BaseFilePreviewTile | Podglądy mediów |
BaseDragHandle | Uchwyt przeciągania |
BaseScrollArea, BaseFeatherIcon, BasePersonCards | Pozostałe |