Motyw
Media
Moduł media udostępnia bibliotekę plików, obejmującą dyski, katalogi, upload i kadrowanie, a także gotowe pola formularza służące do podpinania plików pod modele.
Dwa przepływy
| Przepływ | Zastosowanie | Komponent |
|---|---|---|
| Wybór z biblioteki | Rozwiązanie standardowe: plik trafia najpierw do biblioteki, a następnie zostaje podpięty do rekordu | VFileCollection, VImageCollection |
| Upload bezpośredni | Plik należy wyłącznie do danego rekordu i nie ma trafiać do biblioteki | VMediaUploadField |
Przepływ pierwszy — wybór z biblioteki
Konfiguracja w formularzu
js
// src/modules/blog/forms/article.form.js
initialValues() {
return {
// ...
media_files: getImageCollectionInitialValues("main_picture"),
};
}Funkcja getImageCollectionInitialValues(prefix = "image") zwraca obiekt o postaci:
js
{ [`${prefix}_desktop`]: [], [`${prefix}_mobile`]: [] }vue
<!-- src/modules/blog/components/BlogArticleForm.vue -->
<v-image-collection :label="$t('labels.main_picture')" class="col-12" />Komponent VImageCollection
Renderuje dwa pola umieszczone obok siebie: wersję desktopową oraz mobilną.
| Prop | Domyślnie | Opis |
|---|---|---|
label | null | Etykieta wyświetlana nad parą pól |
nameDesktop | "media_files.image_desktop" | Ścieżka pola desktopowego |
nameMobile | "media_files.image_mobile" | Ścieżka pola mobilnego |
stacked | false | Wartość true układa pola jedno pod drugim |
Nazwy pól muszą odpowiadać wartościom początkowym
Domyślne nazwy zakładają prefiks image. Używając getImageCollectionInitialValues("main_picture"), podaj je jawnie:
vue
<v-image-collection
name-desktop="media_files.main_picture_desktop"
name-mobile="media_files.main_picture_mobile"
/>Komponent VFileCollection
Pojedyncze pole kolekcji plików. Przyjmuje prop name i przekazuje pozostałe propsy do komponentu MediaFileCollection:
| Prop | Typ | Domyślnie | Opis |
|---|---|---|---|
label | String | — | Etykieta |
btnLabel | String | — | Tekst przycisku, domyślnie pobierany z media.files.labels.file |
btnIcon | String | "feather_upload-cloud" | Ikona przycisku |
description | String | — | Tekst pomocniczy |
type | String | Array | — | Ograniczenie typu pliku |
extension | String | Array | — | Ograniczenie rozszerzenia |
multiple | Boolean | false | Umożliwia wybór wielu plików |
limit | Number | — | Maksymalna liczba plików |
cropperProps | Object | — | Konfiguracja kadrowania, opisana poniżej |
disk | String | — | Wymusza wybrany dysk |
disable, readonly | Boolean | false | Blokada edycji |
Modelem jest tablica obiektów plików. Przy włączonej opcji multiple pliki można przestawiać metodą przeciągnij i upuść. Slot button otrzymuje scope { showMediaManager, isEmpty }.
Otwieranie menedżera z poziomu kodu
js
// dowolny komponent otwierający menedżer mediów
const { show } = useMediaManager({
multiple: false,
limit: 1,
type: "image",
cropperProps: { maxWidth: 1920, desiredWidth: 800 },
});
const files = await show({ selectedCollection: model.value });Metoda show() zwraca obiekt Promise rozwiązywany tablicą wybranych plików. Każdy plik zawiera dodatkowy klucz coordinates, przyjmujący postać obiektu { width, height, x, y } przy kadrowaniu albo wartość null, gdy użytkownik wybrał dodanie oryginału.
Przepływ drugi — upload bezpośredni
vue
<!-- komponent formularza korzystającego z uploadu bezpośredniego -->
<v-media-upload-field
media-name="media.main"
upload-name="upload.main"
:label="$t('labels.icon')"
size="icon"
/>| Prop | Domyślnie | Opis |
|---|---|---|
mediaName | "media.main" | Ścieżka pola z plikami już zapisanymi |
uploadName | "upload.main" | Ścieżka pola z nowymi obiektami File |
multiple | false | Umożliwia wybór wielu plików |
accept | image/png,image/jpeg,image/gif,image/svg+xml | Dopuszczalne typy |
size | — | Wartość "icon" albo "image", określająca rozmiar podglądu |
label, buttonLabel, disable | — | — |
Komponent wymaga dwóch osobnych pól formularza
Operuje na dwóch ścieżkach: istniejące media i nowe pliki. Obie muszą występować w initialValues, a backend musi przyjmować jedną i drugą. Do wysyłki służy objectToFormData, opisany w Warstwie API.
Menedżer mediów
| Komponent | Rola |
|---|---|
MediaManager | Widok dwupanelowy: drzewo katalogów oraz tabela plików |
MediaManagerDialog | Opakowanie MediaManager w dialog Quasara |
MediaDirectoryTree | Wybór dysku, uploader i drzewo katalogów wraz z operacjami |
MediaFilesTable | Siatka plików, zaznaczanie i operacje masowe |
MediaFileUploader | Komponent q-uploader z auto-uploadem i obsługą przeciągania |
MediaImageCropperDialog | Kadrowanie oparte na vue-advanced-cropper |
MediaFileUpdateForm | Edycja metadanych pliku |
MediaDirectorySelectDialog, MediaDirectoryDestroyDialog | Operacje na katalogach |
Komponent MediaManager przyjmuje prop filePickerMode i emituje zdarzenie file-picked.
Upload
Konfigurację uploadera buduje funkcja uploaderFactory() z pliku modules/media/composables/media.js:
js
// src/modules/media/composables/media.js
{
url: `${process.env.API_BASE_URL}/media/files/d/${diskStore.currentDiskName}`,
fieldName: "file",
formFields: [{ name: "path", value: directoryStore.currentDirectory?.path }],
headers: [
{ name: "X-XSRF-TOKEN", value: Cookies.get("XSRF-TOKEN") },
{ name: "Authorization", value: `Bearer ${authStore.token}` },
{ name: "Accept", value: "application/json" },
],
withCredentials: true,
}Dlaczego upload omija instancję Http
Komponent q-uploader wysyła pliki własnym mechanizmem, więc nagłówki uwierzytelniające trzeba podać jawnie. Z tego samego powodu błędy uploadu obsługuje osobno handleValidationErrors, a nie interceptor axiosa.
Kadrowanie
Komponent MediaImageCropperDialog opakowuje <Cropper> z biblioteki vue-advanced-cropper.
| Prop | Opis |
|---|---|
file | Wymagany; plik przeznaczony do kadrowania |
cropperProps | Przekazywane do <Cropper>; dodatkowo obsługiwane są maxWidth, maxHeight, desiredWidth oraz desiredHeight |
Dialog kończy się na dwa sposoby:
| Wybór użytkownika | Wywołanie |
|---|---|
| Zatwierdzenie kadrowania | onDialogOK(coordinates) z obiektem { width, height, x, y } |
| Dodanie oryginału | onDialogOK(null) |
Współrzędne wracają razem z plikiem i służą później do zbudowania adresu URL konwersji.
Budowanie adresów URL
js
// dowolny komponent prezentujący media
const { original, conversion, isImage } = useMediaConversion();
original(file);
// → file.url, np. https://host/storage/2024/foo.jpg
conversion(file).crop(800, 600, 10, 20).resize(400, 300).build();
// → https://host/media/crop_800_600_10_20,resize_400_300/2024/foo.jpgŁańcuch wywołań obejmuje metody resize(w, h), crop(w, h, x, y) oraz fit(w, h), zakończone wywołaniem build().
Panel nie zna układu tras. Adresy przychodzą z API w zasobie pliku:
| Pole | Zastosowanie |
|---|---|
url | wyświetlanie, kopiowanie adresu, wstawianie do edytora |
conversion_url | szablon z {pipes}, który wypełnia build(); null dla plików bez konwersji (np. SVG), wtedy build() zwraca url |
original_url | źródło dla MediaImageCropperDialog; idzie przez API, bo kadrowanie pod inną domeną wymaga nagłówków CORS |
Dyski prywatne
Adresy plików z dysku o wartości visibility === "private" prowadzą przez API i mają już doklejony parametr ?signature=<uuid pliku> — nie buduj ich ręcznie.
Funkcja isImage(extension) weryfikuje rozszerzenie względem wzorca jpg|jpeg|png|gif|ico|bmp|svg.
Podglądy
js
const { preview, thumb, isSvg, isImage, fileIcon } = useMediaImagePreview(fileRef, thumbSize);Za normalizację do wspólnego kształtu podglądu odpowiada plik modules/media/services/preview-item.js:
js
mediaToPreviewItem(item); // z zapisanego rekordu media
uploadToPreviewItem(file, objectUrl); // z lokalnego obiektu File
// → { src, thumb, name, kind }Wartość kind wyznacza funkcja resolveFileKind z modules/core/utils/file-kind.js, przyjmująca wartości FILE_KIND.image, video lub file. Odpowiadającą ikonę zwraca fileKindIcon.
Do gotowych komponentów podglądu należą BaseGallery, BaseMediaPreview, BaseFilePreviewTile, MediaFileSimplePreview oraz MediaFileCollectionFilePreview.
Composables
| Composable | Zwracane wartości |
|---|---|
useMediaManager(props) | { show } — otwiera dialog wyboru |
useMediaFile() | { store, additionalParams, forceRefresh, uploaderFactory, destroyMany, copy, move, refresh } |
useMediaDirectory() | { current, root, directories, flatDirectories, rename, create, destroy, isCurrent, setCurrentAsRoot, fetchDirectories } |
useMediaDisk() | { store, current, disks, setDisk } |
useMediaConversion() | { original, conversion, isImage } |
useMediaImagePreview(file, thumbSize) | { preview, thumb, isSvg, isImage, fileIcon } |
Wszystkie podlegają auto-importowi.
Store'y i inicjalizacja
| Store | Zawartość |
|---|---|
media/disk | Lista dysków, defaultDisk, currentDiskName oraz metoda init() |
media/directory | Drzewo katalogów i currentDirectory |
media/file | Kontekst listy plików (additionalParamsWithDirectory) |
Dyski ładowane są jednorazowo, w funkcji preFetch layoutu:
js
// src/layouts/MasterLayout.vue
if (authStore.can("media.disks.index")) {
return await initDisks();
}Menedżer wymaga załadowanych dysków
Otwierając menedżer poza MasterLayout — na przykład w dialogu na ekranie logowania — wywołaj najpierw useMediaDiskStore().init(). Bez wartości currentDiskName upload nie ma określonego adresu docelowego.