# Drawer 抽屉组件

基于 Svelte 构建的滑出面板组件，提供具有可配置定位和动画的平滑侧边导航和内容显示。

## 特性

- **左/右定位**：可配置的抽屉位置（左侧或右侧）
- **平滑动画**：具有可自定义持续时间的飞入过渡效果
- **遮罩背景**：半透明背景，支持点击关闭
- **可自定义宽度**：可调整的抽屉宽度
- **自动隐藏管理**：智能的可见性状态管理
- **事件处理**：点击外部关闭功能
- **响应式设计**：适应不同屏幕尺寸
- **无障碍访问支持**：适当的 ARIA 属性
- **CSS 自定义**：可使用 CSS 变量进行主题化
- **轻量级**：最小的性能影响

## 安装

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

```typescript
import Drawer from '@ticatec/uniface-element/Drawer';
```

或者在同一项目中使用：
```typescript
import Drawer from '$lib/drawer';
```

## 基本用法

```svelte
<script>
  import Drawer from '@ticatec/uniface-element/Drawer';
  import Button from '@ticatec/uniface-elemen/Button';
  
  let showDrawer = false;
  
  function openDrawer() {
    showDrawer = true;
  }
  
  function closeDrawer() {
    showDrawer = false;
  }
</script>

<Button label="打开抽屉" onClick={openDrawer} />

<Drawer bind:visible={showDrawer}>
  <div style="padding: 1rem;">
    <h2>抽屉内容</h2>
    <p>这是抽屉内容区域。</p>
    <Button label="关闭" onClick={closeDrawer} />
  </div>
</Drawer>
```

## 属性

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `visible` | `boolean` | `false` | 控制抽屉可见性 |
| `width` | `number` | `300` | 抽屉宽度（像素） |
| `position` | `'left' \| 'right'` | `'left'` | 抽屉滑入位置 |

## 示例

### 基本左侧抽屉

```svelte
<script>
  import Drawer from '@ticatec/uniface-element/Drawer';
  
  let leftDrawerVisible = false;
</script>

<button on:click={() => leftDrawerVisible = true}>
  打开左侧抽屉
</button>

<Drawer bind:visible={leftDrawerVisible}>
  <div class="drawer-content">
    <h3>导航菜单</h3>
    <ul>
      <li><a href="/home">首页</a></li>
      <li><a href="/about">关于</a></li>
      <li><a href="/contact">联系</a></li>
    </ul>
  </div>
</Drawer>
```

### 右侧抽屉

```svelte
<script>
  import Drawer from '@ticatec/uniface-element/Drawer';
  
  let rightDrawerVisible = false;
</script>

<button on:click={() => rightDrawerVisible = true}>
  打开右侧抽屉
</button>

<Drawer bind:visible={rightDrawerVisible} position="right">
  <div class="drawer-content">
    <h3>设置面板</h3>
    <div class="setting-item">
      <label>
        <input type="checkbox" /> 启用通知
      </label>
    </div>
    <div class="setting-item">
      <label>
        <input type="checkbox" /> 深色模式
      </label>
    </div>
  </div>
</Drawer>
```

### 自定义宽度抽屉

```svelte
<script>
  import Drawer from '@ticatec/uniface-element/Drawer';
  
  let wideDrawerVisible = false;
</script>

<button on:click={() => wideDrawerVisible = true}>
  打开宽抽屉
</button>

<Drawer bind:visible={wideDrawerVisible} width={400}>
  <div class="drawer-content">
    <h3>宽内容面板</h3>
    <p>这个抽屉更宽，可以容纳更多内容。</p>
    <div class="content-grid">
      <div class="card">卡片 1</div>
      <div class="card">卡片 2</div>
      <div class="card">卡片 3</div>
    </div>
  </div>
</Drawer>
```

### 移动端导航抽屉

