cascader —— Каскадный селектор
Каскадный селектор обрабатывает данные с послойным выбором вида «провинция > город > район»: выбранный верхний уровень открывает следующий. core/cascader — один из самых тяжёлых модулей core в share: поиск по дереву, восстановление путей, генерация панелей, поиск и нормализация — всё это чистые функции, а UI-слой только рендерит элементы панелей по столбцам.
Когда использовать
Заголовок раздела «Когда использовать»- В каждой платформе реализован
LhCascader: многоуровневые категории, выбор регионов, организационные структуры. - Данные представляют дерево (или нормализуемы в дерево), на каждом уровне выбор взаимоисключающий.
- Не использовать для: выбора одного уровня —
select; для каскадного выделения, где родитель отмечается вместе с потомками —tree(там есть семантикаcheckStrictly).
import { findNode, findPathByValues, getField, getChildren, getPanelItems, searchNodes, getSelectedLabelPath, getCascaderClassNames, getPanelClassNames, expandLastValue, isLeafNode, normalizeCascaderNodes, flattenNodes, getCascaderTriggerAria, getCascaderColumnAria, getCascaderItemAria,} from '@lh-ui/share/core/cascader'import type { LhCascaderNode, LhCascaderPath, LhCascaderPanelItem, LhCascaderState, LhCascaderSearchResult, LhCascaderItemConfig,} from '@lh-ui/share/core/cascader'Замысел дизайна
Заголовок раздела «Замысел дизайна»Ключевые решения:
- Дедупликация по цепочке предков, а не глобальный visited: операции с деревом повсеместно требуют защиты от циклических ссылок. Здесь для дедупликации используется «цепочка предков» (параметр
ancestorsпередаётся уровень за уровнем), а не модульный или глобальный Set — плата в том, что защита действует только в пределах одного вызова, а выигрыш в том, что корректно разделяемые поддеревья в DAG не пропускаются по ошибке и нет утечки состояния между экземплярами. getFieldзаставляет псевдонимы полей работать по-настоящему: данные с бэкенда часто называются неvalue/label/children(а, например,id/name/subList).LhCascaderItemConfigобъявляет отображение псевдонимов, и все операции с деревом читают поля черезgetField.- Одна нормализация — везде стандартная структура:
normalizeCascaderNodesпревращает исходное дерево с произвольной структурой полей вLhCascaderNode[], и после этого все функции работают со стандартной структурой. Можно и не нормализовывать, а напрямую передать itemConfig — оба пути равноценны (но после нормализации производительность выше, так как псевдонимы не опрашиваются на каждом уровне). - Результаты поиска несут полную цепочку предков: каждый результат
searchNodesсодержитpath(массив значений),nodes(массив узлов) иpathLabels(массив меток) — UI сразу рендерит строку «Чжэцзян / Ханчжоу / Сиху», без повторных запросов. - Программное восстановление не фильтрует отключённые элементы:
findPathByValuesнамеренно не отфильтровывает отключённые дочерние узлы — отключение является ограничением кликов на UI-слое, а программное восстановление (например, начальное значениеv-model) должно уметь восстанавливать любой путь, иначе сценарий «сначала отключили, потом восстанавливаем» потеряет значение.
Таблица функций
Заголовок раздела «Таблица функций»| Функция | Сигнатура | Возвращаемое значение | Назначение |
|---|---|---|---|
findNode |
(nodes, value, strict?, ancestors?, itemConfig?) => { node, path } | null |
узел + цепочка предков | рекурсивный поиск |
getField |
<T>(node, standard: string, configKey?: string) => T |
значение поля | чтение свойства узла по псевдониму |
findPathByValues |
(nodes, values, itemConfig?) => LhCascaderPath | null |
путь | послойное восстановление по массиву значений |
getChildren |
(node, itemConfig?) => LhCascaderNode[] |
массив потомков | при грязных данных откат в [] |
getPanelItems |
(nodes, expandedValues?, itemConfig?) => LhCascaderPanelItem[] |
элементы панели | отображение дерева в элементы панели |
searchNodes |
(nodes, keyword, results?, itemConfig?, maxResults?) => LhCascaderSearchResult[] |
результаты поиска | нечувствителен к регистру, лимит по умолчанию 50 |
getSelectedLabelPath |
(nodes, values, itemConfig?) => string |
"Чжэцзян / Ханчжоу" |
строка выбранных меток |
expandLastValue |
(nodes, value, itemConfig?) => (string|number)[] | null |
массив пути | восстановление пути из одного значения при emitPath: false |
isLeafNode |
(node, itemConfig?) => boolean |
логическое значение | явный leaf в приоритете, иначе по children |
normalizeCascaderNodes |
(nodes: readonly unknown[], itemConfig?) => LhCascaderNode[] |
стандартное дерево | однократная нормализация |
flattenNodes |
(nodes, itemConfig?) => {node, path, pathValues}[] |
плоский список | каждый элемент несёт путь |
getCascaderClassNames |
(state) => string[] |
имена классов | контейнер |
getPanelClassNames |
(state) => string[] |
имена классов | панель (--hover-mode) |
Определения типов
Заголовок раздела «Определения типов»export interface LhCascaderNode { value: string | number label: string disabled?: boolean leaf?: boolean children?: LhCascaderNode[]}
export interface LhCascaderPath { path: (string | number)[] // массив выбранных значений nodes: LhCascaderNode[] // соответствующий массив узлов}
export interface LhCascaderPanelItem { value: string | number label: string isDisabled: boolean isExpanded: boolean hasChildren: boolean}
export interface LhCascaderSearchResult { path: (string | number)[] nodes: LhCascaderNode[] pathLabels: string[]}
export interface LhCascaderItemConfig { value?: string // псевдоним поля value label?: string children?: string disabled?: string leaf?: string}
export interface LhCascaderState { options: LhCascaderNode[] size: LhComponentSize disabled: boolean clearable: boolean placeholder: string visible: boolean checkStrictly: boolean // родитель и потомки не связаны (можно выбирать любой уровень) emitPath: boolean // true — значение это массив пути, false — только значение нижнего уровня multiple: boolean cascading: boolean value: (string | number)[] selectedValue: (string | number)[] | null}Доступность
Заголовок раздела «Доступность»Три уровня пакетов атрибутов:
// Триггер: всплывает с семантикой listboxgetCascaderTriggerAria(true, 'my-panel')// { 'aria-expanded': true, 'aria-controls': 'my-panel', 'aria-haspopup': 'listbox' }
// Каждый столбец: role="listbox" + aria-level отмечает глубину (с 1)getCascaderColumnAria('col-2', 2) // { role: 'listbox', id: 'col-2', 'aria-level': 2 }
// Элемент: role="option" + aria-expanded/selected/disabled по необходимостиgetCascaderItemAria({ selected: true, expanded: true, hasChildren: true })// { role: 'option', 'aria-expanded': true, 'aria-selected': true }Обратите внимание: getCascaderItemAria не выводит aria-expanded для элементов без потомков — объявлять aria-expanded="false" для листовых вариантов разрешено APG, но избыточно, и к тому же даёт программе чтения экрана ложное представление, будто элемент «разворачивается, просто сейчас развёрнут».
Примеры использования
Заголовок раздела «Примеры использования»import { findPathByValues, getSelectedLabelPath, getPanelItems } from '@lh-ui/share/core/cascader'
const tree = [ { value: 'zj', label: 'Чжэцзян', children: [ { value: 'hz', label: 'Ханчжоу', children: [{ value: 'xh', label: 'Сиху' }] }, ], },]
findPathByValues(tree, ['zj', 'hz', 'xh'])// { path: ['zj','hz','xh'], nodes: [Чжэцзян, Ханчжоу, Сиху] }
getSelectedLabelPath(tree, ['zj', 'hz', 'xh']) // 'Чжэцзян / Ханчжоу / Сиху'getSelectedLabelPath(tree, ['nope']) // '' (при отсутствии — пустая строка)
getPanelItems(tree, new Set(['zj']))// [{ value:'zj', label:'Чжэцзян', isExpanded:true, hasChildren:true },// { value:'hz', label:'Ханчжоу', isExpanded:false, hasChildren:true }]Псевдонимы полей:
const raw = [{ id: 'zj', name: 'Чжэцзян', subList: [{ id: 'hz', name: 'Ханчжоу' }] }]const cfg = { value: 'id', label: 'name', children: 'subList' }const standard = normalizeCascaderNodes(raw, cfg)// [{ value:'zj', label:'Чжэцзян', children:[{ value:'hz', label:'Ханчжоу' }] }]findPathByValues(standard, ['zj', 'hz']) // работает корректно<!-- Vue3: каждый столбец рендерит один уровень panelItems --><div :class="getCascaderClassNames({ size, disabled, visible, readonly })"> <button class="lh-cascader__input" v-bind="getCascaderTriggerAria(visible, panelId)"> {{ getSelectedLabelPath(options, value) || placeholder }} </button> <div v-if="visible" class="lh-cascader-panel" :class="getPanelClassNames({ hoverMode })"> <div v-for="(col, ci) in columns" :key="ci" class="lh-cascader-panel__column" v-bind="getCascaderColumnAria(`col-${ci}`, ci + 1)" > <div v-for="item in col" class="lh-cascader-panel__item" v-bind="getCascaderItemAria({ selected: ..., expanded: item.isExpanded, disabled: item.isDisabled, hasChildren: item.hasChildren })" @click="select(item, ci)" >{{ item.label }}</div> </div> </div></div>Стили и токены
Заголовок раздела «Стили и токены»| Имя класса | Семантика |
|---|---|
.lh-cascader / --disabled / --visible |
контейнер |
.lh-cascader__input / __input-inner |
область ввода триггера |
.lh-cascader__tags / __tag / __tag-more |
метки множественного выбора (сворачивание при переполнении) |
.lh-cascader__clear |
кнопка очистки |
.lh-cascader-panel / --hover-mode |
панель (режим hover: следующий уровень разворачивается при наведении) |
.lh-cascader-panel__column / __column-header |
столбец и заголовок столбца |
.lh-cascader-panel__item / --active / --disabled |
элемент столбца |
.lh-cascader-panel__search / __search-item |
область результатов поиска |
.lh-cascader-panel__empty |
заглушка при отсутствии результатов |
.lh-cascader-panel-* переходные классы |
анимация появления и скрытия панели |
Замечания
Заголовок раздела «Замечания»- Лимит
searchNodesпо умолчанию — 50: для большого дерева и частого ввода каждое нажатие клавиши выполняет обход всех узлов, что приведёт к зависаниям, а лимит является страховкой производительности; если нужно больше результатов, UI-слой подсказывает «слишком много результатов, уточните ключевое слово». - Область действия защиты от циклических ссылок: дедупликация по цепочке предков действует только в пределах одного вызова и не кешируется между вызовами. Не пытайтесь кешировать и переиспользовать результат
findNode— если дерево изменилось, всё нужно пересчитать. expandLastValueприменяется только приemitPath: false: в этом режимеv-modelполучает только значение нижнего уровня, и при восстановлении нужно по одному значению развернуть полный путь раскрытия.- Откат
getChildrenпри грязных данных: null-узлы, не-объекты и не-массивы в children возвращают[]без возбуждения TypeError. Благодаря этому каскад «проглатывает» грязные данные с бэкенда (children, записанные как null — частый случай). - Сочетание
checkStrictlyиemitPath:checkStrictlyразрешает выбирать любой уровень, и тогда часто применяется вместе сemitPath: false(нужно только значение нижнего уровня) — эти два параметра ортогональны.
@lh-ui/share/core/cascader → getSelectedLabelPath( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue