# Cascader 级联选择

多级联动选择：支持多选勾选、父子解绑、多选值策略、路径搜索、动态加载、受控开合与标签折叠，键盘可操作。

## 基础用法

<DemoBlock title="基础用法">
  <oas-cascader placeholder="请选择省市区" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"},{"label":"温州","value":"wz"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"},{"label":"苏州","value":"sz"}]},{"label":"四川","value":"sc","children":[{"label":"成都","value":"cd"}]}]'></oas-cascader>
</DemoBlock>

点击展开多级面板，逐级下钻直到叶子节点提交。

## 选择即提交

<DemoBlock title="选择即提交（change-on-select）">
  <oas-cascader change-on-select placeholder="选到任意级即提交" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

开启 `change-on-select` 后，点击任意层级的选项（含含子级的节点）即提交当前路径。

## 预设路径

<DemoBlock title="预设值（value 路径数组）">
  <oas-cascader value='["zj","hz"]' options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

`value` 为 JSON 数组，存放各级选中值；展示时拼接为「浙江 / 杭州」。

## 多选

<DemoBlock title="多选（multiple）">
  <oas-cascader id="cs-multi" multiple placeholder="批量选址" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
  <span id="cs-multi-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

多选时 `value` 为路径数组的数组（JSON），面板行前置复选框：勾选父级会级联勾选全部子级，部分勾选时父级呈半选态；触发器以标签回显，可单独移除。勾选后面板保持展开，便于连续选择。

## 父子解绑

<DemoBlock title="父子解绑（check-strictly）">
  <oas-cascader multiple check-strictly placeholder="父子独立勾选" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

`check-strictly` 下父子勾选互不联动：勾选父级只提交父级路径，值即勾选集合（此时 `value-mode` 不生效）。

## 多选值策略

<DemoBlock title="值策略（value-mode）">
  <oas-cascader multiple value-mode="all" value='[["zj"]]' placeholder="all（默认）" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
  <oas-cascader multiple value-mode="parentFirst" value='[["zj"],["zj","hz"]]' placeholder="parentFirst" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
  <oas-cascader multiple value-mode="onlyLeaf" value='[["zj"],["zj","hz"]]' placeholder="onlyLeaf" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
</DemoBlock>

`value-mode` 决定多选时提交哪些路径：`all`（默认）= 全部勾选节点（子级全选时父级自动进值，与单选全路径心智一致）；`parentFirst` = 子级全选时收敛为父级代表；`onlyLeaf` = 只保留叶子路径。回显标签与 `value` 字面一致（宿主预设不被动改写），交互提交时按各自策略收敛。

## 可搜索

<DemoBlock title="可搜索（filterable）">
  <oas-cascader id="cs-search" filterable placeholder="输入省市名搜索" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"},{"label":"温州","value":"wz"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"},{"label":"苏州","value":"sz"}]},{"label":"四川","value":"sc","children":[{"label":"成都","value":"cd"}]}]'></oas-cascader>
  <span id="cs-search-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

`filterable` 打开面板后出现搜索框，输入按「路径各级名称拼接」匹配，结果为扁平路径列表，点击直接选中该完整路径（多选则勾选）；输入时派发 `oas-search`。可通过 `el.filter = (query, path) => boolean` 自定义匹配（`path` 为各级选项数组）。

## 动态加载

<DemoBlock title="动态加载（lazy + el.load）">
  <oas-cascader id="cs-lazy" placeholder="逐级加载部门" options='[{"label":"技术中心","value":"tech"},{"label":"运营中心","value":"ops"}]'></oas-cascader>
</DemoBlock>

设置 `el.load = ({ option, path, depth }) => Promise<选项数组>` 后，展开未加载的节点会显示「加载中」并请求子级（`path` 为根时 `option` 为 null，即第一级也可懒加载）；选项可标 `isLeaf: true` 声明叶子。本例模拟 500ms 网络延迟。

## 悬停展开

<DemoBlock title="悬停展开（expand-trigger=hover）">
  <oas-cascader expand-trigger="hover" placeholder="悬停即下钻" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

`expand-trigger="hover"` 时悬停父级选项（约 120ms 防误触延时）即展开子级列，点击叶子提交；默认为 `click`。

## 大数据量（虚拟滚动）

<DemoBlock title="虚拟滚动（1000 项长列）">
  <oas-cascader id="cs-virtual" item-height="36" virtual placeholder="1000 项长列，滚动流畅" options='[]'></oas-cascader>
  <span id="cs-virtual-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

