# Nav 导航

用于 App 主导航：纵向当侧栏、横向当底栏。激活项由一个可滑动的实心块标记，键盘方向键可以在项之间移动。与 `MTabs` 一样是纯 `items` 数据驱动，不通过插槽收集子组件。

## 代码演示

### 基础用法

`items` 决定有哪些项，`v-model` 决定哪一项激活。横向排列适合做底部标签栏。

```vue
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import { mdiHomeVariantOutline, mdiCompassOutline, mdiAccountOutline } from '@mdi/js'

const MIcon = resolveComponent('MIcon') as Component

/** items[].icon 需要的是一个组件，这里把 mdi 路径包成接收 size 的小组件 */
function icon(path: string) {
  return defineComponent({
    name: 'DemoNavIcon',
    props: { size: { type: [Number, String], default: 24 } },
    setup(props) {
      return () => h(MIcon, { path, size: props.size })
    },
  })
}

const active = ref('home')
const items = [
  { name: 'home', label: '首页', icon: icon(mdiHomeVariantOutline) },
  { name: 'explore', label: '发现', icon: icon(mdiCompassOutline) },
  { name: 'mine', label: '我的', icon: icon(mdiAccountOutline) },
]
</script>

<template>
  <MNav v-model="active" :items="items" direction="horizontal" size="small" />
</template>
```

### 纵向侧栏

`direction="vertical"` 为默认值，配合 `align` 可以控制整列在容器内的对齐位置。

**演示说明：** 左边 `align="middle"`、右边 `size="small"` + `align="top"`。

```vue
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import {
  mdiViewDashboardOutline,
  mdiChartTimelineVariant,
  mdiAccountGroupOutline,
  mdiCogOutline,
} from '@mdi/js'

const MIcon = resolveComponent('MIcon') as Component

function icon(path: string) {
  return defineComponent({
    name: 'DemoNavIcon',
    props: { size: { type: [Number, String], default: 24 } },
    setup(props) {
      return () => h(MIcon, { path, size: props.size })
    },
  })
}

const active = ref('dashboard')
const items = [
  { name: 'dashboard', label: '总览', icon: icon(mdiViewDashboardOutline) },
  { name: 'analysis', label: '分析', icon: icon(mdiChartTimelineVariant) },
  { name: 'members', label: '成员', icon: icon(mdiAccountGroupOutline) },
  { name: 'settings', label: '设置', icon: icon(mdiCogOutline) },
]
</script>

<template>
  <div class="m-row-demo">
    <MNav v-model="active" :items="items" direction="vertical" align="middle" />
    <MNav v-model="active" :items="items" direction="vertical" size="small" align="top" />
  </div>
</template>
```

### 徽标

`badge` 传数字显示在图标右上角，传 `true` 显示小红点；数字超过 `badgeMax` 时显示为 `{badgeMax}+`。

**演示说明：** 第二排把 `badgeMax` 调成 9，120 就变成了 9+。

```vue
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import {
  mdiInboxOutline,
  mdiBellOutline,
  mdiEmailOutline,
  mdiAlertCircleOutline,
} from '@mdi/js'

const MIcon = resolveComponent('MIcon') as Component

function icon(path: string) {
  return defineComponent({
    name: 'DemoNavIcon',
    props: { size: { type: [Number, String], default: 24 } },
    setup(props) {
      return () => h(MIcon, { path, size: props.size })
    },
  })
}

const active = ref('inbox')
const items = [
  { name: 'inbox', label: '收件箱', icon: icon(mdiInboxOutline), badge: 8 },
  { name: 'notice', label: '通知', icon: icon(mdiBellOutline), badge: true },
  { name: 'mail', label: '邮件', icon: icon(mdiEmailOutline), badge: 120 },
  { name: 'alert', label: '告警', icon: icon(mdiAlertCircleOutline) },
]
</script>

<template>
  <div class="m-row-demo--stretch">
    <MNav v-model="active" :items="items" direction="horizontal" :badge-max="99" />
    <MNav v-model="active" :items="items" direction="horizontal" size="small" :badge-max="9" />
  </div>
</template>
```

### 尺寸与对齐

`size` 影响图标大小、字号与项宽高；`align` 在横向下取 `left` / `center` / `right`。

```vue
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import { mdiHomeVariantOutline, mdiCompassOutline, mdiAccountOutline } from '@mdi/js'

const MIcon = resolveComponent('MIcon') as Component

function icon(path: string) {
  return defineComponent({
    name: 'DemoNavIcon',
    props: { size: { type: [Number, String], default: 24 } },
    setup(props) {
      return () => h(MIcon, { path, size: props.size })
    },
  })
}

const active = ref('home')
const items = [
  { name: 'home', label: '首页', icon: icon(mdiHomeVariantOutline) },
  { name: 'explore', label: '发现', icon: icon(mdiCompassOutline) },
  { name: 'mine', label: '我的', icon: icon(mdiAccountOutline) },
]
</script>

<template>
  <div class="m-row-demo--stretch">
    <MNav v-model="active" :items="items" direction="horizontal" size="small" align="left" />
    <MNav v-model="active" :items="items" direction="horizontal" size="default" align="center" />
    <MNav v-model="active" :items="items" direction="horizontal" size="large" align="right" />
  </div>
</template>
```

### 语义色