```svelte
<script>
  import Drawer from '@ticatec/uniface-element/Drawer';
  import Icon from '@ticatec/uniface-element/Icon';
  
  let mobileMenuVisible = false;
  
  const menuItems = [
    { label: '仪表板', icon: 'dashboard', href: '/dashboard' },
    { label: '项目', icon: 'folder', href: '/projects' },
    { label: '团队', icon: 'people', href: '/team' },
    { label: '设置', icon: 'settings', href: '/settings' }
  ];
  
  function closeMenu() {
    mobileMenuVisible = false;
  }
</script>

<!-- 带菜单按钮的移动端头部 -->
<header class="mobile-header">
  <button class="menu-button" on:click={() => mobileMenuVisible = true}>
    <Icon name="menu" />
  </button>
  <h1>我的应用</h1>
</header>

<!-- 移动端导航抽屉 -->
<Drawer bind:visible={mobileMenuVisible} width={280}>
  <nav class="mobile-nav">
    <div class="nav-header">
      <h2>菜单</h2>
      <button class="close-button" on:click={closeMenu}>
        <Icon name="close" />
      </button>
    </div>
    
    <ul class="nav-menu">
      {#each menuItems as item}
        <li>
          <a href={item.href} on:click={closeMenu}>
            <Icon name={item.icon} />
            <span>{item.label}</span>
          </a>
        </li>
      {/each}
    </ul>
    
    <div class="nav-footer">
      <div class="user-info">
        <Icon name="account_circle" />
        <span>张三</span>
      </div>
    </div>
  </nav>
</Drawer>

<style>
  .mobile-header {
    display: flex;
    align-items: center;
    padding: 1rem;
    background: #fff;
    border-bottom: 1px solid #e1e1e1;
  }
  
  .menu-button {
    background: none;
    border: none;
    margin-right: 1rem;
    cursor: pointer;
  }
  
  .mobile-nav {
    height: 100%;
    display: flex;
    flex-direction: column;
  }
  
  .nav-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border-bottom: 1px solid #e1e1e1;
  }
  
  .nav-menu {
    flex: 1;
    list-style: none;
    padding: 0;
    margin: 0;
  }
  
  .nav-menu a {
    display: flex;
    align-items: center;
    padding: 1rem;
    text-decoration: none;
    color: #333;
    border-bottom: 1px solid #f0f0f0;
  }
  
  .nav-menu a:hover {
    background: #f5f5f5;
  }
  
  .nav-footer {
    padding: 1rem;
    border-top: 1px solid #e1e1e1;
  }
  
  .user-info {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }
</style>
```

### 筛选/搜索抽屉

