GrFilePreview

  • ядро
  • Данные
  • API · 12 props

Берут, когда лента вложений.

import { GrFilePreview } from '@feugene/granularity/components/GrFilePreview'

Обзор

Когда брать

  • лента вложений — чеки, договоры, выгрузки: в наборе вперемешку картинки и документы, и по одному правилу их не покажешь;
  • тип файла заранее неизвестен — контроллер отдаёт варианты без фильтра, и <img> на PDF рисует битую иконку;
  • превью открывает просмотрщик — плитка эмитит click, окно показывает потребитель;
  • плиток на странице десяток — ленивая загрузка и держащее место соотношение сторон уже внутри.

Когда взять другое

НужноБерите
Выбрать файл и отправить его на серверGrFileUpload
Файл как значение поля формыGrFormFile
Открыть картинку во весь экранGrImageViewer
Аватар человека или сущностиGrAvatar
Показать содержимое файла текстомGrCodeBlock (пакет @feugene/granularity-code)

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Вложения авансового отчёта

    mime решает, какой будет плитка: чеки — фото, счёт — PDF, суточные — таблица, посадочные — архив, а тип, которого сервер не знает, получает заглушку, а не дыру. Плитка просмотрщик не открывает — страница отдаёт в GrImageViewer ровно те файлы, которые isPreviewableKind(fileKindOf(mime)) называет картинками, и только их делает clickable.

    Client visit, Lisbon · 14–16 Oct6 attachments · waiting for approval
    412 KB
    hotel-invoice.pdf
    186 KB
    398 KB
    per-diem.xlsx
    24 KB
    boarding-passes.zip
    1.1 MB
    bank-export.dat
    8 KB
  • Фото объявления одной формы

    Агенты загружают горизонтальные и вертикальные снимки вперемешку; ratio="4:3" даёт им одну форму, и ряд выстраивается, а число в tile-size — выход за пределы шкалы. name становится alt каждой фотографии — плитка описание не выдумывает, — а картинки грузятся лениво и держат место.

    2-room flat in Alfama · €1,450 a monthRua dos Remédios 42, 3rd floor · 64 m²
  • Превью, которое больше не открывается

    В переписке поддержки tile-size="sm" вписывает плитку в сообщение. Первый скриншот удалён по сроку хранения: его адрес не открывается, и плитка рисует «изображение не открылось» вместо битой картинки. Лог прошёл через шлюз, который говорит только application/octet-stream, — это заглушка с именем, а не пустая рамка.

    #T-1093 · Export to CSV fails
    MO
    Mina Ortiz · Customer · Mon 09:14The export fails with a red banner every time. Screenshot and the log attached.
    export.log
    DK
    Daniel Kim · Support · Mon 10:02Thanks — the first screenshot did not survive retention. Could you send it again?
    MO
    Mina Ortiz · Customer · Mon 10:20Here it is.

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
ariaLabelstring | undefinedundefinedДоступное имя интерактивной плитки. Не задано — имя приходит из содержимого: `alt` картинки или подписи заглушки.
loading"lazy" | "eager" | undefinedundefinedНативный `loading` картинки: `lazy` грузит превью у края вьюпорта, а не все плитки ленты сразу. Не задан — из `GrConfigProvider`, иначе `lazy`.
namestring | null | undefinedundefinedИмя файла: доступное имя картинки и подпись заглушки.
srcstring | null | undefinedundefinedАдрес превью. Пусто — сразу заглушка по типу.
asstring | Component | undefinedundefinedСвой корневой тег: компонент-ссылка (`RouterLink`, `Link` от Inertia) или тег ради разметки страницы (`article`, `figure`). Сильнее `href`. Неинтерактивный тег — только замена `div`: ни курсора, ни кольца фокуса.
hrefstring | undefinedundefinedСсылка на оригинал — для не-картинок и для перехода мимо просмотрщика.
targetstring | undefinedundefinedКуда открывать ссылку. Объявлен пропом, а не оставлен на fallthrough: иначе `_blank` уезжал бы на `<a>` мимо компонента, и защитный `rel` подставить было бы негде.
relstring | undefinedundefinedСвоё значение `rel`. Задано — отменяет автоподстановку.
clickableboolean | undefinedfalseПлитка кликабельна и эмитит `click` — обычно чтобы открыть просмотрщик.
mimestring | null | undefinedundefinedMIME-тип. Он решает, картинка это или файл.
tileSizeGrSizeWithPx | undefinedundefinedСтупень канонической шкалы либо произвольная ширина в пикселях. Число — escape-hatch, как диаметр у GrAvatar: плитка 96px в ленте вложений в четыре ступени не укладывается.
ratioGrFilePreviewRatio | undefinedundefinedПропорции плитки: держат место до загрузки, картинка обрезается по ним (`object-cover`). Не задан — из `GrConfigProvider`, иначе `1:1`.

