# ListBox 列表框组件

一个多功能的列表组件，支持过滤、懒加载、单选/多选模式和自定义项目渲染。

## 特性

- **多种选择模式**: 支持无选择、单选和多选模式
- **搜索和过滤**: 内置搜索功能，支持自定义过滤函数
- **懒加载**: 支持分页数据加载和无限滚动
- **自定义渲染**: 灵活的项目渲染，支持自定义组件
- **交互状态**: 悬停、选中和点击处理
- **头部/底部插槽**: 可自定义的头部和底部区域
- **加载状态**: 内置异步操作的加载指示器
- **无障碍访问**: 键盘导航和屏幕阅读器支持

## 安装

```bash
npm install @ticatec/uniface-element
```

## 使用方法

### 基本用法

```svelte
<script>
  import ListBox from '@ticatec/uniface-element/list-box';
  import MyItemComponent from './MyItemComponent.svelte';
  
  let items = [
    { id: 1, name: '项目 1', description: '第一个项目' },
    { id: 2, name: '项目 2', description: '第二个项目' },
    { id: 3, name: '项目 3', description: '第三个项目' }
  ];
  
  let selectedItem = null;
</script>

<ListBox 
  list={items}
  itemRender={MyItemComponent}
  selectMode="single"
  bind:selectedItem
  style="width: 300px; height: 400px;"
/>
```

### 只读列表显示

```svelte
<ListBox 
  list={items}
  itemRender={ItemRenderer}
  selectMode="none"
  readonly={true}
  style="width: 100%; height: 300px;"
/>
```

### 多选模式

```svelte
<script>
  let selectedList = [];
</script>

<ListBox 
  list={items}
  itemRender={ItemRenderer}
  selectMode="multiple"
  bind:selectedList
  title="选择多个项目"
  style="width: 100%; height: 400px;"
/>

<p>已选择: {selectedList.length} 个项目</p>
```

### 带搜索/过滤功能

```svelte
<script>
  const filterFunction = (item, searchText) => {
    return item.name.toLowerCase().includes(searchText.toLowerCase()) ||
           item.description.toLowerCase().includes(searchText.toLowerCase());
  };
</script>

<ListBox 
  list={items}
  itemRender={ItemRenderer}
  selectMode="single"
  filter={filterFunction}
  title="可搜索列表"
  bind:selectedItem
/>
```

### 懒加载

```svelte
<script>
  const lazyLoader = async (searchText, pageNo) => {
    const response = await fetch(`/api/items?search=${searchText}&page=${pageNo}`);
    const data = await response.json();
    
    return {
      list: data.items,
      hasMore: data.hasMore
    };
  };
</script>

<ListBox 
  lazyLoader={lazyLoader}
  itemRender={ItemRenderer}
  selectMode="single"
  bind:selectedItem
  style="width: 100%; height: 500px;"
/>
```

### 自定义头部和底部

```svelte
<ListBox 
  list={items}
  itemRender={ItemRenderer}
  selectMode="single"
  bind:selectedItem
>
  <div slot="header" style="padding: 12px; background: #f8f9fa; font-weight: 600;">
    自定义头部 - 总计: {items.length}
  </div>
  
  <div slot="footer" style="padding: 8px; text-align: right; background: #f8f9fa;">
    已选择: {selectedItem?.name || '无'}
  </div>
</ListBox>
```

## API 参考

### 属性

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `list` | `Array<any>` | `[]` | 要显示的项目数组 |
| `itemRender` | `SvelteComponent` | - | 渲染每个项目的组件 |
| `selectMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | 选择模式 |
| `selectedItem` | `any` | `null` | 当前选中的项目（单选模式） |
| `selectedList` | `Array<any>` | `[]` | 选中项目的数组（多选模式） |
| `filter` | `FunFilter \| null` | `null` | 搜索的过滤函数 |
| `lazyLoader` | `LazyLoader \| null` | `null` | 懒加载函数 |
| `readonly` | `boolean` | `false` | 列表是否为只读 |
| `title` | `string` | - | 头部的标题文本 |
| `style` | `string` | `''` | 自定义CSS样式 |
| `header$style` | `string` | `''` | 头部的自定义样式 |
| `footer$style` | `string` | `''` | 底部的自定义样式 |
| `round` | `boolean` | `false` | 是否使用圆角 |
| `item$props` | `any` | `null` | 传递给项目组件的额外属性 |
| `class` | `string` | `''` | 额外的CSS类 |

### 事件

| 事件 | 类型 | 描述 |
|-------|------|-------------|
| `onSelectChange` | `(item: any) => void` | 选择改变时触发（单选模式） |
| `onItemClick` | `(item: any) => void` | 项目被点击时触发 |
| `onItemDblClick` | `(item: any) => void` | 项目被双击时触发 |

### 插槽

| 插槽 | 描述 |
|------|-------------|
| `header` | 自定义头部内容 |
| `footer` | 自定义底部内容 |
| `loadMoreIndicator` | 懒加载的自定义加载指示器 |

### 类型定义

```typescript
// 过滤函数类型
type FunFilter = (item: any, searchText: string) => boolean;

