开发预览 更新于 2026-05-10

Sidebar 侧栏

竖直主导航,支持分组、嵌套子菜单、徽标、收起为图标模式。

基础用法

items 接受两种条目:普通 SidebarItem(叶子菜单)或 SidebarGroup(分组容器,type: 'group' + items[])。v-model 绑定当前选中的 key,defaultOpenKeys 控制初始展开的有 children 的项。

背景 视口
当前选中:analytics
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfSidebar, type SidebarEntry } from '@chufix-design/vue';

const active = ref('analytics');
const items: SidebarEntry[] = [
  {
    type: 'group',
    label: '工作台',
    items: [
      { key: 'overview', label: '概览' },
      { key: 'analytics', label: '分析', badge: '12' },
      { key: 'reports', label: '报表' },
    ],
  },
  {
    type: 'group',
    label: '资源',
    items: [
      {
        key: 'team',
        label: '团队',
        children: [
          { key: 'members', label: '成员' },
          { key: 'roles', label: '角色' },
          { key: 'invitations', label: '邀请', badge: 3 },
        ],
      },
      { key: 'settings', label: '设置' },
      { key: 'billing', label: '账单', disabled: true },
    ],
  },
];
</script>
<template>
  <div style="height: 360px; display:flex; gap: 12px;">
    <div style="border: 1px solid var(--line-1); border-radius: 8px; overflow: hidden;">
      <CfSidebar
        v-model="active"
        :items="items"
        :default-open-keys="['team']"
      />
    </div>
    <div style="flex:1; padding: 12px; color: var(--fg-2); font-size: 12px;">
      当前选中:<code>{{ active }}</code>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import { CfSidebar } from '@chufix-design/vue';

const active = ref('analytics');
const items= [
  {
    type: 'group',
    label: '工作台',
    items: [
      { key: 'overview', label: '概览' },
      { key: 'analytics', label: '分析', badge: '12' },
      { key: 'reports', label: '报表' },
    ],
  },
  {
    type: 'group',
    label: '资源',
    items: [
      {
        key: 'team',
        label: '团队',
        children: [
          { key: 'members', label: '成员' },
          { key: 'roles', label: '角色' },
          { key: 'invitations', label: '邀请', badge: 3 },
        ],
      },
      { key: 'settings', label: '设置' },
      { key: 'billing', label: '账单', disabled: true },
    ],
  },
];
</script>
<template>
  <div style="height: 360px; display:flex; gap: 12px;">
    <div style="border: 1px solid var(--line-1); border-radius: 8px; overflow: hidden;">
      <CfSidebar
        v-model="active"
        :items="items"
        :default-open-keys="['team']"
      />
    </div>
    <div style="flex:1; padding: 12px; color: var(--fg-2); font-size: 12px;">
      当前选中:<code>{{ active }}</code>
    </div>
  </div>
