GrFilePreview
Берут, когда лента вложений.
Обзор
Когда брать
- лента вложений — чеки, договоры, выгрузки: в наборе вперемешку картинки и документы, и по одному правилу их не покажешь;
- тип файла заранее неизвестен — контроллер отдаёт варианты без фильтра, и
<img>на PDF рисует битую иконку; - превью открывает просмотрщик — плитка эмитит
click, окно показывает потребитель; - плиток на странице десяток — ленивая загрузка и держащее место соотношение сторон уже внутри.
Когда взять другое
| Нужно | Берите |
|---|---|
| Выбрать файл и отправить его на сервер | GrFileUpload |
| Файл как значение поля формы | GrFormFile |
| Открыть картинку во весь экран | GrImageViewer |
| Аватар человека или сущности | GrAvatar |
| Показать содержимое файла текстом | GrCodeBlock (пакет @feugene/granularity-code) |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrFilePreview>Текст</GrFilePreview>Примеры 3 у каждого — живое демо и его код
Вложения авансового отчёта
mimeрешает, какой будет плитка: чеки — фото, счёт — PDF, суточные — таблица, посадочные — архив, а тип, которого сервер не знает, получает заглушку, а не дыру. Плитка просмотрщик не открывает — страница отдаёт в GrImageViewer ровно те файлы, которыеisPreviewableKind(fileKindOf(mime))называет картинками, и только их делаетclickable.Client visit, Lisbon · 14–16 Oct6 attachments · waiting for approval 412 KB186 KB398 KB24 KB1.1 MB8 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 MOMina Ortiz · Customer · Mon 09:14The export fails with a red banner every time. Screenshot and the log attached.DKDaniel Kim · Support · Mon 10:02Thanks — the first screenshot did not survive retention. Could you send it again?MOMina Ortiz · Customer · Mon 10:20Here it is.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
ariaLabel | string | undefined | undefined | Доступное имя интерактивной плитки. Не задано — имя приходит из содержимого: `alt` картинки или подписи заглушки. |
loading | "lazy" | "eager" | undefined | undefined | Нативный `loading` картинки: `lazy` грузит превью у края вьюпорта, а не все плитки ленты сразу. Не задан — из `GrConfigProvider`, иначе `lazy`. |
name | string | null | undefined | undefined | Имя файла: доступное имя картинки и подпись заглушки. |
src | string | null | undefined | undefined | Адрес превью. Пусто — сразу заглушка по типу. |
as | string | Component | undefined | undefined | Свой корневой тег: компонент-ссылка (`RouterLink`, `Link` от Inertia) или тег ради разметки страницы (`article`, `figure`). Сильнее `href`. Неинтерактивный тег — только замена `div`: ни курсора, ни кольца фокуса. |
href | string | undefined | undefined | Ссылка на оригинал — для не-картинок и для перехода мимо просмотрщика. |
target | string | undefined | undefined | Куда открывать ссылку. Объявлен пропом, а не оставлен на fallthrough: иначе `_blank` уезжал бы на `<a>` мимо компонента, и защитный `rel` подставить было бы негде. |
rel | string | undefined | undefined | Своё значение `rel`. Задано — отменяет автоподстановку. |
clickable | boolean | undefined | false | Плитка кликабельна и эмитит `click` — обычно чтобы открыть просмотрщик. |
mime | string | null | undefined | undefined | MIME-тип. Он решает, картинка это или файл. |
tileSize | GrSizeWithPx | undefined | undefined | Ступень канонической шкалы либо произвольная ширина в пикселях. Число — escape-hatch, как диаметр у GrAvatar: плитка 96px в ленте вложений в четыре ступени не укладывается. |
ratio | GrFilePreviewRatio | undefined | undefined | Пропорции плитки: держат место до загрузки, картинка обрезается по ним (`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
const grFilePreviewConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFilePreview">;Файлdist/types/components/GrFilePreview/fileKindOf.d.ts
Вид файла по MIME-типу.
Тип решает **mime, а не расширение**: расширение врёт (.dat у выгрузки,
.pdf у переименованного архива), а бэкенд отдаёт настоящий тип — брать надо
его. От вида зависит единственное решение компонента: рисовать <img> или
иконку.
type GrFileKind = 'image' | 'pdf' | 'document' | 'spreadsheet' | 'archive'function fileKindOf(mime: string | null | undefined): GrFileKind;Картинку показываем, остальное — подписываем иконкой.
function isPreviewableKind(kind: GrFileKind): boolean;Файлdist/types/components/GrFilePreview/GrFilePreview.vue.d.ts
Плитка превью файла с фолбэком по типу.
Показывает **сохранённый** файл, а не отправляет новый: отправка — это
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;
}interface GrFilePreviewEmits {
/** Клик по плитке — здесь открывают просмотрщик. С клавиатуры приходит у `clickable` и ссылки. */
(e: 'click', event: MouseEvent): void;
}Файлdist/types/components/GrFilePreview/grFilePreviewStyles.d.ts
Соотношение сторон плитки.
type GrFilePreviewRatio = '1:1' | '4:3' | '16:9';Файлdist/types/components/GrFilePreview/defaults.d.ts
Пропы GrFilePreview, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrFilePreview: { … } }">.
Только оформление: src, mime и name принадлежат файлу. Размер и
пропорции здесь потому, что лента вложений выглядит одинаково на всех
страницах, и повторять решение у каждой плитки значит однажды разойтись.
interface GrFilePreviewConfigurableProps {
tileSize: GrSizeWithPx;
ratio: GrFilePreviewRatio;
loading: 'lazy' | 'eager';
}Файлdist/types/components/GrFilePreview/safelist.d.ts
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 как картинку.
Пропы этого примера