feat(tgu-ui): бронирование слотов — виртуализация, автоподбор покрытия, эргономичный мультивыбор
Вкладка «Слоты» масштабирована под ~20k слотов/мес без торможения: - wktCache: контур слота парсится один раз (кэш по slotKey), не на каждый ререндер сцены - виртуализация таблицы (@tanstack/react-virtual): в DOM только видимое окно строк - автоподбор покрытия: проброс coverageStrategy (GREEDY/CONTINUOUS) в request-cover, результат — преднабор брони (solver не дублируется, используется бэкендный) - мультивыбор: Shift-диапазон по отсортированному списку, «все сутки»/«видимые», счётчик охвата выбора - бронь крупных наборов чанками (по 200) + подтверждение выше 500 + прогресс - отмена расчёта (AbortController), мягкая подсказка перегруза карты - чистая логика выбора/чанкинга вынесена в slotSelection.ts + юнит-тесты Тесты: 70/70 (17 новых). tsc -b и vite build чисто.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Чистая логика выбора слотов и разбиения на чанки — выделена из TguSlotsTab,
|
||||
// чтобы покрыть юнит-тестами без рендера компонента.
|
||||
|
||||
/**
|
||||
* Новое множество выбора после клика по строке.
|
||||
* Обычный клик — переключает один ключ. Shift+клик при заданном якоре — выбирает (или
|
||||
* снимает) непрерывный диапазон от якоря до текущего индекса; направление задаётся новым
|
||||
* состоянием текущей строки (если она была не выбрана — диапазон выбираем, иначе снимаем).
|
||||
*
|
||||
* @param current Текущее множество выбранных ключей.
|
||||
* @param keyAt Доступ к ключу слота по индексу в видимом (отфильтрованном) списке.
|
||||
* @param index Индекс строки, по которой кликнули.
|
||||
* @param shiftKey Был ли зажат Shift.
|
||||
* @param anchorIndex Индекс предыдущего клика (якорь) либо null.
|
||||
*/
|
||||
export function selectionAfterCheck(
|
||||
current: ReadonlySet<string>,
|
||||
keyAt: (index: number) => string,
|
||||
index: number,
|
||||
shiftKey: boolean,
|
||||
anchorIndex: number | null,
|
||||
): Set<string> {
|
||||
const next = new Set(current)
|
||||
const targetKey = keyAt(index)
|
||||
if (shiftKey && anchorIndex != null) {
|
||||
const lo = Math.min(anchorIndex, index)
|
||||
const hi = Math.max(anchorIndex, index)
|
||||
const select = !current.has(targetKey)
|
||||
for (let i = lo; i <= hi; i++) {
|
||||
const k = keyAt(i)
|
||||
if (select) next.add(k)
|
||||
else next.delete(k)
|
||||
}
|
||||
} else if (next.has(targetKey)) {
|
||||
next.delete(targetKey)
|
||||
} else {
|
||||
next.add(targetKey)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбивает массив на чанки заданного размера (для чанкованной брони крупных наборов).
|
||||
* @param items Исходный массив.
|
||||
* @param size Размер чанка; <= 0 — один чанк со всем массивом.
|
||||
*/
|
||||
export function chunk<T>(items: readonly T[], size: number): T[][] {
|
||||
if (size <= 0) return items.length ? [items.slice()] : []
|
||||
const out: T[][] = []
|
||||
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user