События

EventСигнатураОписание
click(event: "click", event: MouseEvent): void—

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrFileKind, GrFilePreviewProps, GrFilePreviewEmits } from '@feugene/granularity/components/GrFilePreview'

Файлdist/types/components/GrFilePreview/config.d.ts

grFilePreviewConfigconst
const grFilePreviewConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFilePreview">;

Файлdist/types/components/GrFilePreview/fileKindOf.d.ts

GrFileKindtype

Вид файла по MIME-типу. Тип решает **mime, а не расширение**: расширение врёт (.dat у выгрузки, .pdf у переименованного архива), а бэкенд отдаёт настоящий тип — брать надо его. От вида зависит единственное решение компонента: рисовать <img> или иконку.

type GrFileKind = 'image' | 'pdf' | 'document' | 'spreadsheet' | 'archive'
fileKindOffunction
function fileKindOf(mime: string | null | undefined): GrFileKind;
isPreviewableKindfunction

Картинку показываем, остальное — подписываем иконкой.

function isPreviewableKind(kind: GrFileKind): boolean;

Файлdist/types/components/GrFilePreview/GrFilePreview.vue.d.ts

GrFilePreviewPropsinterface

Плитка превью файла с фолбэком по типу. Показывает **сохранённый** файл, а не отправляет новый: отправка — это GrFormFile и GrFileUpload. Просмотрщик во весь экран открывает потребитель по событию click.

interface GrFilePreviewProps {
    /** Адрес превью. Пусто — сразу заглушка по типу. */
    src?: string | null;
    /** MIME-тип. Он решает, картинка это или файл. */
    mime?: string | null;
    /** Имя файла: доступное имя картинки и подпись заглушки. */
    name?: string | null;
    /**
     * Ступень канонической шкалы либо произвольная ширина в пикселях. Число —
     * escape-hatch, как диаметр у GrAvatar: плитка 96px в ленте вложений в
     * четыре ступени не укладывается.
     */
    tileSize?: GrSizeWithPx;
    /**
     * Пропорции плитки: держат место до загрузки, картинка обрезается по ним (`object-cover`).
     * Не задан — из `GrConfigProvider`, иначе `1:1`.
     */
    ratio?: GrFilePreviewRatio;
    /** Ссылка на оригинал — для не-картинок и для перехода мимо просмотрщика. */
    href?: string;
    /**
     * Куда открывать ссылку. Объявлен пропом, а не оставлен на fallthrough:
     * иначе `_blank` уезжал бы на `<a>` мимо компонента, и защитный `rel`
     * подставить было бы негде.
     */
    target?: string;
    /** Своё значение `rel`. Задано — отменяет автоподстановку. */
    rel?: string;
    /**
     * Свой корневой тег: компонент-ссылка (`RouterLink`, `Link` от Inertia) или
     * тег ради разметки страницы (`article`, `figure`). Сильнее `href`.
     * Неинтерактивный тег — только замена `div`: ни курсора, ни кольца фокуса.
     */
    as?: string | Component;
    /** Плитка кликабельна и эмитит `click` — обычно чтобы открыть просмотрщик. */
    clickable?: boolean;
    /**
     * Нативный `loading` картинки: `lazy` грузит превью у края вьюпорта, а не все
     * плитки ленты сразу. Не задан — из `GrConfigProvider`, иначе `lazy`.
     */
    loading?: 'lazy' | 'eager';
    /**
     * Доступное имя интерактивной плитки. Не задано — имя приходит из содержимого:
     * `alt` картинки или подписи заглушки.
     */
    ariaLabel?: string;
}
GrFilePreviewEmitsinterface
interface GrFilePreviewEmits {
    /** Клик по плитке — здесь открывают просмотрщик. С клавиатуры приходит у `clickable` и ссылки. */
    (e: 'click', event: MouseEvent): void;
}

Файлdist/types/components/GrFilePreview/grFilePreviewStyles.d.ts

GrFilePreviewRatiotype

Соотношение сторон плитки.

type GrFilePreviewRatio = '1:1' | '4:3' | '16:9';

