# Dropdown 下拉菜单

下拉菜单把一组次要操作收进一个触发区，避免主界面被按钮塞满。MDropdown 负责定位、开合与键盘导航，MDropdownItem 负责单项的语义（普通项、分隔线、禁用项）；两者靠 `#menu` 插槽里的 provide / inject 上下文联动，所以子项点击后菜单会自己收起来。

## 代码演示

### 基础菜单

触发内容放在 `#trigger` 插槽，菜单项放在 `#menu` 插槽，默认点击展开。

```vue
<script setup lang="ts">
import { mdiChevronDown } from '@mdi/js'
</script>

<template>
  <div class="m-row-demo">
    <MDropdown>
      <template #trigger>
        <MButton variant="outlined">
          更多操作
          <MIcon :path="mdiChevronDown" :size="16" />
        </MButton>
      </template>
      <template #menu>
        <MDropdownItem command="edit">编辑</MDropdownItem>
        <MDropdownItem command="duplicate">创建副本</MDropdownItem>
        <MDropdownItem command="move">移动到…</MDropdownItem>
      </template>
    </MDropdown>
  </div>
</template>
```

### 点击命令

每个菜单项用 `command` 携带一个业务标识，点击后父组件抛出 `command` 事件，这里用 `toast.info()` 把收到的值显示出来。

```vue
<script setup lang="ts">
import { toast } from 'miao-design'

function onCommand(value: unknown) {
  toast.info(`收到 command：${String(value)}`)
}
</script>

<template>
  <div class="m-row-demo">
    <MDropdown @command="onCommand">
      <template #trigger>
        <MButton variant="contained" color="primary">执行操作</MButton>
      </template>
      <template #menu>
        <MDropdownItem command="publish">发布</MDropdownItem>
        <MDropdownItem command="schedule">定时发布</MDropdownItem>
        <MDropdownItem divider />
        <MDropdownItem command="delete">删除</MDropdownItem>
        <MDropdownItem command="archive" disabled>归档（无权限）</MDropdownItem>
      </template>
    </MDropdown>
  </div>
</template>
```

### 悬停触发

`trigger="hover"` 时鼠标移入即展开，移出 150ms 后收起；键盘聚焦触发区同样能打开，保证可访问性。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { mdiChevronDown } from '@mdi/js'

const last = ref('未选择')
</script>

<template>
  <div class="m-row-demo">
    <MDropdown trigger="hover" placement="bottom-start" @command="last = String($event)">
      <template #trigger>
        <MButton variant="text">
          悬停展开
          <MIcon :path="mdiChevronDown" :size="16" />
        </MButton>
      </template>
      <template #menu>
        <MDropdownItem command="overview">概览</MDropdownItem>
        <MDropdownItem command="members">成员</MDropdownItem>
        <MDropdownItem command="settings">设置</MDropdownItem>
      </template>
    </MDropdown>

    <span style="color: var(--m-text-sub)">最近选择：{{ last }}</span>
  </div>
</template>
```

### 选中态、分割线与禁用项

组件没有内置的 `selected` 属性，选中态由业务自己用 `command` 记录并用图标 / 颜色表达；分割线用 `divider`，禁用项点击既不抛事件也不收起菜单。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { mdiCheck } from '@mdi/js'

const selected = ref('list')
const views = [
  { value: 'list', label: '列表视图' },
  { value: 'board', label: '看板视图' },
  { value: 'timeline', label: '时间线视图' }
]
</script>

<template>
  <div class="m-row-demo">
    <MDropdown width="200px" @command="selected = String($event)">
      <template #trigger>
        <MButton variant="outlined">切换视图</MButton>
      </template>
      <template #menu>
        <MDropdownItem v-for="view in views" :key="view.value" :command="view.value">
          <span
            :style="{
              display: 'flex',
              alignItems: 'center',
              gap: '8px',
              flex: 1,
              color: selected === view.value ? 'var(--m-primary)' : undefined
            }">
            <MIcon
              :path="mdiCheck"
              :size="16"
              :style="{ visibility: selected === view.value ? 'visible' : 'hidden' }" />
            {{ view.label }}
          </span>
        </MDropdownItem>

        <MDropdownItem divider />

        <MDropdownItem command="refresh">刷新数据</MDropdownItem>
        <MDropdownItem command="export" disabled>导出（需要升级套餐）</MDropdownItem>
      </template>
    </MDropdown>
  </div>
</template>
```

### 弹出位置

`placement` 支持上下各三个水平对齐位；某侧空间不足时会自动翻转到对侧，水平对齐位保持不变。

**演示说明：** 上方空间不够时面板会自动翻到下方，可以拖动页面把按钮推到视口边缘验证。

```vue
<script setup lang="ts">
const placements = [
  'top-start',
  'top-center',
  'top-end',
  'bottom-start',
  'bottom-center',
  'bottom-end'
] as const
</script>

<template>
  <div class="m-row-demo">
    <MDropdown v-for="placement in placements" :key="placement" :placement="placement">
      <template #trigger>
        <MButton variant="outlined" size="small">{{ placement }}</MButton>
      </template>
      <template #menu>
        <MDropdownItem :command="placement">{{ placement }}</MDropdownItem>
        <MDropdownItem command="second">第二项</MDropdownItem>
        <MDropdownItem command="third">第三项</MDropdownItem>
      </template>
    </MDropdown>
  </div>
</template>
```

### 自定义面板

