Skip to content

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ływZastosowanieKomponent
Wybór z bibliotekiRozwiązanie standardowe: plik trafia najpierw do biblioteki, a następnie zostaje podpięty do rekorduVFileCollection, VImageCollection
Upload bezpośredniPlik należy wyłącznie do danego rekordu i nie ma trafiać do bibliotekiVMediaUploadField

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ą.

PropDomyślnieOpis
labelnullEtykieta wyświetlana nad parą pól
nameDesktop"media_files.image_desktop"Ścieżka pola desktopowego
nameMobile"media_files.image_mobile"Ścieżka pola mobilnego
stackedfalseWartość 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:

PropTypDomyślnieOpis
labelString—Etykieta
btnLabelString—Tekst przycisku, domyślnie pobierany z media.files.labels.file
btnIconString"feather_upload-cloud"Ikona przycisku
descriptionString—Tekst pomocniczy
typeString | Array—Ograniczenie typu pliku
extensionString | Array—Ograniczenie rozszerzenia
multipleBooleanfalseUmożliwia wybór wielu plików
limitNumber—Maksymalna liczba plików
cropperPropsObject—Konfiguracja kadrowania, opisana poniżej
diskString—Wymusza wybrany dysk
disable, readonlyBooleanfalseBlokada 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"
/>
PropDomyślnieOpis
mediaName"media.main"Ścieżka pola z plikami już zapisanymi
uploadName"upload.main"Ścieżka pola z nowymi obiektami File
multiplefalseUmożliwia wybór wielu plików
acceptimage/png,image/jpeg,image/gif,image/svg+xmlDopuszczalne 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 ​

KomponentRola
MediaManagerWidok dwupanelowy: drzewo katalogów oraz tabela plików
MediaManagerDialogOpakowanie MediaManager w dialog Quasara
MediaDirectoryTreeWybór dysku, uploader i drzewo katalogów wraz z operacjami
MediaFilesTableSiatka plików, zaznaczanie i operacje masowe
MediaFileUploaderKomponent q-uploader z auto-uploadem i obsługą przeciągania
MediaImageCropperDialogKadrowanie oparte na vue-advanced-cropper
MediaFileUpdateFormEdycja metadanych pliku
MediaDirectorySelectDialog, MediaDirectoryDestroyDialogOperacje 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.

PropOpis
fileWymagany; plik przeznaczony do kadrowania
cropperPropsPrzekazywane do <Cropper>; dodatkowo obsługiwane są maxWidth, maxHeight, desiredWidth oraz desiredHeight

Dialog kończy się na dwa sposoby:

Wybór użytkownikaWywołanie
Zatwierdzenie kadrowaniaonDialogOK(coordinates) z obiektem { width, height, x, y }
Dodanie oryginałuonDialogOK(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:

PoleZastosowanie
urlwyświetlanie, kopiowanie adresu, wstawianie do edytora
conversion_urlszablon 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 ​

ComposableZwracane 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 ​

StoreZawartość
media/diskLista dysków, defaultDisk, currentDiskName oraz metoda init()
media/directoryDrzewo katalogów i currentDirectory
media/fileKontekst 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.

Made by Moonbite