# CascadeOptionSelect 级联选择组件

一个用于从嵌套数据结构中选择选项的分层下拉组件。非常适合位置选择器（国家/省份/城市）、分类层次结构、组织架构以及任何多级选择场景。

## 功能特性

- **分层选择**: 导航通过嵌套选项层级
- **动态加载**: 通过回调异步加载子选项
- **灵活数据结构**: 可配置字段映射适应不同数据格式
- **叶子节点检测**: 自定义逻辑确定可选择的终端节点
- **显示模式**: 编辑和显示模式适应不同UI场景
- **键盘和焦点支持**: 完整的键盘导航和焦点管理
- **加载状态**: 异步操作的内置加载指示器
- **可定制样式**: 灵活的外观选项和变体
- **清除/必选选项**: 可选的清除功能和必选选择

## 基本用法

```svelte
<script>
import CascadeOptionSelect from '@ticatec/uniface-element/CascadeOptionSelect';

const locationData = [
  {
    code: 'CN',
    text: '中国',
    abbr: '中国',
    children: [
      {
        code: 'BJ',
        text: '北京市',
        abbr: '北京',
        children: [
          { code: 'HD', text: '海淀区', abbr: '海淀' },
          { code: 'CY', text: '朝阳区', abbr: '朝阳' }
        ]
      }
    ]
  }
];

let selectedValue = null;

const handleChange = (value) => {
  console.log('选择:', value);
  selectedValue = value;
};
</script>

<CascadeOptionSelect 
  nodes={locationData}
  bind:value={selectedValue}
  onchange={handleChange}
  placeholder="选择位置..."
/>
```

## 属性参数

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `variant` | `'' \| 'plain' \| 'outlined' \| 'filled'` | `''` | 视觉变体样式 |
| `disabled` | `boolean` | `false` | 是否禁用组件 |
| `readonly` | `boolean` | `false` | 是否只读 |
| `compact` | `boolean` | `false` | 紧凑显示模式 |
| `mandatory` | `boolean` | `false` | 是否必选（禁用清除功能） |
| `value` | `any` | `null` | 选中的值（键字段值） |
| `keyField` | `string` | `'code'` | 选项键/值的字段名 |
| `textField` | `string` | `'text'` | 选项显示文本的字段名 |
| `abbrField` | `string` | `'abbr'` | 下拉框中缩写文本的字段名 |
| `childrenField` | `string` | `'children'` | 子选项的字段名 |
| `style` | `string` | `''` | 自定义CSS样式 |
| `placeholder` | `string` | `''` | 占位符文本 |
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 显示或编辑模式 |
| `emptyText` | `string \| null` | `null` | 无选择时显示的文本 |
| `checkLeaf` | `IsLeafDetermine \| null` | `null` | 判断项目是否为可选叶子节点的函数 |
| `text` | `string` | `''` | 显示文本（自动管理） |
| `nodes` | `Array<any>` | `[]` | 根级选项数组 |
| `menu$height` | `number` | `150` | 下拉菜单高度（像素） |
| `onSelect` | `OnSelectOption` | - | 加载子选项的异步函数 |
| `box$style` | `string` | `''` | 选项框的自定义样式 |
| `onchange` | `OnChangeHandler<any>` | - | 值变化事件处理器 |
| `onfocus` | `(() => void) \| null` | `null` | 聚焦事件处理器 |
| `onblur` | `(() => void) \| null` | `null` | 失焦事件处理器 |

## 类型定义

```typescript
// 异步加载子选项的函数
type OnSelectOption = (item: any) => Promise<Array<any>>;

// 判断项目是否为可选叶子节点的函数
type IsLeafDetermine = (item: any) => boolean;

// 值变化事件处理器
type OnChangeHandler<T> = (value: T) => void;
```

## 静态数据示例