设置 `virtual` 后超过阈值（50 项）的长列只渲染可视窗口（复用 `oas-virtual-list` 的窗口计算，首尾 padding 撑起滚动高度），千级选项滚动流畅、面板高度恒定；`item-height` 可调定高（默认 `36`）。键盘 `↑`/`↓` 导航时窗口自动跟随高亮行；低于阈值的短列与搜索结果、加载占位保持普通渲染。

## 仅末级与自定义分隔符

<DemoBlock title="仅末级（show-all-levels）与分隔符（separator）">
  <oas-cascader show-all-levels="false" value='["zj","hz"]' placeholder="仅显示末级" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
  <oas-cascader separator=" - " value='["zj","hz"]' placeholder="自定义分隔符" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
</DemoBlock>

`show-all-levels` 默认 true（显示完整路径），设为 `false` 后触发器只显示末级名称；`separator` 自定义路径分隔符（默认 ` / `），同时作用于搜索结果行。

## 可清空

<DemoBlock title="可清空（clearable）">
  <oas-cascader clearable value='["zj","hz"]' placeholder="可清空" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
  <oas-cascader clearable multiple value='[["zj","hz"],["js","nj"]]' placeholder="多选可清空" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

有选中值时显示清空按钮，点击清空并派发 `oas-clear`（detail 为清空前的值）与 `oas-change`（空值）。

## 标签折叠

<DemoBlock title="标签折叠（max-tag-count）">
  <oas-cascader multiple max-tag-count="2" value='[["zj","hz"],["zj","nb"],["js","nj"]]' placeholder="超出折叠为 +N" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
</DemoBlock>

多选标签默认换行展示；显式设置 `max-tag-count` 后按数量折叠为 `+N`（悬浮列出被折叠项），与 `oas-select` 约定一致。

## 尺寸与校验态

<DemoBlock title="尺寸（size）">
  <oas-cascader size="small" placeholder="small" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <oas-cascader placeholder="medium（默认）" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <oas-cascader size="large" placeholder="large" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
</DemoBlock>

<DemoBlock title="校验态（status）">
  <oas-cascader status="error" placeholder="error" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <oas-cascader status="warning" placeholder="warning" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <oas-cascader status="success" placeholder="success" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
</DemoBlock>

`size` 支持 `small / medium / large`（就近继承 `oas-config-provider` 的全局尺寸）；`status` 支持 `error / warning / success`（`error` 同步 trigger 的 `aria-invalid`，表单校验亦可直接在宿主标 `aria-invalid`）。

## 受控开合

<DemoBlock title="受控开合（open + oas-open-change）">
  <oas-space size="small">
    <oas-button id="cs-open-btn" size="small">打开 / 关闭</oas-button>
    <oas-cascader id="cs-controlled" placeholder="open 属性受控" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
    <oas-tag id="cs-open-state" type="info">open: false</oas-tag>
  </oas-space>
</DemoBlock>

`open` 属性为展开状态的唯一数据源（受控）：组件内外的开合都写回该属性，并在任何状态翻转时派发 `oas-open-change`（`detail.open`）；宿主可拦截事件回写实现完全受控。`Esc` 关闭时焦点返回触发器。

## 空态

<DemoBlock title="空态（empty）">
  <oas-cascader placeholder="暂无选项" options='[]'></oas-cascader>
</DemoBlock>

选项为空（且未设置 `el.load`）时面板显示「暂无数据」；搜索无匹配时显示「无匹配选项」。

## 加载态（loading）

<DemoBlock title="加载态（loading）">
  <oas-space size="small">
    <oas-button size="small" onclick="document.querySelector('#cs-loading').setAttribute('loading','')">进入 loading</oas-button>
    <oas-button size="small" onclick="document.querySelector('#cs-loading').removeAttribute('loading')">结束 loading</oas-button>
    <oas-cascader id="cs-loading" placeholder="选择地区" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]}]'></oas-cascader>
  </oas-space>
</DemoBlock>

`loading` 期间触发器显示 spinner 并标 `aria-busy`（替代展开箭头）；面板展开时只显示「加载中…」占位，不渲染选项——适合宿主拉取远程级联数据的请求窗口期。

## 字段映射（field-names）