</template>
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('analytics');
  const items: SidebarEntry[] = [
    {
      type: 'group',
      label: '工作台',
      items: [
        { key: 'overview', label: '概览' },
        { key: 'analytics', label: '分析', badge: '12' },
        { key: 'reports', label: '报表' },
      ],
    },
    {
      type: 'group',
      label: '资源',
      items: [
        {
          key: 'team',
          label: '团队',
          children: [
            { key: 'members', label: '成员' },
            { key: 'roles', label: '角色' },
            { key: 'invitations', label: '邀请', badge: 3 },
          ],
        },
        { key: 'settings', label: '设置' },
        { key: 'billing', label: '账单', disabled: true },
      ],
    },
  ];
  return (
    <>
      <div style={{ height: 360, display: "flex", gap: 12 }}>
          <div style={{ border: "1px solid var(--line-1)", borderRadius: 8, overflow: "hidden" }}>
            <CfSidebar value={active} onChange={setActive} items={items} defaultOpenKeys={['team']} />
          </div>
          <div style={{ flex: 1, padding: 12, color: "var(--fg-2)", fontSize: 12 }}>
            当前选中:<code>{active}</code>
          </div>
        </div>
    </>
  );
}
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('analytics');
  const items= [
    {
      type: 'group',
      label: '工作台',
      items: [
        { key: 'overview', label: '概览' },
        { key: 'analytics', label: '分析', badge: '12' },
        { key: 'reports', label: '报表' },
      ],
    },
    {
      type: 'group',
      label: '资源',
      items: [
        {
          key: 'team',
          label: '团队',
          children: [
            { key: 'members', label: '成员' },
            { key: 'roles', label: '角色' },
            { key: 'invitations', label: '邀请', badge: 3 },
          ],
        },
        { key: 'settings', label: '设置' },
        { key: 'billing', label: '账单', disabled: true },
      ],
    },
  ];
  return (
    <>
      <div style={{ height: 360, display: "flex", gap: 12 }}>
          <div style={{ border: "1px solid var(--line-1)", borderRadius: 8, overflow: "hidden" }}>
            <CfSidebar value={active} onChange={setActive} items={items} defaultOpenKeys={['team']} />
          </div>
          <div style={{ flex: 1, padding: 12, color: "var(--fg-2)", fontSize: 12 }}>
            当前选中:<code>{active}</code>
          </div>
        </div>
    </>
  );
}

收起为图标

collapsed 把侧栏宽度缩到 56px,只显示图标 + tooltip(鼠标悬停显示 title)。这种模式下子菜单不再展开,需要在你的应用层面接管交互。

背景 视口
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfSidebar, CfButton, type SidebarEntry } from '@chufix-design/vue';

const active = ref('overview');
const collapsed = ref(true);
const homeIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 7l5-4 5 4v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" stroke="currentColor" stroke-width="1.4"/></svg>';
const chartIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 13V8m4 5V5m4 8V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
const usersIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M2 13a4 4 0 0 1 8 0M11 8.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10.5 13a3 3 0 0 1 4-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
const cogIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2.2" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v2M8 12.5v2M14.5 8h-2M3.5 8h-2M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4M12.6 12.6l-1.4-1.4M4.8 4.8 3.4 3.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';

const items: SidebarEntry[] = [
  { key: 'overview', label: '概览', icon: homeIcon },
  { key: 'analytics', label: '分析', icon: chartIcon, badge: '12' },
  { key: 'team', label: '团队', icon: usersIcon },
  { key: 'settings', label: '设置', icon: cogIcon },
];
</script>
<template>
  <div style="display:flex; gap: 12px; align-items: flex-start;">
    <CfButton size="sm" variant="tertiary" @click="collapsed = !collapsed">
      {{ collapsed ? '展开' : '收起' }}
    </CfButton>
    <div style="border: 1px solid var(--line-1); border-radius: 8px; overflow: hidden;">
      <CfSidebar v-model="active" :items="items" :collapsed="collapsed" />
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import { CfSidebar, CfButton } from '@chufix-design/vue';

const active = ref('overview');
const collapsed = ref(true);
const homeIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 7l5-4 5 4v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" stroke="currentColor" stroke-width="1.4"/></svg>';
const chartIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 13V8m4 5V5m4 8V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
const usersIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M2 13a4 4 0 0 1 8 0M11 8.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10.5 13a3 3 0 0 1 4-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
const cogIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2.2" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v2M8 12.5v2M14.5 8h-2M3.5 8h-2M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4M12.6 12.6l-1.4-1.4M4.8 4.8 3.4 3.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';

