Admin 元件
Side Menu
後台側邊導覽。展開/收合、分組、巢狀子項目、行動裝置抽屜。
安裝
Terminal
pnpm dlx shadcn-vue add @admin/side-menuimport { AdminSideMenu, AdminSideMenuContent, AdminSideMenuFooter, AdminSideMenuGroup, AdminSideMenuGroupAction, AdminSideMenuGroupContent, AdminSideMenuGroupLabel, AdminSideMenuHeader, AdminSideMenuInput, AdminSideMenuInset, AdminSideMenuList, AdminSideMenuListAction, AdminSideMenuListBadge, AdminSideMenuListButton, AdminSideMenuListButtonChild, AdminSideMenuListItem, AdminSideMenuListSkeleton, AdminSideMenuListSub, AdminSideMenuListSubButton, AdminSideMenuListSubItem, AdminSideMenuProvider, AdminSideMenuRail, AdminSideMenuSeparator, AdminSideMenuTrigger, useSideMenu,} from '@/components/admin/side-menu'用法
外層需先以 admin-side-menu-provider 提供 context,再以 admin-side-menu 結合 group / list / item / button 組合導覽結構。
<script setup lang="ts">import { IconChevronDown, IconChevronRight, IconComponents, IconLayoutDashboard, IconSettings} from '@tabler/icons-vue'import { ref } from 'vue'import { AdminSideMenu, AdminSideMenuContent, AdminSideMenuGroup, AdminSideMenuList, AdminSideMenuListButton, AdminSideMenuListItem, AdminSideMenuListSub, AdminSideMenuListSubButton, AdminSideMenuListSubItem, AdminSideMenuProvider} from '@/components/admin/side-menu'const componentsExpanded = ref(true)</script><template> <admin-side-menu-provider class="block h-72 w-60"> <admin-side-menu collapsible="none"> <admin-side-menu-content> <admin-side-menu-group> <admin-side-menu-list> <admin-side-menu-list-item> <admin-side-menu-list-button> <icon-layout-dashboard class="size-4 shrink-0" /> Dashboard </admin-side-menu-list-button> </admin-side-menu-list-item> <admin-side-menu-list-item> <admin-side-menu-list-button is-active @click="componentsExpanded = !componentsExpanded" > <icon-components class="size-4 shrink-0" /> <span class="flex-1">Components</span> <icon-chevron-down v-if="componentsExpanded" class="size-4 shrink-0" /> <icon-chevron-right v-else class="size-4 shrink-0" /> </admin-side-menu-list-button> <admin-side-menu-list-sub v-show="componentsExpanded"> <admin-side-menu-list-sub-item> <admin-side-menu-list-sub-button is-active >Button</admin-side-menu-list-sub-button > </admin-side-menu-list-sub-item> <admin-side-menu-list-sub-item> <admin-side-menu-list-sub-button>Input</admin-side-menu-list-sub-button> </admin-side-menu-list-sub-item> <admin-side-menu-list-sub-item> <admin-side-menu-list-sub-button>Dialog</admin-side-menu-list-sub-button> </admin-side-menu-list-sub-item> </admin-side-menu-list-sub> </admin-side-menu-list-item> <admin-side-menu-list-item> <admin-side-menu-list-button> <icon-settings class="size-4 shrink-0" /> Settings </admin-side-menu-list-button> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group> </admin-side-menu-content> </admin-side-menu> </admin-side-menu-provider></template>場景
基本結構
<script setup lang="ts">import { IconComponents, IconLayoutDashboard, IconSettings } from '@tabler/icons-vue'import { AdminSideMenu, AdminSideMenuContent, AdminSideMenuGroup, AdminSideMenuList, AdminSideMenuListButton, AdminSideMenuListItem, AdminSideMenuProvider} from '@/components/admin/side-menu'const items = [ { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: false }, { key: 'components', label: 'Components', icon: IconComponents, active: true }, { key: 'settings', label: 'Settings', icon: IconSettings, active: false }]</script><template> <admin-side-menu-provider class="block h-64 w-60"> <admin-side-menu collapsible="none"> <admin-side-menu-content> <admin-side-menu-group> <admin-side-menu-list> <admin-side-menu-list-item v-for="item in items" :key="item.key"> <admin-side-menu-list-button :is-active="item.active"> <component :is="item.icon" class="size-4 shrink-0" /> {{ item.label }} </admin-side-menu-list-button> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group> </admin-side-menu-content> </admin-side-menu> </admin-side-menu-provider></template>完整組成
header/footer/group-label/group-action/list-badge/list-action/separator/input/skeleton。
<script setup lang="ts">import { IconDots, IconInbox, IconLayoutDashboard, IconPlus, IconSearch, IconSettings, IconUserCircle} from '@tabler/icons-vue'import { AdminSideMenu, AdminSideMenuContent, AdminSideMenuFooter, AdminSideMenuGroup, AdminSideMenuGroupAction, AdminSideMenuGroupContent, AdminSideMenuGroupLabel, AdminSideMenuHeader, AdminSideMenuInput, AdminSideMenuList, AdminSideMenuListAction, AdminSideMenuListBadge, AdminSideMenuListButton, AdminSideMenuListItem, AdminSideMenuListSkeleton, AdminSideMenuProvider, AdminSideMenuSeparator} from '@/components/admin/side-menu'const keyword = ref('')const items = [ { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: true, badge: '' }, { key: 'inbox', label: 'Inbox', icon: IconInbox, active: false, badge: '12' }, { key: 'settings', label: 'Settings', icon: IconSettings, active: false, badge: '' }]</script><template> <admin-side-menu-provider class="block h-96 w-64"> <admin-side-menu collapsible="none"> <admin-side-menu-header> <admin-side-menu-input v-model="keyword" placeholder="搜尋…"> <template #prefix> <icon-search class="size-4 shrink-0 text-text-50" /> </template> </admin-side-menu-input> </admin-side-menu-header> <admin-side-menu-separator /> <admin-side-menu-content> <admin-side-menu-group> <admin-side-menu-group-label>導覽</admin-side-menu-group-label> <admin-side-menu-group-action aria-label="新增"> <icon-plus /> </admin-side-menu-group-action> <admin-side-menu-group-content> <admin-side-menu-list> <admin-side-menu-list-item v-for="item in items" :key="item.key"> <admin-side-menu-list-button :is-active="item.active"> <component :is="item.icon" class="size-4 shrink-0" /> {{ item.label }} </admin-side-menu-list-button> <admin-side-menu-list-badge v-if="item.badge">{{ item.badge }}</admin-side-menu-list-badge> <admin-side-menu-list-action v-else aria-label="更多"> <icon-dots /> </admin-side-menu-list-action> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group-content> </admin-side-menu-group> <admin-side-menu-group> <admin-side-menu-group-label>載入中</admin-side-menu-group-label> <admin-side-menu-group-content> <admin-side-menu-list> <admin-side-menu-list-item v-for="n in 3" :key="n"> <admin-side-menu-list-skeleton show-icon /> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group-content> </admin-side-menu-group> </admin-side-menu-content> <admin-side-menu-footer> <admin-side-menu-list> <admin-side-menu-list-item> <admin-side-menu-list-button> <icon-user-circle class="size-4 shrink-0" /> 帳號 </admin-side-menu-list-button> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-footer> </admin-side-menu> </admin-side-menu-provider></template>變體與收合
variant:default/floating/inset;collapsible:icon/offcanvas。以 admin-side-menu-trigger 或 admin-side-menu-rail 切換展開/收合。
<script setup lang="ts">import type { AdminSideMenuProps } from '@/components/admin/side-menu'import { IconFolder, IconInbox, IconLayoutDashboard, IconLayoutSidebar, IconSettings} from '@tabler/icons-vue'import { ref } from 'vue'import { cn } from '@/lib/utils'import { AdminSideMenu, AdminSideMenuContent, AdminSideMenuFooter, AdminSideMenuGroup, AdminSideMenuHeader, AdminSideMenuInset, AdminSideMenuList, AdminSideMenuListBadge, AdminSideMenuListButton, AdminSideMenuListItem, AdminSideMenuProvider, AdminSideMenuRail, AdminSideMenuTrigger} from '@/components/admin/side-menu'type Variant = NonNullable<AdminSideMenuProps['variant']>type Collapsible = 'icon' | 'offcanvas'const VARIANT_OPTIONS: readonly Variant[] = ['default', 'floating', 'inset']const COLLAPSIBLE_OPTIONS: readonly Collapsible[] = ['icon', 'offcanvas']const variant = ref<Variant>('default')const collapsible = ref<Collapsible>('icon')const mobileSheet = ref(true)const items = [ { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: true, badge: '' }, { key: 'inbox', label: 'Inbox', icon: IconInbox, active: false, badge: '8' }, { key: 'projects', label: 'Projects', icon: IconFolder, active: false, badge: '' }, { key: 'settings', label: 'Settings', icon: IconSettings, active: false, badge: '' }]const rowClass = 'flex flex-col gap-2 sm:flex-row sm:items-center'const brandIconClass = 'flex size-8 shrink-0 items-center justify-center rounded-md bg-primary-50 text-text-10'const brandTextClass = 'truncate typo-title-small text-text-90 group-data-[collapsible=icon]:hidden'const userTextClass = 'truncate typo-content-medium text-text-70 group-data-[collapsible=icon]:hidden'const triggerClass = 'flex size-8 cursor-pointer items-center justify-center rounded-md text-text-70 hover:bg-bg-60'const optionClass = (selected: boolean) => cn( 'cursor-pointer rounded-md border px-3 py-1.5 typo-content-medium transition-colors', selected ? 'border-primary-50 bg-primary-10 text-primary-50' : ` border-text-30 bg-bg-90 text-text-70 hover:border-text-40 ` )</script><template> <div class="flex flex-col gap-4"> <div class="flex flex-col gap-3"> <div :class="rowClass"> <span class="w-24 shrink-0 typo-content-medium text-text-70">variant</span> <div class="flex flex-wrap gap-1.5"> <button v-for="opt in VARIANT_OPTIONS" :key="opt" type="button" :class="optionClass(variant === opt)" @click="variant = opt" > {{ opt }} </button> </div> </div> <div :class="rowClass"> <span class="w-24 shrink-0 typo-content-medium text-text-70">collapsible</span> <div class="flex flex-wrap gap-1.5"> <button v-for="opt in COLLAPSIBLE_OPTIONS" :key="opt" type="button" :class="optionClass(collapsible === opt)" @click="collapsible = opt" > {{ opt }} </button> </div> </div> <div :class="rowClass"> <span class="w-24 shrink-0 typo-content-medium text-text-70">mobileSheet</span> <div class="flex flex-wrap gap-1.5"> <button type="button" :class="optionClass(mobileSheet)" @click="mobileSheet = true"> true </button> <button type="button" :class="optionClass(!mobileSheet)" @click="mobileSheet = false"> false </button> </div> </div> </div> <div :class=" cn( 'relative h-96 w-full overflow-hidden rounded-lg border border-text-20', 'transform-[translateZ(0)]' ) " > <admin-side-menu-provider class="size-full min-h-0" :mobile-sheet="mobileSheet"> <admin-side-menu :collapsible="collapsible" :variant="variant"> <admin-side-menu-header> <div class="flex h-11 items-center gap-2 px-1"> <div :class="brandIconClass"> <icon-layout-sidebar class="size-5" /> </div> <span :class="brandTextClass">Admin</span> </div> </admin-side-menu-header> <admin-side-menu-content> <admin-side-menu-group> <admin-side-menu-list> <admin-side-menu-list-item v-for="item in items" :key="item.key"> <admin-side-menu-list-button :is-active="item.active" :tooltip="item.label"> <component :is="item.icon" class="size-4 shrink-0" /> <span>{{ item.label }}</span> </admin-side-menu-list-button> <admin-side-menu-list-badge v-if="item.badge">{{ item.badge }}</admin-side-menu-list-badge> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group> </admin-side-menu-content> <admin-side-menu-footer> <div class="flex h-11 items-center gap-2 px-1"> <div class="size-8 shrink-0 rounded-full bg-bg-60"></div> <span :class="userTextClass">paul.chen</span> </div> </admin-side-menu-footer> <admin-side-menu-rail /> </admin-side-menu> <admin-side-menu-inset> <div class="flex h-12 shrink-0 items-center gap-2 border-b border-text-20 px-3"> <admin-side-menu-trigger :class="triggerClass"> <icon-layout-sidebar class="size-5" /> </admin-side-menu-trigger> <span class="typo-title-small text-text-90">主內容</span> </div> <div class="flex-1 p-4 typo-content-medium text-text-50"> 切換上方 variant / collapsible,或點左上角按鈕與側邊 rail 觀察收合效果。mobileSheet 僅在瀏覽器視窗寬度 < 768px 時生效:true 於手機以 Sheet 呈現,false 則維持電腦版 offcanvas 樣式。 </div> </admin-side-menu-inset> </admin-side-menu-provider> </div> </div></template>API 參考
組成
<template> <admin-side-menu-provider> <admin-side-menu> <admin-side-menu-content> <admin-side-menu-group> <admin-side-menu-list> <admin-side-menu-list-item> <admin-side-menu-list-button>導覽項目</admin-side-menu-list-button> </admin-side-menu-list-item> </admin-side-menu-list> </admin-side-menu-group> </admin-side-menu-content> </admin-side-menu> <admin-side-menu-inset> <admin-side-menu-trigger /> 主內容 </admin-side-menu-inset> </admin-side-menu-provider></template>admin-side-menu
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
side | "right" | "left" | undefined | "left" | 側選單對齊方向 |
variant | "default" | "inset" | "floating" | undefined | "default" | 視覺樣式變體 |
collapsible | "none" | "icon" | "offcanvas" | undefined | "icon" | 收合行為(offcanvas / icon / none 之一) |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | { isMobile: boolean; state: "expanded" | "collapsed"; } | — |
admin-side-menu-content
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | { state: "expanded" | "collapsed"; } | — |
admin-side-menu-group
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-inset
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-button
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | false | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
isActive | boolean | undefined | false | 設為 `true` 時套用 active 狀態樣式 |
class | ClassValue | — | 根元素之自訂 class |
tooltip | string | globalThis.Component | undefined | — | 收合狀態(collapsed icon-only)時顯示之 tooltip 文字或元件 |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-button-child
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
isActive | boolean | undefined | — | 設為 `true` 時套用 active 狀態樣式 |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-item
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-sub
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-sub-button
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "a" | The element or component this component should render as. Can be overwritten by `asChild`. |
isActive | boolean | undefined | — | 設為 `true` 時套用 active 狀態樣式 |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-sub-item
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-provider
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
defaultOpen | boolean | undefined | true | uncontrolled 模式之初始開啟狀態 |
open | boolean | undefined | — | controlled 模式之開啟狀態(以 `v-model:open` 綁定) |
mobileSheet | boolean | undefined | true | 手機(`< 768px`)是否以 Sheet 渲染;設為 `false` 時關閉手機 Sheet,所有 variant/collapsible 在全寬度維持電腦版樣式,預設 `true` |
class | ClassValue | — | 根元素之自訂 class |
Events
| 名稱 | Payload | 說明 |
|---|---|---|
update:open | boolean | 開啟狀態變更時觸發 |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-trigger
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-rail
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-header
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-footer
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-group-label
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | false | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "div" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-group-content
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-group-action
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | false | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-action
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | false | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
showOnHover | boolean | undefined | false | 設為 `true` 時僅在項目 hover / focus / 開啟狀態顯示 |
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-list-badge
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-side-menu-input
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
admin-side-menu-separator
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
admin-side-menu-list-skeleton
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
showIcon | boolean | undefined | false | 是否顯示前置 icon 佔位 |
width | string | undefined | "70%" | 文字佔位寬度(固定值,避免 SSR hydration mismatch) |
class | ClassValue | — | 根元素之自訂 class |