`#menu` 插槽接受任意节点，不只限于 `MDropdownItem`；面板宽度、圆角与间距分别由 `width`、`panelRadius`、`offset` 控制。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { mdiContentCopy, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'

const last = ref('未操作')
</script>

<template>
  <div class="m-row-demo">
    <MDropdown width="240px" panel-radius="12px" :offset="10" @command="last = String($event)">
      <template #trigger>
        <MButton variant="contained" color="secondary">自定义面板</MButton>
      </template>
      <template #menu>
        <div style="padding: 4px 16px 10px; font-size: 12px; color: var(--m-text-sub)">
          最近的操作
        </div>
        <MDropdownItem command="rename">
          <MIcon :path="mdiPencilOutline" :size="18" />
          重命名
        </MDropdownItem>
        <MDropdownItem command="duplicate">
          <MIcon :path="mdiContentCopy" :size="18" />
          创建副本
        </MDropdownItem>
        <MDropdownItem divider />
        <MDropdownItem command="trash" disabled>
          <MIcon :path="mdiTrashCanOutline" :size="18" />
          移入回收站
        </MDropdownItem>
      </template>
    </MDropdown>

    <span style="color: var(--m-text-sub)">最近选择：{{ last }}</span>
  </div>
</template>
```

## API

### MDropdown Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `placement` | `'top-start' \| 'top-center' \| 'top-end' \| 'bottom-start' \| 'bottom-center' \| 'bottom-end'` | `'bottom-start'` | 弹出位置；空间不足时自动翻转到对侧 |
| `trigger` | `'click' \| 'hover'` | `'click'` | 触发方式：点击或悬停 |
| `disabled` | `boolean` | `false` | 是否禁用（禁用后触发区 `pointer-events: none`、透明度 0.38） |
| `width` | `string` | `undefined` | 菜单宽度（任意 CSS 长度）；不传时 `min-width` 取触发区宽度，内容可继续撑开 |
| `offset` | `number` | `4` | 菜单与触发区之间的间距（px） |
| `panelRadius` | `string` | `undefined` | 菜单面板圆角（任意 CSS 长度，如 `'8px'`）；缺省回退 4px |
| `appendToBody` | `boolean` | `true` | 是否 Teleport 到 body，避免被父级 `overflow` / `z-index` 裁剪 |

### MDropdown Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `command` | `value: unknown` | 任一 `MDropdownItem` 被点击时抛出其 `command` 值 |
| `open` | 无 | 菜单展开 |
| `close` | 无 | 菜单收起 |

### MDropdown Slots

| 插槽 | 参数 | 说明 |
| --- | --- | --- |
| `trigger` | 无 | 触发内容；未提供时回退到默认插槽 |
| `default` | 无 | 作为 `trigger` 的兜底内容 |
| `menu` | 无 | 菜单面板内容，放置 `MDropdownItem` |

### MDropdownItem Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `command` | `unknown` | `undefined` | 点击该项时随 `command` 事件抛出的值 |
| `disabled` | `boolean` | `false` | 是否禁用；禁用时点击不抛事件、不收起菜单 |
| `divider` | `boolean` | `false` | 渲染为分隔线（`role="separator"`），此时不渲染默认插槽内容 |

### MDropdownItem Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `command` | `value: unknown` | 点击该项时抛出自身 `command`；同时在 `MDropdown` 上下文里触发父级 `command` 并收起菜单 |

### MDropdownItem Slots

| 插槽 | 参数 | 说明 |
| --- | --- | --- |
| `default` | 无 | 菜单项内容；`divider` 为 `true` 时不渲染 |

### 类型

```ts
export type DropdownPlacement =
  | 'top-start' | 'top-center' | 'top-end'
  | 'bottom-start' | 'bottom-center' | 'bottom-end'

export type DropdownTrigger = 'click' | 'hover'
```

## 使用建议

::: tip 父子联动是怎么发生的
`MDropdown` 通过 `provide` 向 `#menu` 插槽内的后代注入了两个回调，`MDropdownItem` 用 `inject` 取到它们。一次点击依次发生三件事：

1. `MDropdownItem` 抛出自己的 `command` 事件；
2. 通过上下文调用父级的 `onItemClick`，父级据此抛出 `command` 事件；
3. 通过上下文调用父级的 `close`，菜单自动收起。

所以业务只需要在 `MDropdown` 上监听一次 `command`，用 `value` 做分支即可，不需要在每个菜单项上重复绑定点击处理。
:::

::: warning 菜单项必须放进 `#menu` 插槽
触发框内的内容会被当作触发区，而只有 `#menu` 插槽里的 `MDropdownItem` 才能注入到父级上下文。把菜单项写在默认插槽里，它会变成触发内容的一部分，既不会渲染成菜单，也不会自动收起。
:::

::: warning 脱离父组件的子项不会收起菜单
单独使用 `MDropdownItem`（没有外层的 `MDropdown`）时，注入的上下文为空：点击只保留涟漪反馈并抛出自身的 `command` 事件，不会有任何"收起"行为。若在别处复用它，需要自己处理关闭逻辑。

另外，菜单展开时会锁定页面滚动、监听外部 `pointerdown` 自动收起；面板高度固定在 320px 以内并在内容层内部滚动，超长菜单不会把页面撑高。
:::

## 相关组件

- [Select 选择器](/components/select)：同样是弹层 + 列表，但 `MSelect` 负责维护选中值与输入框形态。
- [Dialog 对话框](/components/dialog)：需要遮罩、必须打断流程时用对话框而非菜单。
- [Icon 图标](/components/icon)：菜单项左侧的图标通常来自 `@mdi/js`。
- [Toast 轻提示](/components/toast)：菜单里执行完命令后给一个轻量反馈。