const items= [
  { key: 'overview', label: '概览', icon: homeIcon },
  { key: 'analytics', label: '分析', icon, badge: '12' },
  { key: 'team', label: '团队', icon: usersIcon },
  { key: 'settings', label: '设置', icon: cogIcon },
];
</script>
<template>
  <div style="display:flex; gap: 12px; align-items: flex-start;">
    <CfButton size="sm" variant="tertiary" @click="collapsed = !collapsed">
      {{ collapsed ? '展开' : '收起' }}
    </CfButton>
    <div style="border: 1px solid var(--line-1); border-radius: 8px; overflow: hidden;">
      <CfSidebar v-model="active" :items="items" :collapsed="collapsed" />
    </div>
  </div>
</template>
import { useState } from 'react';
import { CfButton, CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('overview');
  const [collapsed, setCollapsed] = useState(true);
  const homeIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 7l5-4 5 4v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" stroke="currentColor" stroke-width="1.4"/></svg>';
  const chartIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 13V8m4 5V5m4 8V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
  const usersIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M2 13a4 4 0 0 1 8 0M11 8.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10.5 13a3 3 0 0 1 4-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
  const cogIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2.2" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v2M8 12.5v2M14.5 8h-2M3.5 8h-2M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4M12.6 12.6l-1.4-1.4M4.8 4.8 3.4 3.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';

  const items: SidebarEntry[] = [
    { key: 'overview', label: '概览', icon: homeIcon },
    { key: 'analytics', label: '分析', icon: chartIcon, badge: '12' },
    { key: 'team', label: '团队', icon: usersIcon },
    { key: 'settings', label: '设置', icon: cogIcon },
  ];
  return (
    <>
      <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
          <CfButton size="sm" variant="tertiary" onClick={() => setCollapsed(!collapsed)}>
            {collapsed ? '展开' : '收起'}
          </CfButton>
          <div style={{ border: "1px solid var(--line-1)", borderRadius: 8, overflow: "hidden" }}>
            <CfSidebar value={active} onChange={setActive} items={items} collapsed={collapsed} />
          </div>
        </div>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('overview');
  const [collapsed, setCollapsed] = useState(true);
  const homeIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 7l5-4 5 4v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" stroke="currentColor" stroke-width="1.4"/></svg>';
  const chartIcon = '<svg viewBox="0 0 16 16" fill="none"><path d="M3 13V8m4 5V5m4 8V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
  const usersIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M2 13a4 4 0 0 1 8 0M11 8.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10.5 13a3 3 0 0 1 4-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
  const cogIcon = '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2.2" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v2M8 12.5v2M14.5 8h-2M3.5 8h-2M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4M12.6 12.6l-1.4-1.4M4.8 4.8 3.4 3.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';

  const items= [
    { key: 'overview', label: '概览', icon: homeIcon },
    { key: 'analytics', label: '分析', icon, badge: '12' },
    { key: 'team', label: '团队', icon: usersIcon },
    { key: 'settings', label: '设置', icon: cogIcon },
  ];
  return (
    <>
      <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
          <CfButton size="sm" variant="tertiary" onClick={() => setCollapsed(!collapsed)}>
            {collapsed ? '展开' : '收起'}
          </CfButton>
          <div style={{ border: "1px solid var(--line-1)", borderRadius: 8, overflow: "hidden" }}>
            <CfSidebar value={active} onChange={setActive} items={items} collapsed={collapsed} />
          </div>
        </div>
    </>
  );
}

徽标 + 禁用

item.badge 接收数字或字符串,渲染在 label 右侧。item.disabled 灰显且不响应点击。

背景 视口
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfSidebar, type SidebarEntry } from '@chufix-design/vue';

const active = ref('inbox');
const items: SidebarEntry[] = [
  { key: 'inbox', label: '收件箱', badge: 12 },
  { key: 'starred', label: '加星' },
  { key: 'sent', label: '已发送' },
  { key: 'drafts', label: '草稿', badge: 'NEW' },
  { key: 'trash', label: '已删除', disabled: true },
];
</script>
<template>
  <CfSidebar v-model="active" :items="items" />
</template>
<script setup>
import { ref } from 'vue';
import { CfSidebar } from '@chufix-design/vue';

