cascader —— 級聯選擇器
Ce contenu n’est pas encore disponible dans votre langue.
級聯選擇器處理「省 > 市 > 區」這類逐層選擇的資料:選了上一層,下一層才出現。core/cascader 是 share 裡最重的 core 模組之一——樹狀資料的查找、路徑還原、面板生成、搜尋、歸一化全是純函式,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}三層屬性包:
// 觸發器:以 listbox 語義彈出getCascaderTriggerAria(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/disabledgetCascaderItemAria({ 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:大樹 + 高頻輸入時每次擊鍵 O(全量節點) 會卡頓,上限是效能保險;需要更多結果由 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