Кастомные состояния мест (Renderer SDK)
Кастомные состояния мест позволяют визуально пометить отдельные места произвольным, определяемым приложением состоянием – например reserved, held, vip или sold – поверх того, что рендерер уже отрисовывает. Состояние – это строковый ключ: вы назначаете этот ключ набору мест во время выполнения через setSeatsState, а то, как ключ должен выглядеть, описываете в theme.seatStyles рендерера.
Это руководство охватывает клиентский Renderer SDK (@seatmap.pro/renderer). Оно объясняет, как применять и визуализировать состояния из браузера. Если вам нужен серверный жизненный цикл места и определяемые клиентом типы холдов (модель данных state / holdType в booking service), смотрите вместо этого Состояния мест и типы холдов – эти две темы дополняют друг друга.
Обзор
Функциональность построена из двух независимых слоёв. Держать их раздельно в голове – ключ к корректному использованию API.
| Слой | Что он контролирует | Как вы им управляете |
|---|---|---|
| Движок назначения | Какие места в каком состоянии | setSeatsState(seats, key) / clearSeatsState(seats) |
| Конфиг стилей | Как выглядит ключ состояния | theme.seatStyles (декларативно, задаётся при создании) |
Движок назначения поддерживает отображение из ID места в единственный ключ состояния (не более одного ключа на место). Он ничего не знает о цветах, размерах или DOM. Конфиг стилей сопоставляет каждый ключ состояния с описанием стиля и живёт в настройках рендерера, которые вы передаёте при создании; он никогда не мутируется во время выполнения методами состояний.
Место выдаёт видимый оверлей только тогда, когда оба слоя согласны: движок назначил ему ключ, и в seatStyles есть запись для этого ключа.
Оверлей – это слой DOM
То, что фактически появляется на экране, – это оверлей DOM: единственный абсолютно спозиционированный контейнер <div>, добавленный в host-элемент рендерера, с одним маленьким узлом <div> на каждое видимое назначенное место. Он не отрисовывается на сцене WebGL или Canvas2D. Поскольку он читает только общую трансформацию вьюпорта (scale / translate), он работает идентично как под WebGL-рендерером, так и под запасным Canvas2D. Контейнер использует pointer-events: none, поэтому он никогда не перехватывает ввод на canvas.
Быстрый старт
Минимальный сквозной поток: создать рендерер с одним кастомным состоянием в theme.seatStyles, загрузить событие, затем назначить это состояние некоторым местам после того, как места появятся.
import { SeatmapBookingRenderer } from '@seatmap.pro/renderer';
const element = document.getElementById('seatmap') as HTMLElement;
const renderer = new SeatmapBookingRenderer(element, {
publicKey: 'your-public-key',
theme: {
seatStyles: {
mykey: {
tint: 'rgba(76,141,255,0.5)',
keepSeatName: true,
},
},
},
});
void renderer.loadEvent('event-guid').then(() => {
renderer.setSeatsState([101, 102, 103], 'mykey');
});
Примечания:
publicKeyобязателен во время выполнения дляSeatmapBookingRenderer; конструктор бросает'Public key is undefined', если он отсутствует.setSeatsStateдолжен вызываться после того, как места появятся – то есть после того, какloadEventзарезолвится (или изнутри колбэкаonSchemaDataLoaded). Состояния применяются к местам, которые уже загружены.- Узлы оверлея крошечные при масштабе «по размеру площадки». Чтобы сделать их явно видимыми, приблизьтесь к секции с помощью
zoomToSection(см. проработанные примеры).
Концепции
Семантика движка назначения
- Одно состояние на место. У каждого места ровно один назначенный ключ, либо ни одного.
- Перезапись, а не слияние. Назначение нового состояния месту отбрасывает любое предыдущее назначение. Назначение
reserved, затемsoldтому же месту оставляет его какsold. - Очистка работает поместно и молча игнорирует промахи. Очистка состояния на ID, которые не были назначены, просто пропускает их; частичная очистка оставляет другие места нетронутыми.
- Блокировка – это отдельный шлюз. Место считается заблокированным только когда у него есть назначение и стиль для этого ключа задаёт
blockInteractionистинным. Само по себе состояние никогда не подразумевает блокировку.
Встроенные ключи против кастомных ключей
Ключи состояний делятся на две группы.
Встроенные ключи – это собственные состояния мест рендерера, перечисленные в BuiltinSeatStateKey:
type BuiltinSeatStateKey =
| 'default'
| 'unavailable'
| 'filtered'
| 'hovered'
| 'selected'
| 'loading'
| 'error';
Они настраиваются более богатым типом ISeatStyle (размер, цвет, обводка, тень, accessible-переопределение). Поставляемые темы booking и admin уже их заполняют. В частности, size записи default – это след (footprint), который использует каждый узел оверлея (см. Примечания о производительности и рендеринге).
Кастомные ключи – это любая другая строка: 'reserved', 'held', 'vip', 'sold', 'restricted' и так далее. Вы назначаете их через setSeatsState и стилизуете более лёгким ICustomSeatStyle (или полным ISeatStyle, если предпочитаете).
Конфиг стилей
Вся стилизация мест живёт в theme.seatStyles, типизированном как SeatStylesMap.
Форма SeatStylesMap
type SeatStylesMap = Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & {
[customKey: string]: ICustomSeatStyle | ISeatStyle | undefined;
};
- Половина
Partial<Record<BuiltinSeatStateKey, ISeatStyle>>ограничивает семь встроенных ключей типомISeatStyle(более богатый тип, добавляющий переопределениеaccessible). Все семь необязательны. - Половина с index-сигнатурой открывает отображение для любого произвольного строкового ключа, значением которого может быть либо
ICustomSeatStyle(легковесный: tint, svg, render-колбэк и т. д.), либо полныйISeatStyle.undefinedв объединении требуется TypeScript при смешиванииPartial<Record<...>>с общей index-сигнатурой.
На практике: встроенные ключи получают ISeatStyle; кастомные ключи обычно используют более лёгкий ICustomSeatStyle.
IBasicSeatStyle
interface IBasicSeatStyle {
size: number;
color: string;
seatName?: {
font: string;
color: string;
};
stroke?: {
width: number;
color: string;
align: 'center' | 'inside' | 'outside';
};
imageId?: string;
shadow?: {
blur: number;
color: string;
x?: number;
y?: number;
};
}
| Поле | Тип | Необязательно | Значение |
|---|---|---|---|
size |
number |
Нет | Размер отрисовки места (диаметр/радиус в единицах рендерера). |
color |
string |
Нет | Цвет заливки места. |
seatName |
{ font: string; color: string } |
Да | Типографика подписи места. font – это CSS-строка шрифта (например '18px Arial'); color – цвет текста. |
stroke |
{ width: number; color: string; align: 'center' | 'inside' | 'outside' } |
Да | Граница места: толщина, цвет и выравнивание относительно края места. |
imageId |
string |
Да | ID предварительно зарегистрированного изображения, отрисованного на месте. |
shadow |
{ blur: number; color: string; x?: number; y?: number } |
Да | Падающая тень: радиус размытия, цвет и необязательное смещение по X/Y. |
ISeatStyle
interface ISeatStyle extends IBasicSeatStyle {
accessible?: IBasicSeatStyle;
}
| Поле | Тип | Необязательно | Значение |
|---|---|---|---|
(наследует все поля IBasicSeatStyle) |
См. таблицу выше. | ||
accessible |
IBasicSeatStyle |
Да | Переопределяющий стиль, применяемый когда место помечено как accessible/для инвалидной коляски. Несёт те же шесть базовых полей, кроме самого accessible. |
ICustomSeatStyle
interface ICustomSeatStyle {
tint?: string;
svg?: string;
imageId?: string;
className?: string;
priority?: number;
blockInteraction?: boolean;
keepSeatName?: boolean;
render?: (args: ISeatStateRenderArgs) => HTMLElement | string;
}
| Поле | Тип | Необязательно | Значение |
|---|---|---|---|
tint |
string |
Да | Цветовой оттенок, наложенный на след места (отрисовывается как залитый круг). |
svg |
string |
Да | SVG для отрисовки на месте. Если начинается с <, трактуется как встроенная SVG-разметка; иначе используется как URL. |
imageId |
string |
Да | ID предварительно зарегистрированного изображения (ищется в theme.images[imageId]). |
className |
string |
Да | Имя CSS-класса, добавляемое к оверлейному <div> места (для кастомной стилизации / анимации). |
priority |
number |
Да | Принимается типом, но в настоящее время не используется оверлеем. Z-упорядочивания нет, и задание его сегодня не даёт эффекта. |
blockInteraction |
boolean |
Да | Когда true, место сообщается как заблокированное (см. Блокировка взаимодействия). |
keepSeatName |
boolean |
Да | Управляет подписью места на узле оверлея. Подпись показывается по умолчанию; только keepSeatName: false её подавляет. |
render |
(args: ISeatStateRenderArgs) => HTMLElement | string |
Да | Кастомный DOM-колбэк, полностью поставляющий содержимое оверлея места. |
Примечание: у ICustomSeatStyle нет собственного size. Узлы оверлея разделяют след места по умолчанию – размер берётся из seatStyles['default']?.size (с откатом на 10, если записи default нет).
ISeatStateRenderArgs
Единственный аргумент, передаваемый в кастомный render-колбэк:
interface ISeatStateRenderArgs {
seat: ISeat;
stateKey: string;
style: ICustomSeatStyle;
sizePx: number;
}
| Поле | Тип | Значение |
|---|---|---|
seat |
ISeat |
Полный объект данных отрисовываемого места. |
stateKey |
string |
Активный ключ состояния (встроенный или кастомный), который запустил эту отрисовку. |
style |
ICustomSeatStyle |
Разрешённый ICustomSeatStyle для этого состояния, чтобы колбэк мог проинспектировать свой конфиг. |
sizePx |
number |
Текущий отрисованный размер места в пикселях, чтобы колбэк мог отмасштабировать свой вывод. |
Публичный API
Оба метода публичны на базовом рендерере, поэтому SeatmapBookingRenderer и SeatmapAdminRenderer предоставляют их идентично. Применяйте состояния только после того, как событие и его места загрузились.
setSeatsState
setSeatsState(seats: ISeat[] | number[] | string[], stateKey: string): void
Назначает stateKey заданным местам: разрешает ID мест, записывает назначение в движок (перезаписывая любое предыдущее состояние), пишет customState в каждый совпавший объект места, затем перерисовывает, чтобы оверлей пересинхронизировался. Если seats пуст, вызов ничего не делает.
clearSeatsState
clearSeatsState(seats: ISeat[] | number[] | string[]): void
Очищает состояние с заданных мест: разрешает ID, очищает назначение в движке (промахи молча пропускаются), удаляет customState из каждого совпавшего объекта места, затем перерисовывает. Если seats пуст, вызов ничего не делает.
Принимаемые формы аргументов
Оба метода принимают одно и то же объединение для seats:
ISeat[]– объекты мест (разрешаются по их.id).number[]– сырые числовые ID мест.string[]– составные ключи мест (разрешаются в ID внутренне).
Диспетчер «нюхает» тип первого элемента, чтобы решить, как разрешать ID.
Очистка каждого места
На рендерере нет перегрузки «очистить всё» без аргументов. Чтобы очистить каждое место, явно передайте все ID мест через getSeats:
renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));
Определение кастомных состояний
Кастомное состояние – это запись seatStyles под произвольным ключом. Комбинируйте поля ICustomSeatStyle:
- Tint / color.
tintнакладывает залитый круг CSS-цвета поверх следа места. (Для встроенных ключей, стилизованныхISeatStyle,color– это заливка места.) - Size. У кастомного состояния нет независимого размера; оно разделяет след места
default(seatStyles['default']?.size, откат10).ISeatStateRenderArgs.sizePxсообщает разрешённое значение. - Stroke. Доступна на
IBasicSeatStyle/ISeatStyle(width,color,align). Используйте полную записьISeatStyle, если она вам нужна. - Shadow. Доступна на
IBasicSeatStyle/ISeatStyle(blur,color,x,y). - Имя места /
keepSeatName. Узел оверлея показывает подпись места по умолчанию; задайтеkeepSeatName: false, чтобы подавить её. ТипографикаseatName(font,color) – часть встроенногоISeatStyle. - className. Добавляет CSS-класс к оверлейному
<div>, чтобы вы могли стилизовать или анимировать его из своей таблицы стилей. - imageId. Отрисовывает предварительно зарегистрированное изображение, разрешённое из
theme.images[imageId]. - priority. Объявлено на
ICustomSeatStyle, но в настоящее время не читается оверлеем. Z-упорядочивание не реализовано, и поскольку каждое место держит максимум одно состояние, на месте никогда не бывает более одного кастомного состояния для упорядочивания. Задание его сегодня не даёт эффекта. - blockInteraction. Сообщается через шлюз блокировки (см. Блокировка взаимодействия).
Пример кастомных состояний:
const seatStyles: SeatStylesMap = {
restricted: {
tint: 'rgba(214,88,78,0.6)',
blockInteraction: true,
keepSeatName: false,
},
promo: {
tint: 'rgba(76,141,255,0.5)',
className: 'promo-seat',
},
};
Переопределение встроенных состояний мест
Та же самая карта theme.seatStyles, которая объявляет кастомные ключи, также переопределяет собственные встроенные состояния рендерера. Отличие от кастомных ключей – в том, где они отрисовываются и что ими управляет: встроенные ключи перестилизуют глиф места, нарисованный на сцене canvas, и рендерер применяет их автоматически из собственного условия каждого места (locked, filtered, hovered, selected и так далее). Вы не вызываете для них setSeatsState.
| Встроенный ключ | Применяется, когда место… |
|---|---|
default |
доступно, не наведено и не выбрано |
unavailable |
заблокировано / не для продажи |
filtered |
отфильтровано из текущего вида |
hovered |
под указателем (и интерактивно) |
selected |
выбрано / в корзине |
loading |
в состоянии загрузки |
error |
в состоянии ошибки |
Ваш seatStyles глубоко сливается поверх поставляемой темы по умолчанию при создании. Поэтому вы можете переопределить единственное поле встроенного состояния и унаследовать остальное – нет необходимости переписывать весь стиль. Например, чтобы сделать выбранные места крупнее и зелёными с более толстой границей, и перекрасить недоступные места:
const renderer = new SeatmapBookingRenderer(element, {
publicKey: 'your-public-key',
theme: {
seatStyles: {
selected: {
size: 46,
color: '#1f9d55',
stroke: { width: 3, color: '#0b5d2e', align: 'outside' },
},
unavailable: { size: 8, color: '#d0d0d0' },
},
},
});
Примечания:
- Встроенные ключи принимают более богатый
ISeatStyle(size,color,seatName,stroke,imageId,shadow,accessible). Поставляйте только те поля, которые хотите изменить; остальное приходит из темы по умолчанию через глубокое слияние. - Это стилизует глиф места на canvas, независимо от DOM-оверлея кастомного состояния. Эти два компонуются: место может быть отрисовано как
selectedна canvas и при этом нести оверлей кастомного состояния поверх. default.sizeтакже определяет след каждого узла оверлея кастомного состояния (см. Примечания о производительности и рендеринге), поэтому изменение его меняет размер как базовых мест, так и оверлеев.
Рецепт: цвета по цене, изображения состояний и номера мест вместе
Частая потребность – раскрасить места по ценовому тиру (чтобы покупатели видели цену) и наложить транзакционное состояние – reserved, held, sold – как иконку или SVG-маску поверх, сохранив номер места читаемым. Отрисованное как единственный глиф canvas, это борется само с собой:
- Изображение, нарисованное прямо на месте (через
imageIdв стиле места или переопределениеonBeforeSeatDraw), заменяет весь глиф, поэтому номер места исчезает – путь отрисовки места возвращается, как только изображение нарисовано, и никогда не рисует подпись. - Поскольку это изображение есть глиф места, оно рисуется в собственном пиксельном размере изображения; подгонять его размер под заполнение круга места неудобно.
- Задание изображения поместно через
onBeforeSeatDrawвозвращает свежийISeatStyle, который заменяет весь стиль и отбрасывает цвет цены/категории, который вы задали черезsetSeatsCategory.
Оверлей кастомного состояния избегает всех трёх проблем, потому что это отдельный слой DOM, отрисовываемый поверх canvas, а не замена глифа места:
- Цвет цены остаётся на месте.
setSeatsStateникогда не трогаетseat.special.categoryили карту цветов категорий, поэтому место сохраняет цвет, который вы назначили черезsetSeatsCategory. - Изображение заполняет место. Оверлей рисует
svg/imageIdсостояния по полному следу места (inset: 0, ширина и высота равны размеру места по умолчанию). - Номер остаётся сверху. Номер места перерисовывается по центру поверх изображения по умолчанию (
keepSeatNameпо умолчаниюtrue); задавайтеkeepSeatName: falseтолько когда хотите, чтобы иконка заменила номер (замок наsold, например).
Поэтому замените поместный подход onBeforeSeatDraw двумя независимыми, компонуемыми вызовами:
// 1) Price-tier color, painted on the canvas seat -- persists.
renderer.setSeatsCategory(seatIds, priceCategoryId, '#1f9d55');
// 2) Transactional state overlay on top -- seat number preserved.
renderer.setSeatsState(seatIds, 'reserved');
с состоянием, объявленным один раз при создании. Опустите tint, чтобы цвет цены просвечивал через прозрачные части маски:
const seatStyles: SeatStylesMap = {
reserved: {
keepSeatName: true, // default -- seat number stays visible on top
svg:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
'<circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="6"/></svg>',
},
};
Место сохраняет свой цвет цены, SVG заполняет место как оверлейная маска, а номер отрисовывается сверху – без onBeforeSeatDraw. Чтобы перекрасить место под состояние вместо того, чтобы цвет цены просвечивал, добавьте полупрозрачный tint. Чтобы сослаться на именованный ассет вместо встроенного SVG, зарегистрируйте его под theme.images и нацельте imageId на него:
const renderer = new SeatmapBookingRenderer(element, {
publicKey: 'your-public-key',
theme: {
images: { holdIcon: 'data:image/svg+xml;utf8,...' }, // or a URL
seatStyles: {
held: { imageId: 'holdIcon', keepSeatName: true },
},
},
});
Оверлей крошечный при масштабе «по размеру площадки» и скрыт в детальном виде одной секции; смотрите Примечания о производительности и рендеринге, если состояние не появляется. Поскольку каждое место с состоянием – это отдельный узел DOM, держите ограниченным количество мест с состоянием, видимых одновременно – оверлей не батчится, как места на canvas, поэтому большие количества деградируют производительность.
Кастомный DOM через render()
Когда декларативных полей недостаточно, поставьте render-колбэк. Он полностью предоставляет содержимое узла оверлея для этого состояния.
render?: (args: ISeatStateRenderArgs) => HTMLElement | string;
Колбэк получает один ISeatStateRenderArgs (seat, stateKey, style, sizePx) и возвращает либо:
HTMLElement– добавляемый прямо к узлу оверлея; используйте это для сложного или интерактивного содержимого, либоstring– трактуемый как сырая SVG-разметка. Рендерер оборачивает её в абсолютно спозиционированный<div>(inset: 0) и отрисовывает через<img src="data:image/svg+xml;utf8,...">, отмасштабированный под заполнение узла.
Когда render присутствует, он перенимает генерацию содержимого для узла; декларативный путь tint / svg / imageId для этого состояния не используется. Используйте render для динамических, управляемых данными визуалов (например, рисуя что-то на основе seat или sizePx). Для статических визуалов предпочитайте декларативные поля – они проще и не требуют колбэка.
const seatStyles: SeatStylesMap = {
badge: {
render: ({ sizePx }: ISeatStateRenderArgs): string =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sizePx} ${sizePx}">` +
`<circle cx="${sizePx / 2}" cy="${sizePx / 2}" r="${sizePx / 3}" fill="#fff"/></svg>`,
},
};
Анимации
Есть два пути анимации.
Самодостаточные SVG-анимации (SMIL)
Канонические кастомные состояния анимируются целиком внутри самой SVG-строки, используя элементы SMIL (<animate>, <animateTransform>). Это не требует ни внешнего CSS, ни @keyframes, ни className – например, <animate> на r и stroke-opacity круга производит пульсирующее кольцо. Это подход, используемый в проработанных примерах ниже.
CSS-анимации через className
Если вы предпочитаете анимацию на CSS, задайте className на кастомном состоянии. Узел оверлея получает этот класс на своём <div>, и вы анимируете его из своей таблицы стилей:
.promo-seat {
animation: promo-pulse 1.4s ease-in-out infinite;
}
@keyframes promo-pulse {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
const seatStyles: SeatStylesMap = {
promo: { tint: 'rgba(76,141,255,0.5)', className: 'promo-seat' },
};
Блокировка взаимодействия
Место сообщается как заблокированное только когда оба условия выполняются:
- У места есть назначенный ключ состояния.
seatStyles[stateKey]существует и имеетblockInteractionистинным.
Поэтому:
- Место без назначения никогда не заблокировано.
- Место, у стиля которого отсутствует
blockInteraction(или он ложный), не заблокировано. - Если карта
seatStylesотсутствует, ничего не заблокировано.
Конкретно, с { sold: { blockInteraction: true }, held: { tint: '#4c8dff' } }: место в sold заблокировано, место в held – нет.
keepSeatName независим от блокировки. Подпись показывается по умолчанию; только keepSeatName: false её подавляет. В проработанных примерах ниже sold задаёт keepSeatName: false (иконка замка заменяет номер), тогда как reserved, held и vip задают keepSeatName: true.
Booking vs Admin Renderer
setSeatsState и clearSeatsState определены на базовом рендерере и наследуются без изменений как SeatmapBookingRenderer, так и SeatmapAdminRenderer. API, формы аргументов и поведение идентичны на обоих.
Конструкторы различаются только типами своих настроек.
SeatmapBookingRenderer:
constructor(
element: HTMLElement,
settings?: IBookingRendererSettings,
tags?: Record<string, string | number | boolean>,
)
SeatmapAdminRenderer:
constructor(element: HTMLElement, settings?: IAdminRendererSettings)
Оба типа настроек принимают settings.theme.seatStyles.
Почему демо запускается на admin renderer
Когда вы хотите продемонстрировать и визуально проверить кастомные состояния, admin renderer – более удобная поверхность – ради видимости рендеринга, а не возможностей API. На booking renderer доступность мест для реального события управляет внешним видом: демо-места, которые на самом деле недоступны, читаются как недоступные, и стилизация недоступности может заслонить оверлей кастомного состояния. Admin renderer не применяет эту стилизацию доступности booking, поэтому состояния показываются чисто. В продакшене booking renderer предоставляет ровно те же методы для действительно доступных мест.
Проработанные примеры
Следующее воспроизводит четыре канонических состояния и полный поток. Его можно копировать и вставлять.
Четыре состояния в theme.seatStyles
const seatStyles: SeatStylesMap = {
reserved: {
tint: 'rgba(224,161,60,0.55)',
keepSeatName: true,
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="6"><animate attributeName="r" values="34;46;34" dur="1.4s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.9;0.3;0.9" dur="1.4s" repeatCount="indefinite"/></circle></svg>',
},
held: {
tint: 'rgba(76,141,255,0.5)',
keepSeatName: true,
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="5"><animate attributeName="stroke-opacity" values="0.15;0.85;0.15" dur="2.2s" repeatCount="indefinite"/></circle></svg>',
},
vip: {
tint: 'rgba(155,108,255,0.5)',
keepSeatName: true,
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="5" stroke-dasharray="30 200" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="2.4s" repeatCount="indefinite"/></circle></svg>',
},
sold: {
tint: 'rgba(214,88,78,0.6)',
blockInteraction: true,
keepSeatName: false,
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5m0 2a3 3 0 0 1 3 3v3H9V6a3 3 0 0 1 3-3"/></svg>',
},
};
Сводка по состояниям:
reserved–tint,keepSeatName: true, анимированное пульсирующее кольцо (SMIL<animate>наrиstroke-opacity, 1.4 с).held–tint,keepSeatName: true, затухающее кольцо (SMIL<animate>наstroke-opacity, 2.2 с).vip–tint,keepSeatName: true, вращающееся пунктирное кольцо (SMIL<animateTransform>rotate, 2.4 с).sold–tint,blockInteraction: true,keepSeatName: false, статичная иконка замка (без анимации).
Ни одно из четырёх состояний не использует color, size, stroke, render или className.
Выбор демо-секции
Сгруппируйте места по их секции и верните первую секцию минимум с 44 местами, иначе самую крупную:
const pickCustomStateDemoSection = (
renderer: SeatmapAdminRenderer,
): { sectionId: number; ids: number[] } => {
const bySection = new Map<number, number[]>();
for (const seat of renderer.getSeats()) {
const list = bySection.get(seat.sectorId) ?? [];
list.push(seat.id);
bySection.set(seat.sectorId, list);
}
let best: { sectionId: number; ids: number[] } | null = null;
for (const [sectionId, ids] of bySection) {
if (ids.length >= 44) return { sectionId, ids };
if (!best || ids.length > best.ids.length) best = { sectionId, ids };
}
return best ?? { sectionId: 0, ids: [] };
};
Смешанный демо-поток
Очистить каждое место, применить четыре состояния по непересекающимся срезам демо-секции, затем приблизить:
const demoMix = (renderer: SeatmapAdminRenderer): void => {
const { sectionId, ids } = pickCustomStateDemoSection(renderer);
const pick = (from: number, to: number): number[] => ids.slice(from, to);
renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));
renderer.setSeatsState(pick(0, 12), 'reserved');
renderer.setSeatsState(pick(12, 24), 'held');
renderer.setSeatsState(pick(24, 32), 'vip');
renderer.setSeatsState(pick(32, 44), 'sold');
renderer.zoomToSection(sectionId, { focus: true });
};
Вызов zoomToSection(sectionId, { focus: true }) – это то, что делает маленькие узлы оверлея видимыми.
Действия с одним состоянием
Каждое действие с одним состоянием применяет одно состояние к своему срезу (без предварительной очистки), затем приближает:
const applyReserved = (renderer: SeatmapAdminRenderer): void => {
const { sectionId, ids } = pickCustomStateDemoSection(renderer);
renderer.setSeatsState(ids.slice(0, 12), 'reserved');
renderer.zoomToSection(sectionId, { focus: true });
};
Действия held, vip и sold следуют той же форме, каждое на собственном срезе ids: held на ids.slice(12, 24), vip на ids.slice(24, 32), и sold на ids.slice(32, 44).
Очистка всех состояний
const clearStates = (renderer: SeatmapAdminRenderer): void => {
const ids = renderer.getSeats().map((s) => s.id);
renderer.clearSeatsState(ids);
};
Примечания о производительности и рендеринге
Оверлей держит стоимость DOM пропорциональной тому, что фактически на экране – но это реальный DOM, поэтому количество мест с состоянием, видимых одновременно, управляет производительностью.
- Стоимость масштабируется с количеством видимых мест с состоянием – держите её ограниченной. Каждое видимое назначенное место монтирует собственный узел
<div>, плюс внутренний<img>дляsvg/imageId, необязательный слойtintи узел номера места. В отличие от слоя мест WebGL, это отдельные DOM-элементы, не сбатченные – и анимированные (SMIL) SVG каждый запускает собственную анимацию. Горстка или несколько десятков мест с состоянием на экране – это нормально; применение состояний к сотням или тысячам мест, видимых одновременно – целая большая секция или любой набор состояний на масштабе, где видна значительная часть площадки – создаёт столько же узлов и значительно деградирует частоту кадров, тем сильнее с анимацией. Для стилизации, которая должна масштабироваться на всю площадку, предпочитайте сбатченные на canvas пути: цвета цены/категории черезsetSeatsCategoryи переопределения встроенных состояний вseatStyles. Зарезервируйте оверлей кастомного состояния для ограниченного набора мест, на которые пользователь действительно смотрит, и предпочитайте статичные (неанимированные) визуалы, когда много мест несут состояние одновременно. - Виртуализация. Узел монтируется только для мест на пересечении видимого набора и назначенного набора. Места вне вьюпорта никогда не монтируются; у мест, прокрученных из вида, узлы удаляются.
- Ленивый контейнер. Контейнер
<div>не создаётся, пока не назначено хотя бы одно состояние. Когда состояний не остаётся, оверлей полностью разбирается – узлы очищены, контейнер удалён из DOM. Очистка всех состояний полностью удаляет оверлей. - Подавление в виде секции. Когда рендерер входит в детальный вид одной секции, оверлей подавляется (
display: none), а согласование пропускается. Он снова появляется, когда подавление снимается. Это привязано к виду детали секции, а не к режиму 3D. - Крошечный при масштабе «по размеру площадки». Размер узла берётся из
seatStyles['default']?.size(откат10), и узлы позиционируются в мировых координатах, отмасштабированных общей трансформацией вьюпорта. При масштабе «по размеру площадки» узлы очень маленькие. Именно поэтому примеры вызываютzoomToSection(sectionId, { focus: true }). - Независимость от сцены. Оверлей читает только агностичное к сцене состояние – трансформацию вьюпорта, host-элемент,
devicePixelRatioиtheme.imagesдля разрешенияimageId. Он ведёт себя идентично под WebGL и запасным Canvas2D.
Устранение неполадок
Оверлей не виден.
- Уровень масштаба. При масштабе «по размеру площадки» узлы крошечные. Вызовите
zoomToSection(sectionId, { focus: true })(или иначе приблизьтесь к секции), чтобы сделать их видимыми. - Подавление в виде секции. В детальном виде одной секции контейнер оверлея скрыт. Он возвращается, когда подавление снимается. Вращение 3D не подавляет оверлей.
- Места ещё не загружены. Состояние применяется только к загруженным местам. Вызывайте
setSeatsStateпосле того, какloadEventзарезолвится (или изonSchemaDataLoaded). - Неверный ключ / нет в
seatStyles. Место отрисовывает оверлей только когда у его назначенного ключа есть совпадающая записьseatStyles. Проверьте, что строка ключа, переданная вsetSeatsState, точно совпадает с ключом вtheme.seatStyles.
Состояния не очищаются.
clearSeatsStateочищает только те ID, которые вы передаёте; промахи молча пропускаются. Чтобы очистить всё, передайте все ID:renderer.clearSeatsState(renderer.getSeats().map((s) => s.id)). Когда последнее состояние очищено, весь контейнер оверлея удаляется из DOM.
Демо-места booking renderer выглядят недоступными.
- На booking renderer реальная доступность события стилизует недоступные места, что может заслонить оверлей кастомного состояния. Демонстрируйте кастомные состояния на
SeatmapAdminRenderer, который не применяет стилизацию доступности booking. В продакшене на booking renderer применяйте кастомные состояния к действительно доступным местам.
Краткий справочник API
| Символ | Сигнатура / форма | Где |
|---|---|---|
setSeatsState |
(seats: ISeat[] | number[] | string[], stateKey: string): void |
Renderer (booking + admin) |
clearSeatsState |
(seats: ISeat[] | number[] | string[]): void |
Renderer (booking + admin) |
setSeatsCategory |
(seats: ISeat[] | number[] | string[], category: number, color?: string): void |
Renderer (booking + admin) |
getSeats |
() => ISeatDTO[] (используется для перечисления мест при очистке всех) |
Renderer |
zoomToSection |
(sectionId, { focus: true }) |
Renderer |
loadEvent |
booking: (eventId: string, sectorId?: number) => Promise<void>; admin: (eventId: string) => Promise<void> |
renderers |
BuiltinSeatStateKey |
'default' | 'unavailable' | 'filtered' | 'hovered' | 'selected' | 'loading' | 'error' |
type |
SeatStylesMap |
Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & { [customKey: string]: ICustomSeatStyle | ISeatStyle | undefined } |
type |
IBasicSeatStyle |
{ size; color; seatName?; stroke?; imageId?; shadow? } |
interface |
ISeatStyle |
IBasicSeatStyle & { accessible?: IBasicSeatStyle } |
interface |
ICustomSeatStyle |
{ tint?; svg?; imageId?; className?; priority?; blockInteraction?; keepSeatName?; render? } |
interface |
ISeatStateRenderArgs |
{ seat: ISeat; stateKey: string; style: ICustomSeatStyle; sizePx: number } |
interface |
IRendererTheme.seatStyles |
seatStyles?: SeatStylesMap |
theme field |
IRendererTheme.images |
images?: { [id: string]: string } (именованные SVG/image-ассеты, разрешаемые по imageId) |
theme field |
Связанное
- Состояния мест и типы холдов – серверный жизненный цикл типа холда и модель данных (
state/holdTypeв booking service). Та статья охватывает, как места проходят свой жизненный цикл на стороне сервера; эта охватывает, как визуализировать и применять состояния на стороне клиента через Renderer SDK. - Состояния и стилизация секций – эквивалентная концепция, применённая к обводкам секций (highlighted, selected, unavailable, filtered).
- Инициализация SDK – установите
@seatmap.pro/renderer, создайте рендерер и загрузите событие перед применением любых кастомных состояний.