`color` 决定激活块的底色与 hover 底色。

```vue
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import { mdiHomeVariantOutline, mdiCompassOutline, mdiAccountOutline } from '@mdi/js'

const MIcon = resolveComponent('MIcon') as Component

function icon(path: string) {
  return defineComponent({
    name: 'DemoNavIcon',
    props: { size: { type: [Number, String], default: 24 } },
    setup(props) {
      return () => h(MIcon, { path, size: props.size })
    },
  })
}

const colors = ['primary', 'secondary', 'success', 'warning', 'error', 'info'] as const
const active = ref('home')
const items = [
  { name: 'home', label: '首页', icon: icon(mdiHomeVariantOutline) },
  { name: 'explore', label: '发现', icon: icon(mdiCompassOutline) },
  { name: 'mine', label: '我的', icon: icon(mdiAccountOutline) },
]
</script>

<template>
  <div class="m-row-demo--stretch">
    <MNav
      v-for="color in colors"
      :key="color"
      v-model="active"
      :items="items"
      direction="horizontal"
      size="small"
      align="left"
      :color="color" />
  </div>
</template>
```

### 禁用与受控激活

单项 `disabled` 只锁住那一项，顶层 `disabled` 锁住全部。激活值指向禁用项或不存在项时，组件会自动回退到第一个可用项并回写。

**演示说明：** 点按钮把 `active` 设成禁用项，状态会被组件改回来。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const active = ref<string | number>('home')
const items = [
  { name: 'home', label: '首页' },
  { name: 'locked', label: '受限项', disabled: true },
  { name: 'mine', label: '我的' },
]

/** 主动把激活项指到一个禁用项上，观察组件回退 */
function pickDisabled() {
  active.value = 'locked'
}
</script>

<template>
  <div class="m-row-demo--stretch">
    <MNav v-model="active" :items="items" direction="horizontal" size="small" align="left" />
    <MNav :items="items" direction="horizontal" size="small" align="left" disabled />
    <div class="m-row-demo">
      <MButton variant="outlined" color="primary" size="small" @click="pickDisabled">
        把 active 设为禁用项
      </MButton>
      <span>当前激活：{{ active }}</span>
    </div>
  </div>
</template>
```

## API

### Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string \| number` | `undefined` | 当前激活项的 `name`（`v-model`） |
| `items` | `NavItem[]` | `[]` | 导航项数据 |
| `direction` | `'vertical' \| 'horizontal'` | `'vertical'` | 排列方向：纵向侧栏 / 横向底栏 |
| `align` | `'top' \| 'middle' \| 'bottom' \| 'left' \| 'center' \| 'right'` | `'center'` | 沿主轴对齐；纵向取 `top` / `middle` / `bottom`，横向取 `left` / `center` / `right` |
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'primary'` | 主题色，决定激活实心块与 hover 底色 |
| `size` | `'small' \| 'default' \| 'large'` | `'default'` | 尺寸，影响图标大小（20 / 24 / 28）、字号与项宽高 |
| `disabled` | `boolean` | `false` | 是否禁用全部导航项 |
| `badgeMax` | `number` | `99` | 数字徽标上限，超过时显示为 `{badgeMax}+` |
| `ariaLabel` | `string` | `'导航'` | 绑定到 `nav` 元素的 `aria-label` |

### Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: string \| number` | 激活项变化，包含内部回退时的回写 |
| `change` | `name: string \| number` | 用户切换激活项（点击或键盘） |

### Slots

无。

### 类型定义

```ts
export interface NavItem {
  name: string | number;
  label: string;
  icon?: Component;
  badge?: string | number | boolean;
  disabled?: boolean;
}
```

## 使用建议

::: tip `items[].icon` 是组件而不是字符串
`icon` 的类型是 `Component`，组件会被渲染成 `<component :is="icon" :size="iconSize" />` 并接收当前尺寸。直接用 `MIcon` 需要先把路径绑定进去：

```ts
import { defineComponent, h, resolveComponent } from 'vue';
import { mdiHome } from '@mdi/js';

const MIcon = resolveComponent('MIcon');

const HomeIcon = defineComponent({
  props: { size: { type: [Number, String], default: 24 } },
  setup: (props) => () => h(MIcon, { path: mdiHome, size: props.size }),
});
```
:::

::: warning 激活值失效会被自动改写
`modelValue` 不存在于 `items` 中，或指向一个被禁用的项时，`watchEffect` 会把激活值改成第一个可用项，并通过 `update:modelValue` 回写。所以外部持有的激活变量会被组件改掉，别把它当成只读的展示值。
:::

::: warning 徽标的三种取值语义不同
`badge: true` 渲染为小红点（无文字）；数字或字符串渲染为文字徽标；`badge` 为 `null` / `false` 时不渲染。数字超过 `badgeMax` 会显示成 `{badgeMax}+`。
:::

::: tip 方向键随 `direction` 变化
纵向导航用 ↑ / ↓，横向导航用 ← / →，`Home` / `End` 跳到首尾，`Enter` / `Space` 确认。焦点在项之间走 roving tabindex，只有激活项是 `tabindex="0"`。
:::

## 相关组件

- [Tabs 标签页](/components/tabs)：同样是 `items` 驱动 + 共享 `v-model` 的标签栏，适合内容区切换。
- [Icon 图标](/components/icon)：导航项的图标来源。