```svelte
<script>
  import Drawer from '$lib/drawer';
  import TextEditor from '$lib/text-editor';
  import Checkbox from '$lib/checkbox';
  import Button from '$lib/button';
  
  let filterDrawerVisible = false;
  let filters = {
    searchTerm: '',
    categories: [],
    priceRange: { min: 0, max: 1000 },
    inStock: false
  };
  
  const categories = ['电子产品', '服装', '图书', '家居园艺'];
  
  function applyFilters() {
    console.log('应用筛选：', filters);
    filterDrawerVisible = false;
  }
  
  function resetFilters() {
    filters = {
      searchTerm: '',
      categories: [],
      priceRange: { min: 0, max: 1000 },
      inStock: false
    };
  }
</script>

<button on:click={() => filterDrawerVisible = true}>
  显示筛选
</button>

<Drawer bind:visible={filterDrawerVisible} position="right" width={350}>
  <div class="filter-panel">
    <div class="filter-header">
      <h3>筛选</h3>
      <button class="reset-button" on:click={resetFilters}>
        重置全部
      </button>
    </div>
    
    <div class="filter-section">
      <label>搜索：</label>
      <TextEditor bind:value={filters.searchTerm} placeholder="搜索产品..." />
    </div>
    
    <div class="filter-section">
      <label>分类：</label>
      {#each categories as category}
        <Checkbox 
          label={category}
          checked={filters.categories.includes(category)}
          onchange={(checked) => {
            if (checked) {
              filters.categories = [...filters.categories, category];
            } else {
              filters.categories = filters.categories.filter(c => c !== category);
            }
          }}
        />
      {/each}
    </div>
    
    <div class="filter-section">
      <label>价格范围：</label>
      <div class="price-inputs">
        <input type="number" bind:value={filters.priceRange.min} min="0" />
        <span>到</span>
        <input type="number" bind:value={filters.priceRange.max} min="0" />
      </div>
    </div>
    
    <div class="filter-section">
      <Checkbox 
        label="仅显示有库存"
        bind:checked={filters.inStock}
      />
    </div>
    
    <div class="filter-actions">
      <Button label="应用筛选" type="primary" onClick={applyFilters} />
    </div>
  </div>
</Drawer>

<style>
  .filter-panel {
    padding: 1rem;
    height: 100%;
    display: flex;
    flex-direction: column;
  }
  
  .filter-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #e1e1e1;
  }
  
  .filter-section {
    margin-bottom: 1.5rem;
  }
  
  .filter-section label {
    display: block;
    font-weight: 600;
    margin-bottom: 0.5rem;
  }
  
  .price-inputs {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }
  
  .price-inputs input {
    width: 80px;
    padding: 0.25rem;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
  
  .filter-actions {
    margin-top: auto;
    padding-top: 1rem;
    border-top: 1px solid #e1e1e1;
  }
  
  .reset-button {
    background: none;
    border: none;
    color: #666;
    cursor: pointer;
    text-decoration: underline;
  }
</style>
```

## 动画和过渡

抽屉使用 Svelte 的 `fly` 过渡，具有以下行为：

- **滑动方向**：基于位置（左侧从左滑入，右侧从右滑入）
- **持续时间**：默认 1000ms（1 秒）
- **不透明度**：从 0.5 开始淡入到 1.0
- **缓动**：线性过渡

### 自定义动画

虽然组件没有直接暴露动画属性，但您可以通过编辑组件或使用 CSS 过渡来修改过渡效果：

```css
.uniface-drawer > div {
  transition: transform 0.3s ease-out;
}
```

## 交互模式

### 点击外部关闭

抽屉在点击遮罩背景时自动关闭：

```svelte
<!-- 此行为是内置的 -->
<Drawer bind:visible={showDrawer}>
  <!-- 点击此内容区域外会关闭抽屉 -->
</Drawer>
```

### 防止内容点击关闭

内容点击会自动阻止冒泡到遮罩层：

```svelte
<Drawer bind:visible={showDrawer}>
  <!-- 在这里点击不会关闭抽屉 -->
  <div>
    <button>这不会关闭抽屉</button>
  </div>
</Drawer>
```

### 手动关闭控制

```svelte
<script>
  import Drawer from '$lib/drawer';
  
  let drawerVisible = false;
  
  function closeDrawer() {
    drawerVisible = false;
  }
  
  // Escape 键关闭
  function handleKeydown(event) {
    if (event.key === 'Escape' && drawerVisible) {
      closeDrawer();
    }
  }
</script>

<svelte:window on:keydown={handleKeydown} />

<Drawer bind:visible={drawerVisible}>
  <div class="drawer-content">
    <button class="close-button" on:click={closeDrawer}>
      ✕ 关闭
    </button>
    <!-- 内容 -->
  </div>
</Drawer>
```

## 样式定制

### CSS 变量

可以使用 CSS 变量自定义抽屉：

```css
:root {
  --uniface-drawer-bg: #ffffff;
  --uniface-drawer-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px, rgba(0, 0, 0, 0.23) 0 3px 6px;
  --uniface-drawer-border-color: #e1e1e1;
}
```

### 自定义样式