```svelte
<script>
const categories = [
  {
    code: 'electronics',
    text: '电子产品',
    abbr: '电子',
    children: [
      {
        code: 'computers',
        text: '计算机',
        abbr: '计算机',
        children: [
          { code: 'laptops', text: '笔记本电脑', abbr: '笔记本' },
          { code: 'desktops', text: '台式机', abbr: '台式机' }
        ]
      },
      {
        code: 'phones',
        text: '手机',
        abbr: '手机',
        children: [
          { code: 'smartphones', text: '智能手机', abbr: '智能机' },
          { code: 'basic-phones', text: '功能手机', abbr: '功能机' }
        ]
      }
    ]
  }
];

let selectedCategory = null;
</script>

<CascadeOptionSelect 
  nodes={categories}
  bind:value={selectedCategory}
  placeholder="选择分类..."
  onchange={(value) => console.log('选择的分类:', value)}
/>
```

## 动态加载示例

```svelte
<script>
const loadRegions = async (country) => {
  // 模拟API调用
  const response = await fetch(`/api/regions?country=${country.code}`);
  return await response.json();
};

const loadCities = async (region) => {
  const response = await fetch(`/api/cities?region=${region.code}`);
  return await response.json();
};

const dynamicLoader = async (item) => {
  // 根据项目类型确定要加载的数据
  if (!item.children) {
    if (item.type === 'country') {
      return await loadRegions(item);
    } else if (item.type === 'region') {
      return await loadCities(item);
    }
  }
  return item.children || [];
};

const countries = [
  { code: 'CN', text: '中国', abbr: '中国', type: 'country' },
  { code: 'US', text: '美国', abbr: '美国', type: 'country' },
  { code: 'JP', text: '日本', abbr: '日本', type: 'country' }
];

let selectedLocation = null;
</script>

<CascadeOptionSelect 
  nodes={countries}
  bind:value={selectedLocation}
  onSelect={dynamicLoader}
  placeholder="选择位置..."
  onchange={(value) => console.log('选择的位置:', value)}
/>
```

## 自定义字段映射

```svelte
<script>
// 使用不同字段名的数据
const orgData = [
  {
    id: 'corp',
    name: '集团公司',
    short: '集团',
    divisions: [
      {
        id: 'tech',
        name: '技术部门',
        short: '技术',
        departments: [
          { id: 'dev', name: '开发部', short: '开发' },
          { id: 'qa', name: '质量保证部', short: '质保' }
        ]
      }
    ]
  }
];

let selectedOrg = null;
</script>

<CascadeOptionSelect 
  nodes={orgData}
  bind:value={selectedOrg}
  keyField="id"
  textField="name"
  abbrField="short"
  childrenField="divisions"
  placeholder="选择组织..."
/>
```

## 叶子节点检测示例

```svelte
<script>
const menuData = [
  {
    code: 'file',
    text: '文件',
    abbr: '文件',
    isLeaf: false,
    children: [
      { code: 'new', text: '新建文件', abbr: '新建', isLeaf: true },
      { code: 'open', text: '打开文件', abbr: '打开', isLeaf: true },
      { code: 'save', text: '保存文件', abbr: '保存', isLeaf: true }
    ]
  }
];

const checkIfLeaf = (item) => {
  return item.isLeaf === true;
};

let selectedMenuItem = null;
</script>

<CascadeOptionSelect 
  nodes={menuData}
  bind:value={selectedMenuItem}
  checkLeaf={checkIfLeaf}
  placeholder="选择菜单项..."
/>
```

## 不同变体样式

```svelte
<!-- 默认变体 -->
<CascadeOptionSelect 
  nodes={data}
  placeholder="默认样式"
/>

<!-- 平面变体 -->
<CascadeOptionSelect 
  nodes={data}
  variant="plain"
  placeholder="平面样式"
/>

<!-- 轮廓变体 -->
<CascadeOptionSelect 
  nodes={data}
  variant="outlined"
  placeholder="轮廓样式"
/>

<!-- 填充变体 -->
<CascadeOptionSelect 
  nodes={data}
  variant="filled"
  placeholder="填充样式"
/>
```

## 事件处理

```svelte
<script>
let currentValue = null;
let focusCount = 0;

const handleChange = (value) => {
  console.log('值变化为:', value);
  currentValue = value;
};

const handleFocus = () => {
  console.log('组件获得焦点');
  focusCount++;
};

const handleBlur = () => {
  console.log('组件失去焦点');
};
</script>

<CascadeOptionSelect 
  nodes={data}
  bind:value={currentValue}
  onchange={handleChange}
  onfocus={handleFocus}
  onblur={handleBlur}
  placeholder="事件处理示例"
/>

<p>当前值: {currentValue}</p>
<p>聚焦次数: {focusCount}</p>
```