<DemoBlock title="字段映射（field-names）">
  <oas-cascader id="cs-fields" placeholder="数据字段为 name/id/subs/off" options='[{"name":"前端","id":"fe","subs":[{"name":"Vue","id":"vue","off":false}]},{"name":"设计","id":"design","subs":[{"name":"UI","id":"ui","off":true}]}]' field-names='{"label":"name","value":"id","children":"subs","disabled":"off"}'></oas-cascader>
  <span id="cs-fields-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

`field-names` 为 JSON 字段别名（`{ label, value, children, disabled }`，对齐 `oas-tree-select` 契约），组件按别名读取原始数据（`isLeaf` 恒读原始键名）；懒加载 `el.load` 返回的数据同样按映射归一。上例中「设计 / UI」经 `off` 字段禁用。

## 禁用

<DemoBlock title="禁用">
  <oas-cascader disabled value='["zj","hz"]' placeholder="禁用" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
</DemoBlock>

## 无障碍名称（label）

<DemoBlock title="label（可访问名称）">
  <oas-cascader id="cs-label-set" label="所属地区" placeholder="请选择地区" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <oas-cascader id="cs-label-fallback" placeholder="无 label，回退占位文本" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <span id="cs-label-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 280px"></span>
</DemoBlock>

`label` 作为触发器的可访问名称（`aria-label`）来源：设置后读屏朗读该名称，优先于选中值文本与占位文本回退；未设置时按值文本 / 占位文本回退（对齐 `oas-select` 契约）。

## 弹出方向（placement）

<DemoBlock title="placement（面板 12 向）">
  <oas-space size="small">
    <oas-cascader placement="top-start" placeholder="top-start" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
    <oas-cascader placement="bottom" placeholder="bottom（默认）" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
    <oas-cascader id="cs-placement-right" placement="right-start" placeholder="right-start" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  </oas-space>
</DemoBlock>

`placement` 支持 12 向：`top` / `bottom` / `left` / `right` × `start` / `end` / 居中（如 `top-start`、`right-end`），缺省 `bottom`（下方弹出，空间不足自动翻转避让）；非法值静默回落。侧边栏、表格内等贴边场景可直接声明方向。

## 自定义后缀图标（suffix-icon 插槽）

<DemoBlock title="suffix-icon（替换默认箭头）">
  <oas-cascader id="cs-suffix-icon" placeholder="自定义后缀图标" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'>
    <template slot="suffix-icon"><span style="display:inline-flex">▤</span></template>
  </oas-cascader>
</DemoBlock>

`template[slot="suffix-icon"]` 替换触发器默认下拉箭头（展开时随容器旋转；`loading` 时仍由 spinner 接管）。

## 自定义选项渲染（option 插槽）

<DemoBlock title="option（选项行自定义渲染）">
  <oas-cascader id="cs-option-slot" placeholder="带图标的选项行" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'>
    <template slot="option"><span style="display:inline-flex">📍</span><span data-option-label></span></template>
  </oas-cascader>
</DemoBlock>

`template[slot="option"]` 克隆到每个选项行（面板行与搜索结果行两路生效），`[data-option-label]` 节点自动绑定展示文本（面板行为选项 label、搜索行为完整路径文本）；也可监听宿主事件改写行内容。

## 事件

<DemoBlock title="选中事件">
  <oas-cascader id="cs-event" placeholder="选择后触发 oas-change" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"},{"label":"宁波","value":"nb"}]},{"label":"江苏","value":"js","children":[{"label":"南京","value":"nj"}]}]'></oas-cascader>
  <span id="cs-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

监听 `oas-change`，`detail.value` 单选为路径数组、多选为路径数组的数组：

## 焦点事件（oas-focus / oas-blur）

<DemoBlock title="focus / blur 事件">
  <oas-cascader id="cs-focus" placeholder="聚焦 / 失焦看反馈" options='[{"label":"浙江","value":"zj","children":[{"label":"杭州","value":"hz"}]}]'></oas-cascader>
  <span id="cs-focus-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