```css
/* 深色主题抽屉 */
.dark-drawer {
  --uniface-drawer-bg: #2a2a2a;
  --uniface-drawer-border-color: #444;
  --uniface-drawer-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  color: white;
}

/* 圆角抽屉 */
.rounded-drawer .uniface-drawer > div {
  border-radius: 0 12px 12px 0;
}

.rounded-drawer .uniface-drawer > div.right {
  border-radius: 12px 0 0 12px;
}

/* 自定义遮罩 */
.custom-overlay .uniface-drawer {
  background-color: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
}
```

### 响应式设计

```scss
.drawer-content {
  padding: 1rem;
  
  @media (max-width: 768px) {
    padding: 0.5rem;
  }
}

/* 移动端全宽抽屉 */
@media (max-width: 480px) {
  .uniface-drawer > div {
    width: 100% !important;
  }
}
```

## 无障碍访问

- **ARIA 属性**：抽屉包含适当的 `aria-hidden` 属性
- **焦点管理**：考虑添加焦点陷阱以提高无障碍访问
- **键盘支持**：实现 Escape 键处理以关闭
- **屏幕阅读器支持**：添加描述性标签和角色

### 增强无障碍访问示例

```svelte
<script>
  import Drawer from '$lib/drawer';
  
  let drawerVisible = false;
  let drawerContent;
  
  function openDrawer() {
    drawerVisible = true;
    // 打开时聚焦第一个可交互元素
    setTimeout(() => {
      const firstButton = drawerContent?.querySelector('button, a, input, select, textarea');
      firstButton?.focus();
    }, 100);
  }
  
  function handleKeydown(event) {
    if (event.key === 'Escape' && drawerVisible) {
      drawerVisible = false;
    }
  }
</script>

<svelte:window on:keydown={handleKeydown} />

<button on:click={openDrawer} aria-label="打开导航菜单">
  打开菜单
</button>

<Drawer bind:visible={drawerVisible}>
  <nav bind:this={drawerContent} aria-label="主导航" role="navigation">
    <h2 id="drawer-title">导航菜单</h2>
    <!-- 带有适当焦点管理的内容 -->
  </nav>
</Drawer>
```

## 最佳实践

### 1. 内容组织
- 保持抽屉内容专注和有组织
- 使用清晰的标题和章节
- 将内容限制为基本项目

### 2. 宽度指南
- **移动端**：使用屏幕宽度的 80-90% 或全宽
- **平板**：通常 300-400px 合适
- **桌面端**：导航使用 250-350px，详细内容最多 500px

### 3. 性能
- 避免在频繁打开的抽屉中放置重内容
- 对复杂的抽屉内容使用懒加载
- 对长列表考虑虚拟化

### 4. 用户体验
- 提供清晰的关闭按钮或说明
- 在应用中使用一致的定位
- 确保移动设备上的触摸友好目标

## 常见用例

### 导航抽屉
```svelte
<!-- 移动优先导航 -->
<Drawer bind:visible={navOpen} width={280}>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/products">产品</a></li>
      <li><a href="/contact">联系</a></li>
    </ul>
  </nav>
</Drawer>
```

### 设置面板
```svelte
<!-- 右侧设置 -->
<Drawer bind:visible={settingsOpen} position="right" width={400}>
  <div class="settings-panel">
    <!-- 设置表单内容 -->
  </div>
</Drawer>
```

### 筛选/搜索面板
```svelte
<!-- 筛选控件 -->
<Drawer bind:visible={filtersOpen} position="right" width={350}>
  <div class="filters">
    <!-- 筛选控件 -->
  </div>
</Drawer>
```

### 购物车
```svelte
<!-- 购物车抽屉 -->
<Drawer bind:visible={cartOpen} position="right" width={400}>
  <div class="cart">
    <!-- 购物车项目和结账 -->
  </div>
</Drawer>
```

## 注意事项

- 抽屉使用固定定位，显示在所有其他内容之上
- 遮罩背景的 z-index 为 100
- 如果抽屉内容超过视口高度，会自动滚动
- 抽屉自动处理显示/隐藏状态管理
- 点击外部行为是内置的，无法禁用