## 高级示例

### 带API集成的位置选择器

```svelte
<script>
import { onMount } from 'svelte';

let countries = [];
let selectedLocation = null;

onMount(async () => {
  // 加载初始国家列表
  const response = await fetch('/api/countries');
  countries = await response.json();
});

const loadLocationChildren = async (location) => {
  let endpoint = '';
  
  if (location.type === 'country') {
    endpoint = `/api/provinces?country=${location.code}`;
  } else if (location.type === 'province') {
    endpoint = `/api/cities?province=${location.code}`;
  }
  
  if (endpoint) {
    const response = await fetch(endpoint);
    return await response.json();
  }
  
  return [];
};

const isLocationLeaf = (location) => {
  return location.type === 'city';
};
</script>

<div class="location-selector">
  <label>选择位置:</label>
  <CascadeOptionSelect 
    nodes={countries}
    bind:value={selectedLocation}
    onSelect={loadLocationChildren}
    checkLeaf={isLocationLeaf}
    placeholder="国家 / 省份 / 城市"
    mandatory={true}
    variant="outlined"
  />
</div>
```

### 产品分类导航器

```svelte
<script>
const productCategories = [
  {
    code: 'electronics',
    text: '电子产品',
    abbr: '电子',
    children: [
      {
        code: 'computers',
        text: '计算机及配件',
        abbr: '计算机',
        children: [
          { code: 'laptops', text: '笔记本电脑', abbr: '笔记本' },
          { code: 'desktops', text: '台式电脑', abbr: '台式机' },
          { code: 'accessories', text: '电脑配件', abbr: '配件' }
        ]
      },
      {
        code: 'mobile',
        text: '移动设备',
        abbr: '移动',
        children: [
          { code: 'smartphones', text: '智能手机', abbr: '手机' },
          { code: 'tablets', text: '平板电脑', abbr: '平板' },
          { code: 'wearables', text: '可穿戴设备', abbr: '穿戴' }
        ]
      }
    ]
  },
  {
    code: 'clothing',
    text: '服装时尚',
    abbr: '服装',
    children: [
      {
        code: 'mens',
        text: '男装',
        abbr: '男装',
        children: [
          { code: 'shirts', text: '衬衫', abbr: '衬衫' },
          { code: 'pants', text: '裤子', abbr: '裤子' },
          { code: 'shoes', text: '鞋子', abbr: '鞋子' }
        ]
      }
    ]
  }
];

let selectedCategory = null;

$: if (selectedCategory) {
  console.log('浏览分类的产品:', selectedCategory);
}
</script>

<div class="category-navigator">
  <h3>浏览产品</h3>
  <CascadeOptionSelect 
    nodes={productCategories}
    bind:value={selectedCategory}
    placeholder="选择分类浏览..."
    variant="filled"
    menu$height={200}
  />
</div>
```

### 组织层次结构选择器

```svelte
<script>
const organizationData = [
  {
    code: 'headquarters',
    text: '总部',
    abbr: '总部',
    children: [
      {
        code: 'exec',
        text: '高管',
        abbr: '高管',
        children: [
          { code: 'ceo', text: '首席执行官', abbr: 'CEO' },
          { code: 'cto', text: '首席技术官', abbr: 'CTO' }
        ]
      },
      {
        code: 'engineering',
        text: '工程部',
        abbr: '工程',
        children: [
          { code: 'frontend', text: '前端开发', abbr: '前端' },
          { code: 'backend', text: '后端开发', abbr: '后端' },
          { code: 'devops', text: '运维开发', abbr: '运维' }
        ]
      }
    ]
  }
];

let assignedDepartment = null;

const handleDepartmentChange = (value) => {
  console.log('分配到部门:', value);
  // 更新用户分配
};
</script>

<div class="department-assignment">
  <label>分配到部门:</label>
  <CascadeOptionSelect 
    nodes={organizationData}
    bind:value={assignedDepartment}
    onchange={handleDepartmentChange}
    placeholder="选择部门..."
    mandatory={true}
    compact={true}
  />
</div>
```

### 带动态加载的菜单系统