触发器获得/失去焦点派发 `oas-focus` / `oas-blur`（`detail.value` 与 `oas-change` 同形状：单选路径数组、多选路径二维数组）；触发器 ↔ 搜索框 ↔ 标签按钮之间的组件内焦点转移不会误报。

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  const pathText = (paths) => `[${paths.map((p) => p.join('/')).join(', ')}]`

  // 多选 demo：值反馈
  const multi = document.getElementById('cs-multi')
  const multiOut = document.getElementById('cs-multi-output')
  multi?.addEventListener('oas-change', (e) => {
    multiOut.textContent = `oas-change: ${pathText(e.detail.value)}`
  })

  // 搜索 demo：oas-search / oas-change 反馈
  const search = document.getElementById('cs-search')
  const searchOut = document.getElementById('cs-search-output')
  search?.addEventListener('oas-search', (e) => {
    searchOut.textContent = `oas-search: ${e.detail.value}`
  })
  search?.addEventListener('oas-change', (e) => {
    searchOut.textContent = `oas-change: ${pathText(e.detail.value)}`
  })

  // 懒加载 demo：el.load property 通道（模拟 500ms 延迟）
  customElements.whenDefined('oas-cascader').then(() => {
    const lazy = document.getElementById('cs-lazy')
    if (lazy) {
      lazy.load = ({ path }) =>
        new Promise((resolve) => {
          setTimeout(() => {
            if (path.length === 0) {
              resolve([
                { label: '技术中心', value: 'tech' },
                { label: '运营中心', value: 'ops' },
              ])
            } else if (path[0] === 'tech') {
              resolve([
                { label: '前端组', value: 'fe', isLeaf: true },
                { label: '后端组', value: 'be', isLeaf: true },
                { label: '测试组', value: 'qa' },
              ])
            } else if (path[1] === 'qa') {
              resolve([
                { label: '功能测试', value: 'func', isLeaf: true },
                { label: '自动化', value: 'auto', isLeaf: true },
              ])
            } else {
              resolve([])
            }
          }, 500)
        })
    }
  })

  // 受控开合 demo：按钮写 open 属性 + 状态回显
  const btn = document.getElementById('cs-open-btn')
  const controlled = document.getElementById('cs-controlled')
  const state = document.getElementById('cs-open-state')
  btn?.addEventListener('click', () => {
    if (!controlled) return
    if (controlled.hasAttribute('open')) controlled.removeAttribute('open')
    else controlled.setAttribute('open', '')
  })
  controlled?.addEventListener('oas-open-change', (e) => {
    if (state) state.textContent = `open: ${e.detail.open}`
  })

  // 事件 demo
  const el = document.getElementById('cs-event')
  const out = document.getElementById('cs-output')
  el?.addEventListener('oas-change', (e) => {
    out.textContent = `oas-change: ${pathText([e.detail.value])}`
  })

  // 字段映射 demo：选择反馈映射后的 value
  const fields = document.getElementById('cs-fields')
  const fieldsOut = document.getElementById('cs-fields-output')
  fields?.addEventListener('oas-change', (e) => {
    fieldsOut.textContent = `oas-change: ${pathText([e.detail.value])}`
  })

  // 焦点事件 demo：触发器聚焦/失焦反馈
  const focus = document.getElementById('cs-focus')
  const focusOut = document.getElementById('cs-focus-output')
  focus?.addEventListener('oas-focus', () => {
    focusOut.textContent = 'oas-focus'
  })
  focus?.addEventListener('oas-blur', () => {
    focusOut.textContent = 'oas-blur'
  })

  // 虚拟滚动 demo：1000 项长列（各带一个叶子子级，数据由脚本注入 options 通道）+ 选中反馈
  const virtualCs = document.getElementById('cs-virtual')
  const virtualCsOut = document.getElementById('cs-virtual-output')
  if (virtualCs) {
    virtualCs.setAttribute(
      'options',
      JSON.stringify(
        Array.from({ length: 1000 }, (_, i) => ({
          label: `项目 ${i}`,
          value: `v${i}`,
          children: [{ label: `${i}-子`, value: `c${i}` }],
        })),
      ),
    )
    virtualCs.addEventListener('oas-change', (e) => {
      if (virtualCsOut) virtualCsOut.textContent = `oas-change: ${(e.detail.value || []).join(' / ')}`
    })
  }

  // label（可访问名称）demo：读取触发器 aria-label（设置 label 与回退占位对照）
  const readLabel = () => {
    const a = document.getElementById('cs-label-set')?.shadowRoot?.querySelector('[part="trigger"]')?.getAttribute('aria-label')
    const b = document.getElementById('cs-label-fallback')?.shadowRoot?.querySelector('[part="trigger"]')?.getAttribute('aria-label')
    const out = document.getElementById('cs-label-output')
    if (a && b && out) {
      out.textContent = `aria-label：设置「${a}」 / 回退「${b}」`
    } else {
      setTimeout(readLabel, 60)
    }
  }
  readLabel()
})
</script>

## 移动形态（底部抽屉）