const active = ref('inbox');
const items= [
  { key: 'inbox', label: '收件箱', badge: 12 },
  { key: 'starred', label: '加星' },
  { key: 'sent', label: '已发送' },
  { key: 'drafts', label: '草稿', badge: 'NEW' },
  { key: 'trash', label: '已删除', disabled: true },
];
</script>
<template>
  <CfSidebar v-model="active" :items="items" />
</template>
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('inbox');
  const items: SidebarEntry[] = [
    { key: 'inbox', label: '收件箱', badge: 12 },
    { key: 'starred', label: '加星' },
    { key: 'sent', label: '已发送' },
    { key: 'drafts', label: '草稿', badge: 'NEW' },
    { key: 'trash', label: '已删除', disabled: true },
  ];
  return (
    <>
      <CfSidebar value={active} onChange={setActive} items={items} />
    </>
  );
}
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState('inbox');
  const items= [
    { key: 'inbox', label: '收件箱', badge: 12 },
    { key: 'starred', label: '加星' },
    { key: 'sent', label: '已发送' },
    { key: 'drafts', label: '草稿', badge: 'NEW' },
    { key: 'trash', label: '已删除', disabled: true },
  ];
  return (
    <>
      <CfSidebar value={active} onChange={setActive} items={items} />
    </>
  );
}

图标菜单

item.icon 接收原始 SVG 字符串,组件通过 v-html 注入到 .cf-sidebar__icon 容器里。建议外层 svg 用 currentColor 描边/填充,这样会自动跟随当前菜单项的文字色(包括选中态的强调色)。

背景 视口
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfSidebar, type SidebarEntry } from '@chufix-design/vue';

const active = ref<string>('dashboard');

const SVG = {
  dashboard: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="6" rx="1"/><rect x="9" y="2" width="5" height="4" rx="1"/><rect x="2" y="10" width="5" height="4" rx="1"/><rect x="9" y="8" width="5" height="6" rx="1"/></svg>',
  inbox: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8l2-5h8l2 5v5H2z"/><path d="M2 8h3l1 2h4l1-2h3"/></svg>',
  team: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><path d="M2 14c0-2.5 2-4 4-4s4 1.5 4 4"/><circle cx="12" cy="5" r="1.6"/><path d="M10.5 13c0-1.8 1-2.8 2.5-2.8s2.5 1 2.5 2.8"/></svg>',
  settings: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></svg>',
};

const items: SidebarEntry[] = [
  { key: 'dashboard', label: '工作台', icon: SVG.dashboard },
  { key: 'inbox', label: '收件箱', icon: SVG.inbox, badge: 5 },
  { key: 'team', label: '团队', icon: SVG.team },
  { key: 'settings', label: '设置', icon: SVG.settings },
];
</script>
<template>
  <CfSidebar v-model="active" :items="items" />
</template>
<script setup>
import { ref } from 'vue';
import { CfSidebar } from '@chufix-design/vue';

const active = ref<string>('dashboard');

const SVG = {
  dashboard: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="6" rx="1"/><rect x="9" y="2" width="5" height="4" rx="1"/><rect x="2" y="10" width="5" height="4" rx="1"/><rect x="9" y="8" width="5" height="6" rx="1"/></svg>',
  inbox: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8l2-5h8l2 5v5H2z"/><path d="M2 8h3l1 2h4l1-2h3"/></svg>',
  team: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><path d="M2 14c0-2.5 2-4 4-4s4 1.5 4 4"/><circle cx="12" cy="5" r="1.6"/><path d="M10.5 13c0-1.8 1-2.8 2.5-2.8s2.5 1 2.5 2.8"/></svg>',
  settings: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></svg>',
};

const items= [
  { key: 'dashboard', label: '工作台', icon: SVG.dashboard },
  { key: 'inbox', label: '收件箱', icon: SVG.inbox, badge: 5 },
  { key: 'team', label: '团队', icon: SVG.team },
  { key: 'settings', label: '设置', icon: SVG.settings },
];
</script>
<template>
  <CfSidebar v-model="active" :items="items" />