```svelte
<script>
let menuItems = [
  { code: 'file', text: '文件', abbr: '文件', hasChildren: true },
  { code: 'edit', text: '编辑', abbr: '编辑', hasChildren: true },
  { code: 'view', text: '查看', abbr: '查看', hasChildren: true }
];

const loadMenuChildren = async (menuItem) => {
  // 模拟从服务器加载菜单项
  await new Promise(resolve => setTimeout(resolve, 500));
  
  const menuMap = {
    'file': [
      { code: 'new', text: '新建', abbr: '新建', action: 'file.new' },
      { code: 'open', text: '打开', abbr: '打开', action: 'file.open' },
      { code: 'save', text: '保存', abbr: '保存', action: 'file.save' }
    ],
    'edit': [
      { code: 'cut', text: '剪切', abbr: '剪切', action: 'edit.cut' },
      { code: 'copy', text: '复制', abbr: '复制', action: 'edit.copy' },
      { code: 'paste', text: '粘贴', abbr: '粘贴', action: 'edit.paste' }
    ],
    'view': [
      { code: 'zoom-in', text: '放大', abbr: '放大', action: 'view.zoomIn' },
      { code: 'zoom-out', text: '缩小', abbr: '缩小', action: 'view.zoomOut' }
    ]
  };
  
  return menuMap[menuItem.code] || [];
};

const isMenuItem = (item) => {
  return item.action != null;
};

let selectedAction = null;

$: if (selectedAction) {
  console.log('执行操作:', selectedAction);
}
</script>

<CascadeOptionSelect 
  nodes={menuItems}
  bind:value={selectedAction}
  onSelect={loadMenuChildren}
  checkLeaf={isMenuItem}
  placeholder="选择操作..."
  variant="outlined"
/>
```

## 样式定制

组件使用以下CSS类，可以通过以下方式进行样式定制：

- 主容器的自定义 `style` 属性
- 单个选项框的 `box$style` 属性
- 下拉框高度的 `menu$height` 属性
- 深度定制的CSS自定义属性

### 样式示例

```svelte
<CascadeOptionSelect 
  nodes={data}
  style="border-radius: 8px; border: 2px solid #007acc;"
  box$style="background: #f5f5f5; border-radius: 4px;"
  menu$height={250}
/>
```

## 无障碍访问

组件包含：

- 适当的焦点管理
- 键盘导航支持
- 屏幕阅读器的ARIA属性
- 聚焦和失焦事件处理
- 选中项目的清晰视觉指示器

## 最佳实践

1. **数据结构**: 在所有层级使用一致的字段命名
2. **加载状态**: 在异步操作期间提供反馈
3. **错误处理**: 在onSelect回调中优雅地处理API失败
4. **性能**: 对于非常大的数据集考虑虚拟化
5. **验证**: 当mandatory为true时验证叶子节点选择
6. **响应式设计**: 在移动设备上测试下拉框行为
7. **用户体验**: 在选项显示中提供清晰的视觉层次

## 常见模式

### 表单集成

```svelte
<script>
let formData = {
  location: null,
  category: null
};

const validateForm = () => {
  return formData.location && formData.category;
};
</script>

<form>
  <div class="form-field">
    <label>位置:</label>
    <CascadeOptionSelect 
      nodes={locationData}
      bind:value={formData.location}
      mandatory={true}
      placeholder="选择位置..."
    />
  </div>
  
  <button type="submit" disabled={!validateForm()}>
    提交
  </button>
</form>
```

### 状态管理

```svelte
<script>
import { writable } from 'svelte/store';

const selectedValues = writable({
  location: null,
  category: null,
  department: null
});

$: console.log('当前选择:', $selectedValues);
</script>

<CascadeOptionSelect 
  nodes={locationData}
  value={$selectedValues.location}
  onchange={(value) => selectedValues.update(s => ({...s, location: value}))}
/>
```

## 浏览器支持

在支持以下特性的所有现代浏览器中工作：
- ES6+ JavaScript
- CSS Flexbox
- 现代DOM API
- Promises/async-await

## 性能考虑

- 通过选项框中的虚拟滚动高效处理大数据集
- 防抖异步加载以防止过度API调用
- 适当清理事件监听器和超时
- 使用Svelte响应式系统优化重新渲染