触屏（coarse pointer）或窄视口（<768px）下，多级面板自动改由 `oas-bottom-sheet` 底部抽屉承载：贴视口底升起 + 遮罩 + 拖拽把手下滑关闭（点遮罩 / Esc 同样收起），底部避让安全区；多级列在抽屉内容区内横向滑动；PC 形态保持原有浮层下拉不变。

## API

### oas-cascader

#### 属性

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `change-on-select` | 选中任意层级即提交 | `boolean` | — |
| `check-strictly` | 多选父子勾选解绑（勾选父级不联动子级） | `boolean` | — |
| `clearable` | 可清空（派发 oas-clear） | `boolean` | — |
| `disabled` | 禁用 | `boolean` | — |
| `expand-trigger` | 子级展开触发方式：`click`（默认）/ `hover`（120ms 延时防误触） | `string` | `click` |
| `field-names` | 字段别名 JSON（`{ label, value, children, disabled }`），对齐 tree-select 契约；懒加载结果同样映射 | `string` | — |
| `filterable` | 可搜索（扁平路径结果） | `boolean` | — |
| `item-height` | 虚拟列定高（px，配合 virtual，默认 36） | `string` | `36` |
| `label` | 触发器可访问名称（aria-label），优先于值/占位回退 | `string` | — |
| `loading` | 加载态：触发器 spinner + aria-busy，面板显示加载占位 | `boolean` | — |
| `max-tag-count` | 多选标签按数量折叠 +N（带 title 列隐藏项） | `number` | — |
| `multiple` | 多选（复选框级联勾选；子级全选父级自动进值） | `boolean` | — |
| `open` | 受控展开状态（唯一状态源）；移动形态（触屏/窄视口 <768px）下展开态由 oas-bottom-sheet 底部抽屉承载（下滑/遮罩/Esc 收起），PC 为浮层下拉 | `boolean` | — |
| `options` | 级联选项，JSON 数组，支持 `children` / `disabled` | `CascaderOption[] \| string` | `[]` |
| `placeholder` | 占位提示 | — | — |
| `placement` | 面板 12 向弹出方向（非法回落 bottom，保留自动翻转避让） | `string` | `bottom` |
| `separator` | 路径分隔符（默认 ` / `） | `string` | ` / ` |
| `show-all-levels` | 回显完整路径（默认 true）；`false` 仅末级 | `string` | `true` |
| `size` | 尺寸档位 `small` / `medium`（默认）/ `large` | `string` | `medium` |
| `status` | 校验态：`error` / `warning` / `success`；error 联动 aria-invalid | `string` | — |
| `value` | 路径数组（JSON），如 `["zj","hz"]` | `string[] \| string[][] \| null` | `[]` |
| `value-mode` | 多选值策略：`all`（默认，全路径）/ `parentFirst` / `onlyLeaf` | `string` | `all` |
| `virtual` | 面板列虚拟滚动（超过 50 项的长列复用 oas-virtual-list 窗口渲染；短列/搜索/加载占位保持普通渲染） | `boolean` | — |

#### Property（JS property；是否写回 attribute 见各行说明）

| Property | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `value` | 当前值：单选取选中路径数组，`multiple` 时取选中路径集合数组 | `string[] \| string[][] \| null` | — |

#### 事件

| 事件 | 说明 |
| --- | --- |
| `oas-blur` | 触发器失焦时派发，`detail.value` 与 oas-change 同形状（组件内焦点转移不误报） |
| `oas-change` | 选择变化，`detail: { value }`（路径数组） |
| `oas-clear` | 点击清空按钮，`detail` 为清空前的值 |
| `oas-focus` | 触发器聚焦时派发，`detail.value` 与 oas-change 同形状（组件内焦点转移不误报） |
| `oas-open-change` | 展开状态翻转，`detail: { open }` |
| `oas-search` | filterable 输入，`detail: { value }` |

#### 插槽

| 名称 | 说明 |
| --- | --- |
| `template[slot="option"]` | 选项行自定义渲染模板，`[data-option-label]` 节点绑定选项文本 |
| `template[slot="suffix-icon"]` | 自定义触发器后缀图标（替换默认箭头；loading 时让位 spinner） |

#### CSS 变量

| CSS 变量 | 默认值 |
| --- | --- |
| `--oas-cascader-dropdown-height` | `240px` |
| `--oas-glass-blur` | `none` |
| `--oas-glass-ring` | `transparent` |