</template>
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState<string>('dashboard');

  const SVG = {
    dashboard: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="6" rx="1"/><rect x="9" y="2" width="5" height="4" rx="1"/><rect x="2" y="10" width="5" height="4" rx="1"/><rect x="9" y="8" width="5" height="6" rx="1"/></svg>',
    inbox: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8l2-5h8l2 5v5H2z"/><path d="M2 8h3l1 2h4l1-2h3"/></svg>',
    team: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><path d="M2 14c0-2.5 2-4 4-4s4 1.5 4 4"/><circle cx="12" cy="5" r="1.6"/><path d="M10.5 13c0-1.8 1-2.8 2.5-2.8s2.5 1 2.5 2.8"/></svg>',
    settings: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></svg>',
  };

  const items: SidebarEntry[] = [
    { key: 'dashboard', label: '工作台', icon: SVG.dashboard },
    { key: 'inbox', label: '收件箱', icon: SVG.inbox, badge: 5 },
    { key: 'team', label: '团队', icon: SVG.team },
    { key: 'settings', label: '设置', icon: SVG.settings },
  ];
  return (
    <>
      <CfSidebar value={active} onChange={setActive} items={items} />
    </>
  );
}
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState<string>('dashboard');

  const SVG = {
    dashboard: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="6" rx="1"/><rect x="9" y="2" width="5" height="4" rx="1"/><rect x="2" y="10" width="5" height="4" rx="1"/><rect x="9" y="8" width="5" height="6" rx="1"/></svg>',
    inbox: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8l2-5h8l2 5v5H2z"/><path d="M2 8h3l1 2h4l1-2h3"/></svg>',
    team: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><path d="M2 14c0-2.5 2-4 4-4s4 1.5 4 4"/><circle cx="12" cy="5" r="1.6"/><path d="M10.5 13c0-1.8 1-2.8 2.5-2.8s2.5 1 2.5 2.8"/></svg>',
    settings: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></svg>',
  };

  const items= [
    { key: 'dashboard', label: '工作台', icon: SVG.dashboard },
    { key: 'inbox', label: '收件箱', icon: SVG.inbox, badge: 5 },
    { key: 'team', label: '团队', icon: SVG.team },
    { key: 'settings', label: '设置', icon: SVG.settings },
  ];
  return (
    <>
      <CfSidebar value={active} onChange={setActive} items={items} />
    </>
  );
}

受控 openKeys

v-model:open-keys (Vue) / openKeys + onOpenKeysChange (React) 实现完全受控的展开状态 —— 配合按钮可以一键展开 / 收起所有分组,或在不同视图间同步展开状态。

背景 视口
openKeys = [team]
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfSidebar, type SidebarEntry } from '@chufix-design/vue';

const active = ref<string>('members');
const openKeys = ref<string[]>(['team']);

const items: SidebarEntry[] = [
  {
    key: 'team',
    label: '团队',
    children: [
      { key: 'members', label: '成员' },
      { key: 'roles', label: '角色' },
      { key: 'invitations', label: '邀请', badge: 3 },
    ],
  },
  {
    key: 'projects',
    label: '项目',
    children: [
      { key: 'active', label: '进行中' },
      { key: 'archive', label: '归档' },
    ],
  },
  { key: 'settings', label: '设置' },
];

function expandAll() {
  openKeys.value = ['team', 'projects'];
}
function collapseAll() {
  openKeys.value = [];
}
</script>
<template>
  <div class="demo-row">
    <CfButton size="sm" variant="tertiary" @click="expandAll">全部展开</CfButton>
    <CfButton size="sm" variant="tertiary" @click="collapseAll">全部收起</CfButton>
    <span class="adm-hint">openKeys = [{{ openKeys.join(', ') }}]</span>
  </div>
  <CfSidebar
    v-model="active"
    v-model:open-keys="openKeys"
    :items="items"
  />