Файлdist/types/components/GrFilePreview/defaults.d.ts

GrFilePreviewConfigurablePropsinterface

Пропы GrFilePreview, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrFilePreview: { … } }">. Только оформление: src, mime и name принадлежат файлу. Размер и пропорции здесь потому, что лента вложений выглядит одинаково на всех страницах, и повторять решение у каждой плитки значит однажды разойтись.

interface GrFilePreviewConfigurableProps {
    tileSize: GrSizeWithPx;
    ratio: GrFilePreviewRatio;
    loading: 'lazy' | 'eager';
}

Файлdist/types/components/GrFilePreview/safelist.d.ts

grFilePreviewSafelistconst
const grFilePreviewSafelist: string[];

Использование из прозы библиотеки

Тип решает mime, а не расширение

Расширение врёт: .dat у выгрузки, .pdf у переименованного архива. Бэкенд отдаёт настоящий тип — берётся он.

Видов шесть: картинка, PDF, документ, таблица, архив и неопознанное. Картинка — единственный, который рисуется <img>; остальные получают иконку и подпись. Пустой mime, application/octet-stream и незнакомый тип дают заглушку, а не пустоту: «типа нет» — обычное состояние строки в БД, а не повод показать дыру.

text/csv разбирается как таблица, а не как текст: открывается он таблицей.

Классификация публичная: fileKindOf(mime) и isPreviewableKind(kind) отдаются из пакета. Она нужна снаружи ровно потому, что плитка просмотрщик не открывает (см. «Границы»): решать, какие файлы отдать в GrImageViewer, приходится потребителю — и без этих функций он заводит свой mime.startsWith('image/'), который расходится с плиткой на первом же новом типе.

Заглушка вместо сломанной картинки

Три пути ведут в одну и ту же заглушку, и это осознанно:

  • тип не картинка;
  • src пуст;
  • загрузка сорвалась (onerror) — превью могло исчезнуть с диска.

В последнем случае значок другой — «изображение не открылось», а не «это файл»: разница между «файл такого рода» и «картинка была, но не доехала» видна сразу. Новый src ошибку не наследует.

alt не выдумывается

Задано name — оно и становится alt. Не задано — картинка декоративна (alt=""), потому что придуманное компонентом описание диктор прочитает как факт, и это хуже пустого.

У заглушки name печатается подписью под иконкой, а сама иконка скрыта от диктора: текст уже сказал всё.

Интерактивная плитка берёт имя из содержимого — alt картинки или подписи. Если содержимое безымянно, имя задаётся ariaLabel: кнопка без имени для скринридера пуста.

Плитка кликабельна только когда её попросили

Без clickable и href это картинка, а не контрол: плитка не занимает остановку Tab и не получает курсор — пустая остановка хуже её отсутствия.

Порядок выбора тега — as → <a href> → <button clickable> → <div>, как у GrCard и GrLink. Компонент-ссылка (Link от Inertia, RouterLink) получает href; строковый тег, кроме a, — нет.

as называет тег, а не поведение. as="article" или as="figure" меняет разметку страницы и ничего больше: классы те же, что у <div>, курсора и кольца фокуса нет, родовое имя плитке не навязывается — link-name к <article> не применяется. Контролом плитку делают clickable, href и компонент-ссылка. Действие, повешенное на тег вне таб-порядка, кликается мышью и предупреждает в dev-сборке.

Размер и пропорции

tileSize — ступень канонической шкалы либо число: плитка 96px в ленте вложений в четыре ступени не укладывается. Числовой escape-hatch здесь по той же причине, что диаметр у GrAvatar.

ratio держит место до загрузки. Без него ряд плиток прыгает, когда картинки доезжают вразнобой.

Место держит скелет, а не пустота

Пока картинка едет, плитка показывает скелет. Состояний у неё три — «грузится», «готова», «не открылась», — и без среднего лента вложений врёт: пустая ячейка читается как «у файла нет превью», хотя запрос ещё в пути. На двух десятках плиток, приезжающих вразнобой, разница видна сразу.

Картинка при этом остаётся в дереве и просто ждёт невидимой: убери её оттуда — браузер не начнёт загрузку, и состояние «грузится» не кончится никогда.

Границы

Компонент не грузит файлы, не открывает просмотрщик сам и не генерирует превью для PDF: отрисовать первую страницу документа в браузере — задача отдельной библиотеки, и её вес несоразмерен плитке. Нужна миниатюра PDF — готовьте её на сервере и передавайте в src как картинку.