// 懒加载函数类型
type LazyLoader = (searchText: string, pageNo: number) => Promise<LoadResult>;

// 加载结果接口
interface LoadResult {
  list: Array<any>;
  hasMore: boolean;
}
```

## 自定义项目渲染器

创建自定义组件来渲染列表项：

```svelte
<!-- EmployeeRenderer.svelte -->
<script lang="ts">
  export let item;
  export let selected = false;
  export let readonly = false;
</script>

<div class="employee-item" class:selected>
  <div class="employee-name">{item.name}</div>
  <div class="employee-department">{item.department}</div>
  <div class="employee-phone">{item.phone}</div>
</div>

<style>
  .employee-item {
    padding: 12px;
    border-radius: 4px;
    transition: background-color 0.2s;
  }
  
  .employee-item.selected {
    background-color: #e3f2fd;
  }
  
  .employee-name {
    font-weight: 600;
    font-size: 1.1em;
    margin-bottom: 4px;
  }
  
  .employee-department {
    color: #666;
    font-size: 0.9em;
  }
  
  .employee-phone {
    color: #888;
    font-size: 0.8em;
    margin-top: 4px;
  }
</style>
```

## 示例

### 员工目录

```svelte
<script>
  import ListBox from '@ticatec/uniface-element/list-box';
  import EmployeeRenderer from './EmployeeRenderer.svelte';
  
  let employees = [
    { id: 1, name: '张三', department: '工程部', phone: '+86-138-0013-8000' },
    { id: 2, name: '李四', department: '市场部', phone: '+86-138-0013-8001' },
    { id: 3, name: '王五', department: '销售部', phone: '+86-138-0013-8002' }
  ];
  
  let selectedEmployee = null;
  
  const filterEmployees = (employee, searchText) => {
    const text = searchText.toLowerCase();
    return employee.name.toLowerCase().includes(text) ||
           employee.department.toLowerCase().includes(text);
  };
  
  function handleEmployeeSelect(employee) {
    console.log('选中员工:', employee);
  }
</script>

<ListBox 
  list={employees}
  itemRender={EmployeeRenderer}
  selectMode="single"
  filter={filterEmployees}
  bind:selectedItem={selectedEmployee}
  onSelectChange={handleEmployeeSelect}
  title="员工目录"
  style="width: 400px; height: 500px;"