</template>
<style scoped>
.demo-row { margin-bottom: 12px; }
.adm-hint { color: var(--fg-3); font-size: var(--t-12); margin-left: 8px; font-family: var(--font-mono); }
</style>
<script setup>
import { ref } from 'vue';
import { CfButton, CfSidebar } from '@chufix-design/vue';

const active = ref<string>('members');
const openKeys = ref<string[]>(['team']);

const items= [
  {
    key: 'team',
    label: '团队',
    children: [
      { key: 'members', label: '成员' },
      { key: 'roles', label: '角色' },
      { key: 'invitations', label: '邀请', badge: 3 },
    ],
  },
  {
    key: 'projects',
    label: '项目',
    children: [
      { key: 'active', label: '进行中' },
      { key: 'archive', label: '归档' },
    ],
  },
  { key: 'settings', label: '设置' },
];

function expandAll() {
  openKeys.value = ['team', 'projects'];
}
function collapseAll() {
  openKeys.value = [];
}
</script>
<template>
  <div class="demo-row">
    <CfButton size="sm" variant="tertiary" @click="expandAll">全部展开</CfButton>
    <CfButton size="sm" variant="tertiary" @click="collapseAll">全部收起</CfButton>
    <span class="adm-hint">openKeys = [{{ openKeys.join(', ') }}]</span>
  </div>
  <CfSidebar
    v-model="active"
    v-model:open-keys="openKeys"
    :items="items"
  />
</template>
<style scoped>
.demo-row { margin-bottom: 12px; }
.adm-hint { color: var(--fg-3); font-size: var(--t-12); margin-left: 8px; font-family: var(--font-mono); }
</style>
import { useState } from 'react';
import { CfButton, CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState<string>('members');
  const [openKeys, setOpenKeys] = useState<string[]>(['team']);

  const items: SidebarEntry[] = [
    {
      key: 'team',
      label: '团队',
      children: [
        { key: 'members', label: '成员' },
        { key: 'roles', label: '角色' },
        { key: 'invitations', label: '邀请', badge: 3 },
      ],
    },
    {
      key: 'projects',
      label: '项目',
      children: [
        { key: 'active', label: '进行中' },
        { key: 'archive', label: '归档' },
      ],
    },
    { key: 'settings', label: '设置' },
  ];

  function expandAll() {
    setOpenKeys(['team', 'projects']);
  }
  function collapseAll() {
    setOpenKeys([]);
  }
  return (
    <>
      <div className="demo-row">
          <CfButton size="sm" variant="tertiary" onClick={expandAll}>全部展开</CfButton>
          <CfButton size="sm" variant="tertiary" onClick={collapseAll}>全部收起</CfButton>
          <span className="adm-hint">openKeys = [{openKeys.join(', ')}]</span>
        </div>
        <CfSidebar value={active} onChange={setActive} openKeys={openKeys} onOpenKeysChange={setOpenKeys} items={items} />
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [active, setActive] = useState<string>('members');
  const [openKeys, setOpenKeys] = useState<string[]>(['team']);

  const items= [
    {
      key: 'team',
      label: '团队',
      children: [
        { key: 'members', label: '成员' },
        { key: 'roles', label: '角色' },
        { key: 'invitations', label: '邀请', badge: 3 },
      ],
    },
    {
      key: 'projects',
      label: '项目',
      children: [
        { key: 'active', label: '进行中' },
        { key: 'archive', label: '归档' },
      ],
    },
    { key: 'settings', label: '设置' },
  ];

  function expandAll() {
    setOpenKeys(['team', 'projects']);
  }
  function collapseAll() {
    setOpenKeys([]);
  }
  return (
    <>
      <div className="demo-row">
          <CfButton size="sm" variant="tertiary" onClick={expandAll}>全部展开</CfButton>
          <CfButton size="sm" variant="tertiary" onClick={collapseAll}>全部收起</CfButton>
          <span className="adm-hint">openKeys = [{openKeys.join(', ')}]</span>
        </div>
        <CfSidebar value={active} onChange={setActive} openKeys={openKeys} onOpenKeysChange={setOpenKeys} items={items} />
    </>
  );
}

三档尺寸

size —— sm(紧凑型,适合多级嵌套或多条目)/ md / lg(触摸友好)。

src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfSidebar, type SidebarEntry } from '@chufix-design/vue';

