内容目录

GitHub
具有自动活动锚链接突出显示的粘性目录。
此组件仅在安装@nuxt/content模块时可用。

用法

使用links属性和page?.body?.toc?.links属性获取页面。

<script setup lang="ts">
import type { Collections } from '@nuxt/content'

const route = useRoute()
const { locale } = useI18n()

const contentPath = computed(() => {
  const path = route.path.replace(/\/$/, '')
  if (locale.value === 'en') return path
  return path.replace(`/${locale.value}`, '') || '/'
})

const { data: page } = await useAsyncData(
  `toc-${locale.value}-${contentPath.value}`,
  async () => {
    const collection = `docs_${locale.value}` as keyof Collections
    let content = await queryCollection(collection).path(contentPath.value).first()
    if (!content && locale.value !== 'en') {
      content = await queryCollection('docs_en').path(contentPath.value).first()
    }
    return content
  },
  { watch: [locale] }
)
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UContentToc :links="page?.body?.toc?.links" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Collections } from '@nuxt/content'

const route = useRoute()
const { locale } = useI18n()

const contentPath = computed(() => {
  const path = route.path.replace(/\/$/, '')
  if (locale.value === 'en') return path
  return path.replace(`/${locale.value}`, '') || '/'
})

const { data: page } = await useAsyncData(
  `toc-${locale.value}-${contentPath.value}`,
  async () => {
    const collection = `docs_${locale.value}` as keyof Collections
    let content = await queryCollection(collection).path(contentPath.value).first()
    if (!content && locale.value !== 'en') {
      content = await queryCollection('docs_en').path(contentPath.value).first()
    }
    return content
  },
  { watch: [locale] }
)
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UContentToc :links="page?.body?.toc?.links" />
</template>

标题

使用title属性更改目录的标题。

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc title="On this page" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc title="On this page" :links="links" />
</template>

Color

使用color属性更改链接的颜色。

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc color="neutral" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc color="neutral" :links="links" />
</template>

亮点

使用highlight属性显示活动项的高亮边框。

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight :links="links" />
</template>

高亮颜色

使用highlight-color属性来改变高亮的颜色。默认为color属性。

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="neutral" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="neutral" :links="links" />
</template>

突出显示变体:badge{label="4.6+" class="align-text-top"}