>
  <div slot="footer" style="padding: 8px; text-align: center; background: #f5f5f5;">
    总员工数: {employees.length}
    {#if selectedEmployee}
      | 已选择: {selectedEmployee.name}
    {/if}
  </div>
</ListBox>
```

### 带懒加载的产品目录

```svelte
<script>
  import ListBox from '@ticatec/uniface-element/list-box';
  import ProductRenderer from './ProductRenderer.svelte';
  
  let selectedProducts = [];
  
  const loadProducts = async (searchText, pageNo) => {
    const params = new URLSearchParams({
      search: searchText || '',
      page: pageNo.toString(),
      limit: '20'
    });
    
    try {
      const response = await fetch(`/api/products?${params}`);
      const data = await response.json();
      
      return {
        list: data.products,
        hasMore: data.totalPages > pageNo
      };
    } catch (error) {
      console.error('加载产品失败:', error);
      return { list: [], hasMore: false };
    }
  };
</script>

<ListBox 
  lazyLoader={loadProducts}
  itemRender={ProductRenderer}
  selectMode="multiple"
  bind:selectedList={selectedProducts}
  style="width: 100%; height: 600px;"
>
  <div slot="header" style="padding: 12px; background: #2196f3; color: white;">
    <h3 style="margin: 0;">产品目录</h3>
  </div>
  
  <div slot="footer" style="padding: 8px; background: #f0f0f0; text-align: right;">
    已选择: {selectedProducts.length} 个产品
    <button 
      on:click={() => selectedProducts = []}
      disabled={selectedProducts.length === 0}
    >
      清除选择
    </button>
  </div>
  
  <div slot="loadMoreIndicator" style="padding: 16px; text-align: center;">
    <div class="loading-spinner"></div>
    正在加载更多产品...
  </div>
</ListBox>
```

### 带操作的联系人列表

```svelte
<script>
  import ListBox from '@ticatec/uniface-element/list-box';
  import ContactRenderer from './ContactRenderer.svelte';
  
  let contacts = [
    { id: 1, name: '张小明', email: 'zhangxm@example.com', phone: '+86-138-0001-0001' },
    { id: 2, name: '李小红', email: 'lixh@example.com', phone: '+86-138-0001-0002' }
  ];
  
  let selectedContact = null;
  
  function handleContactDblClick(contact) {
    // 打开联系人详情或编辑模态框
    console.log('打开联系人:', contact);
  }
  
  function handleContactCall(contact) {
    console.log('拨打电话:', contact.phone);
  }
  
  function handleContactEmail(contact) {
    window.location.href = `mailto:${contact.email}`;
  }
</script>

<ListBox 
  list={contacts}
  itemRender={ContactRenderer}
  selectMode="single"
  bind:selectedItem={selectedContact}
  onItemDblClick={handleContactDblClick}
  item$props={{ onCall: handleContactCall, onEmail: handleContactEmail }}
  title="联系人"
  style="width: 350px; height: 450px;"
>
  <div slot="footer" style="padding: 12px; display: flex; gap: 8px;">
    <button 
      disabled={!selectedContact}
      on:click={() => handleContactCall(selectedContact)}
    >
      拨打电话
    </button>
    <button 
      disabled={!selectedContact}
      on:click={() => handleContactEmail(selectedContact)}
    >
      发送邮件
    </button>
  </div>
</ListBox>
```

## 样式

### CSS 变量

ListBox组件使用CSS变量进行一致的主题化：

```css
:root {
  --uniface-listbox-separator-color: #f0f0f0;
  --uniface-list-box-hover-bg: #ffffe1;
  --uniface-list-box-selected-bg: #b1c8f6;
  --uniface-list-box-header-bottom-border: 1px solid #f0f0f0;
  --uniface-list-box-border-color: #f0f0f0;
  --uniface-list-box-header-bg: #f8fafc;
  --uniface-list-box-footer-bg: #f8fafc;
}
```

### 自定义样式

```css
.custom-listbox {
  --uniface-list-box-hover-bg: #e3f2fd;
  --uniface-list-box-selected-bg: #1976d2;
  --uniface-list-box-border-color: #1976d2;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.custom-listbox .listview-item {
  transition: all 0.2s ease;
}

.custom-listbox .box-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}
```

## 无障碍功能

- **键盘导航**: 方向键导航，回车键选择
- **屏幕阅读器支持**: 适当的ARIA标签和角色
- **焦点管理**: 可见的焦点指示器和逻辑tab顺序
- **选择通知**: 选择改变时的屏幕阅读器反馈
- **搜索无障碍**: 搜索功能的适当标签

## 性能考虑

### 大型列表

对于大型数据集，使用懒加载：

```svelte
<script>
  const lazyLoader = async (searchText, pageNo) => {
    // 实现高效的服务器端分页
    const response = await fetch(`/api/items?search=${searchText}&page=${pageNo}&limit=50`);
    return await response.json();
  };
</script>

<ListBox 
  lazyLoader={lazyLoader}
  itemRender={OptimizedItemRenderer}
  style="height: 400px;"
/>
```

### 虚拟滚动

对于极大的列表，可以在项目渲染器中实现虚拟滚动：

```svelte
<!-- VirtualizedItemRenderer.svelte -->
<script>
  export let item;
  export let index;
  
  // 在这里实现虚拟滚动逻辑