const a = ref('overview');
const b = ref('overview');
const c = ref('overview');

const items: SidebarEntry[] = [
  { key: 'overview', label: '概览' },
  { key: 'analytics', label: '分析' },
  { key: 'reports', label: '报表' },
  { key: 'settings', label: '设置' },
];
</script>
<template>
  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
    <CfSidebar v-model="a" :items="items" size="sm" />
    <CfSidebar v-model="b" :items="items" size="md" />
    <CfSidebar v-model="c" :items="items" size="lg" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import { CfSidebar } from '@chufix-design/vue';

const a = ref('overview');
const b = ref('overview');
const c = ref('overview');

const items= [
  { key: 'overview', label: '概览' },
  { key: 'analytics', label: '分析' },
  { key: 'reports', label: '报表' },
  { key: 'settings', label: '设置' },
];
</script>
<template>
  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
    <CfSidebar v-model="a" :items="items" size="sm" />
    <CfSidebar v-model="b" :items="items" size="md" />
    <CfSidebar v-model="c" :items="items" size="lg" />
  </div>
</template>
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [a, setA] = useState('overview');
  const [b, setB] = useState('overview');
  const [c, setC] = useState('overview');

  const items: SidebarEntry[] = [
    { key: 'overview', label: '概览' },
    { key: 'analytics', label: '分析' },
    { key: 'reports', label: '报表' },
    { key: 'settings', label: '设置' },
  ];
  return (
    <>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12 }}>
          <CfSidebar value={a} onChange={setA} items={items} size="sm" />
          <CfSidebar value={b} onChange={setB} items={items} size="md" />
          <CfSidebar value={c} onChange={setC} items={items} size="lg" />
        </div>
    </>
  );
}
import { useState } from 'react';
import { CfSidebar } from '@chufix-design/react';

export default function Demo() {
  const [a, setA] = useState('overview');
  const [b, setB] = useState('overview');
  const [c, setC] = useState('overview');

  const items= [
    { key: 'overview', label: '概览' },
    { key: 'analytics', label: '分析' },
    { key: 'reports', label: '报表' },
    { key: 'settings', label: '设置' },
  ];
  return (
    <>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12 }}>
          <CfSidebar value={a} onChange={setA} items={items} size="sm" />
          <CfSidebar value={b} onChange={setB} items={items} size="md" />
          <CfSidebar value={c} onChange={setC} items={items} size="lg" />
        </div>
    </>
  );
}

API

属性类型默认值说明
itemsSidebarEntry[][]菜单条目,可混排叶子和分组
modelValue (Vue) / value (React)string当前选中的 item.key
openKeys / defaultOpenKeysstring[]受控 / 非受控的展开 key 列表
collapsedbooleanfalse是否收成 56px 图标模式
size'sm' | 'md' | 'lg''md'字号 + 内距

SidebarItem{ key, label, icon?, href?, badge?, disabled?, children? }SidebarGroup{ type: 'group', key?, label?, items[] }

事件:update:modelValue / update:openKeys / select(React 端:onChange / onOpenKeysChange / onSelect)。

AppShell 配合使用时,把 <CfSidebar> 放到 #sidebar slot 即可拼出完整后台壳。

反馈与讨论

Sidebar 侧栏 的讨论

0
0 / 600
正在加载评论...