使用highlight-variant属性将highlight.xml的样式更改为straight。

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  },
  {
    id: 'examples',
    depth: 2,
    text: 'Examples',
    children: [
      {
        id: 'within-a-page',
        depth: 3,
        text: 'Within a Page'
      }
    ]
  },
  {
    id: 'api',
    depth: 2,
    text: 'API',
    children: [
      {
        id: 'props',
        depth: 3,
        text: 'Props'
      },
      {
        id: 'slots',
        depth: 3,
        text: 'Slots'
      },
      {
        id: 'emits',
        depth: 3,
        text: 'Emits'
      }
    ]
  },
  {
    id: 'theme',
    depth: 2,
    text: 'Theme'
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="primary" highlight-variant="circuit" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  },
  {
    id: 'examples',
    depth: 2,
    text: 'Examples',
    children: [
      {
        id: 'within-a-page',
        depth: 3,
        text: 'Within a Page'
      }
    ]
  },
  {
    id: 'api',
    depth: 2,
    text: 'API',
    children: [
      {
        id: 'props',
        depth: 3,
        text: 'Props'
      },
      {
        id: 'slots',
        depth: 3,
        text: 'Slots'
      },
      {
        id: 'emits',
        depth: 3,
        text: 'Emits'
      }
    ]
  },
  {
    id: 'theme',
    depth: 2,
    text: 'Theme'
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="primary" highlight-variant="circuit" :links="links" />
</template>

示例

页面内

在页面中使用ContentToc组件可显示目录:

pages/[...slug].vue
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UPage v-if="page">
    <UPageHeader :title="page.title" />

    <UPageBody>
      <ContentRenderer v-if="page.body" :value="page" />

      <USeparator v-if="surround?.filter(Boolean).length" />

      <UContentSurround :surround="(surround as any)" />
    </UPageBody>

    <template v-if="page?.body?.toc?.links?.length" #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>

API

Props

Prop Default Type
as
'nav'
any

The element or component this component should render as.

trailingIcon
appConfig.ui.icons.chevronDown
any

The icon displayed to collapse the content.

title
t('contentToc.title')
string

The title of the table of contents.

color
'primary'
"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
highlight
false
boolean

Display a line next to the active link.

highlightColor
'primary'
"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
highlightVariant
'straight'
"straight" | "circuit"

The variant of the highlight indicator.

links
T[]
defaultOpen
boolean

The open state of the collapsible when it is initially rendered.
Use when you do not need to control its open state.

open
boolean

The controlled open state of the collapsible. Can be binded with v-model.

ui
{ root?: SlotClass; container?: SlotClass; top?: SlotClass; bottom?: SlotClass; trigger?: SlotClass; title?: SlotClass; trailing?: SlotClass; trailingIcon?: SlotClass; content?: SlotClass; list?: SlotClass; listWithChildren?: SlotClass; item?: SlotClass; itemWithChildren?: SlotClass; link?: SlotClass; linkText?: SlotClass; indicator?: SlotClass; indicatorLine?: SlotClass; indicatorActive?: SlotClass; }

Slots

Slot Type
leading
{ open: boolean; ui: object; }
default
{ open: boolean; }
trailing
{ open: boolean; ui: object; }
content
{ links: T[]; }
link
{ link: T; }
top
{ links?: T[] | undefined; }
bottom
{ links?: T[] | undefined; }

Emits

Event Type
update:open
[value: boolean]
move
[id: string]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    contentToc: {
      slots: {
        root: 'sticky top-(--ui-header-height) z-10 bg-default/75 lg:bg-[initial] backdrop-blur-sm -mx-4 px-4 sm:px-6 sm:-mx-6 lg:ms-0 flex flex-col overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
        container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-default lg:border-0 flex flex-col lg:min-h-0',
        top: 'lg:shrink-0',
        bottom: 'hidden lg:flex lg:flex-col lg:shrink-0 gap-6',
        trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 rounded-sm outline-primary/25 focus-visible:outline-3 lg:shrink-0',
        title: 'truncate',
        trailing: 'ms-auto inline-flex gap-1.5 items-center',
        trailingIcon: 'size-5 transform transition-transform duration-200 ease-out motion-reduce:transition-none shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
        content: 'relative data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden focus:outline-none lg:min-h-[min(var(--list-height,8rem),8rem)] lg:overflow-y-auto lg:scrollbar-none',
        list: 'min-w-0',
        listWithChildren: 'ms-3',
        item: 'min-w-0',
        itemWithChildren: '',
        link: 'group relative text-sm flex items-center rounded-sm outline-primary/25 focus-visible:outline-3 py-1',
        linkText: 'truncate',
        indicator: '',
        indicatorLine: '',
        indicatorActive: ''
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        highlightColor: {
          primary: {
            indicatorActive: 'bg-primary'
          },
          secondary: {
            indicatorActive: 'bg-secondary'
          },
          success: {
            indicatorActive: 'bg-success'
          },
          info: {
            indicatorActive: 'bg-info'
          },
          warning: {
            indicatorActive: 'bg-warning'
          },
          error: {
            indicatorActive: 'bg-error'
          },
          neutral: {
            indicatorActive: 'bg-inverted'
          }
        },
        active: {
          false: {
            link: [
              'text-muted hover:text-default',
              'transition-colors'
            ]
          }
        },
        highlight: {
          true: ''
        },
        highlightVariant: {
          straight: '',
          circuit: ''
        },
        body: {
          true: {
            bottom: 'mt-6'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          active: true,
          class: {
            link: 'text-primary'
          }
        },
        {
          color: 'neutral',
          active: true,
          class: {
            link: 'text-highlighted'
          }
        },
        {
          highlight: true,
          highlightVariant: 'straight',
          class: {
            list: 'ms-2.5 ps-4 border-s border-default',
            item: '-ms-px',
            indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 ease-out motion-reduce:transition-none h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full',
            indicatorLine: 'hidden',
            indicatorActive: 'w-full h-full'
          }
        },
        {
          highlight: true,
          highlightVariant: 'circuit',
          class: {
            list: 'ps-6.5',
            item: '-ms-px',
            itemWithChildren: 'ps-px',
            indicator: 'absolute ms-2.5 start-0 top-0 rtl:-scale-x-100',
            indicatorLine: 'absolute inset-0 bg-(--ui-border)',
            indicatorActive: 'absolute w-full h-(--indicator-size) translate-y-(--indicator-position) transition-[translate,height] duration-200 ease-out motion-reduce:transition-none'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        highlightColor: 'primary',
        highlightVariant: 'straight'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        contentToc: {
          slots: {
            root: 'sticky top-(--ui-header-height) z-10 bg-default/75 lg:bg-[initial] backdrop-blur-sm -mx-4 px-4 sm:px-6 sm:-mx-6 lg:ms-0 flex flex-col overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
            container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-default lg:border-0 flex flex-col lg:min-h-0',
            top: 'lg:shrink-0',
            bottom: 'hidden lg:flex lg:flex-col lg:shrink-0 gap-6',
            trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 rounded-sm outline-primary/25 focus-visible:outline-3 lg:shrink-0',
            title: 'truncate',
            trailing: 'ms-auto inline-flex gap-1.5 items-center',
            trailingIcon: 'size-5 transform transition-transform duration-200 ease-out motion-reduce:transition-none shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
            content: 'relative data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden focus:outline-none lg:min-h-[min(var(--list-height,8rem),8rem)] lg:overflow-y-auto lg:scrollbar-none',
            list: 'min-w-0',
            listWithChildren: 'ms-3',
            item: 'min-w-0',
            itemWithChildren: '',
            link: 'group relative text-sm flex items-center rounded-sm outline-primary/25 focus-visible:outline-3 py-1',
            linkText: 'truncate',
            indicator: '',
            indicatorLine: '',
            indicatorActive: ''
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            highlightColor: {
              primary: {
                indicatorActive: 'bg-primary'
              },
              secondary: {
                indicatorActive: 'bg-secondary'
              },
              success: {
                indicatorActive: 'bg-success'
              },
              info: {
                indicatorActive: 'bg-info'
              },
              warning: {
                indicatorActive: 'bg-warning'
              },
              error: {
                indicatorActive: 'bg-error'
              },
              neutral: {
                indicatorActive: 'bg-inverted'
              }
            },
            active: {
              false: {
                link: [
                  'text-muted hover:text-default',
                  'transition-colors'
                ]
              }
            },
            highlight: {
              true: ''
            },
            highlightVariant: {
              straight: '',
              circuit: ''
            },
            body: {
              true: {
                bottom: 'mt-6'
              }
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              active: true,
              class: {
                link: 'text-primary'
              }
            },
            {
              color: 'neutral',
              active: true,
              class: {
                link: 'text-highlighted'
              }
            },
            {
              highlight: true,
              highlightVariant: 'straight',
              class: {
                list: 'ms-2.5 ps-4 border-s border-default',
                item: '-ms-px',
                indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 ease-out motion-reduce:transition-none h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full',
                indicatorLine: 'hidden',
                indicatorActive: 'w-full h-full'
              }
            },
            {
              highlight: true,
              highlightVariant: 'circuit',
              class: {
                list: 'ps-6.5',
                item: '-ms-px',
                itemWithChildren: 'ps-px',
                indicator: 'absolute ms-2.5 start-0 top-0 rtl:-scale-x-100',
                indicatorLine: 'absolute inset-0 bg-(--ui-border)',
                indicatorActive: 'absolute w-full h-(--indicator-size) translate-y-(--indicator-position) transition-[translate,height] duration-200 ease-out motion-reduce:transition-none'
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            highlightColor: 'primary',
            highlightVariant: 'straight'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog

No recent changes