</script>
```

## 最佳实践

### 1. 优化项目渲染器

保持项目渲染器简单高效：

```svelte
<!-- 好的做法：简单高效 -->
<script>
  export let item;
</script>

<div class="item">
  <h4>{item.title}</h4>
  <p>{item.description}</p>
</div>

<!-- 避免：在渲染器中进行重计算 -->
<script>
  export let item;
  
  // 不要在这里进行重计算
  $: processedData = heavyComputation(item);
</script>
```

### 2. 实现适当的加载状态

```svelte
<ListBox 
  lazyLoader={dataLoader}
  itemRender={ItemRenderer}
>
  <div slot="loadMoreIndicator" class="loading-indicator">
    <div class="spinner"></div>
    正在加载更多项目...
  </div>
</ListBox>
```

### 3. 处理空状态

```svelte
<script>
  let items = [];
  let isLoading = false;
</script>

{#if isLoading}
  <div class="loading-state">正在加载...</div>
{:else if items.length === 0}
  <div class="empty-state">未找到项目</div>
{:else}
  <ListBox list={items} itemRender={ItemRenderer} />
{/if}
```

### 4. 提供清晰的选择反馈

```svelte
<ListBox 
  bind:selectedItem
  selectMode="single"
  style="--uniface-list-box-selected-bg: #2196f3; --uniface-list-box-hover-bg: #e3f2fd;"
/>

<div class="selection-info">
  {#if selectedItem}
    已选择: {selectedItem.name}
  {:else}
    未选择项目
  {/if}
</div>
```

## 组件优化建议

分析代码后，发现以下可以优化的地方：

### 1. 性能优化

```svelte
<!-- 当前实现中的问题 -->
<script>
  // 在每次过滤时都重新创建数组
  const doFilter = () => {
    const newList: Array<any> = [];
    list.forEach(item => {
      if (filter?.(item, filterText)) {
        newList.push(item)
      }
      filteredList = newList; // 这行代码位置有问题
    });
  }
</script>
```

建议优化为：

```svelte
<script>
  const doFilter = () => {
    if (!list || !filter) {
      filteredList = list || [];
      return;
    }
    
    filteredList = list.filter(item => filter(item, filterText));
    
    // 清除单选状态
    if (selectMode === 'single') {
      selectedItem = null;
      onSelectChange?.(null);
    }
  };
</script>
```

### 2. 键盘导航优化

当前缺少键盘导航支持，建议添加：

```svelte
<script>
  let focusedIndex = -1;
  
  function handleKeyDown(event) {
    if (!filteredList || filteredList.length === 0) return;
    
    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        focusedIndex = Math.min(focusedIndex + 1, filteredList.length - 1);
        break;
      case 'ArrowUp':
        event.preventDefault();
        focusedIndex = Math.max(focusedIndex - 1, 0);
        break;
      case 'Enter':
        if (focusedIndex >= 0 && selectMode === 'single') {
          selectedItem = filteredList[focusedIndex];
          onSelectChange?.(selectedItem);
        }
        break;
    }
  }
</script>

<div on:keydown={handleKeyDown} tabindex="0">
  <!-- 列表内容 -->
</div>
```

### 3. 无障碍访问优化

添加适当的ARIA属性：

```svelte
<div 
  class="listbox-content" 
  role="listbox"
  aria-multiselectable={selectMode === 'multiple'}
  bind:this={scrollElement} 
  on:scroll={handleScroll}
>
  {#each filteredList as item, index}
    <div 
      class="listview-item" 
      role="option"
      aria-selected={selectMode === 'single' ? item === selectedItem : selectedList.includes(item)}
      tabindex="-1"
    >
      <!-- 项目内容 -->
    </div>
  {/each}
</div>
```

## 浏览器支持

- **现代浏览器**: Chrome、Firefox、Safari、Edge（最新版本）
- **移动浏览器**: iOS Safari、Chrome Mobile、Firefox Mobile
- **Flexbox支持**: 布局功能必需
- **Intersection Observer**: 懒加载需要（可提供polyfill）

## 相关组件

- **SearchBox**: 内部用于过滤功能
- **CheckBox**: 用于多选模式
- **Box**: 基础布局组件

## 许可证

MIT许可证 - 详情请参阅LICENSE文件。