npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tker/layout

v1.1.9

Published

一个灵活的 Vue 3 中后台系统布局框架,支持适配器模式,可与任意 UI 组件库集成。

Downloads

1,306

Readme

@tker/layout

一个灵活的 Vue 3 中后台系统布局框架,支持适配器模式,可与任意 UI 组件库集成。

⚠️ Vite 项目必须配置预编译排除! 见下方 "Vite 预编译配置" 章节

特性

  • 适配器模式:通过适配器注入任意 UI 组件,不与特定 UI 库绑定
  • 单例管理:adapterManager 和 layoutManager 单例管理适配器和布局数据
  • 自动路由监听:Layout 内部自动监听 vue-router,无需手动设置 activePath
  • 内置 NaiveUI 适配器:提供菜单、面包屑、标签页的内置适配器
  • 自动数据管理:菜单、面包屑、标签页数据由框架内部自动维护
  • 多种布局模式:支持侧边栏菜单(side-menu)和顶部菜单(top-menu)
  • TypeScript 支持:完整的类型定义

安装

pnpm add @tker/layout

⚠️ Vite 预编译配置(必须)

重要:本步骤必须在安装后立即完成,否则所有功能都将失效!

本包使用单例模式管理状态(adapterManagerlayoutManager)。Vite 默认会对依赖进行预编译优化,这会导致单例分离问题:预编译后的模块与运行时模块被当作两个独立的实例,导致适配器和状态无法正确共享。

必须配置

vite.config.ts 中添加以下配置:

import { defineConfig } from "vite";

export default defineConfig({
    optimizeDeps: {
        exclude: ["@tker/layout"],  // 必须!排除预编译以保持单例
    },
});

问题表现

如果不配置,会出现以下问题:

  • addLogoAdapter 等适配器注入后,组件无法获取到适配器
  • setMenus 设置菜单数据后,组件显示空菜单
  • ❌ 状态更新不同步,数据丢失

其他构建工具

  • Webpack:无需额外配置
  • Rollup:无需额外配置
  • esbuild:可能需要类似配置

推荐的项目组织方式

前置条件检查(Vite 项目):

  • ✅ 已安装 @tker/layout
  • ✅ 已在 vite.config.ts 配置 optimizeDeps.exclude: ["@tker/layout"](必须!)

Webpack/Rollup 项目无需此配置。

为了更好地组织 layout 相关代码,建议采用以下目录结构:

src/
├── layout/
│   ├── index.ts              # 导出 setupLayout 函数
│   ├── components/           # 自定义适配器目录
│   │   ├── LogoAdapter.vue   # 自定义 Logo 适配器
│   │   ├── ToolbarAdapter.vue # 自定义工具栏适配器(可选)
│   │   └── UserAvatarAdapter.vue # 自定义用户头像适配器(可选)
│   └── menuData.ts           # 菜单数据配置(可选,可拆分)
├── App.vue                   # 使用 Layout 组件包裹 router-view
├── main.ts                   # 初始化时调用 setupLayout
└── router/
│   └── index.ts              # 路由配置

1. 创建 layout 目录

将 layout 相关的配置和自定义适配器集中管理:

src/layout/index.ts - 初始化配置

import type { MenuItem } from "@tker/layout";

import { h } from "vue";

import {
    addBreadcrumbAdapter,
    addLogoAdapter,
    addMenuAdapter,
    addTabAdapter,
    setMenus,
    NaiveBreadcrumbAdapter,
    NaiveMenuAdapter,
    NaiveTabAdapter,
} from "@tker/layout";

import { NIcon } from "naive-ui";

// 自定义适配器
import LogoAdapter from "./components/LogoAdapter.vue";

// 菜单数据
const menuData: MenuItem[] = [
    {
        path: "/dashboard",
        title: "仪表盘",
        icon: () => h(NIcon, null, { default: () => "📊" }),
    },
    {
        path: "/system",
        title: "系统管理",
        icon: () => h(NIcon, null, { default: () => "⚙️" }),
        children: [
            { path: "/system/user", title: "用户管理" },
            { path: "/system/role", title: "角色管理" },
        ],
    },
];

/**
 * 初始化 Layout 配置
 */
export function setupLayout(): void {
    // 1. 注入适配器组件
    addLogoAdapter(LogoAdapter); // Logo 需自行实现
    addMenuAdapter(NaiveMenuAdapter);
    addBreadcrumbAdapter(NaiveBreadcrumbAdapter);
    addTabAdapter(NaiveTabAdapter);

    // 2. 设置菜单数据
    setMenus(menuData);
}

src/layout/components/LogoAdapter.vue - 自定义 Logo

<script setup lang="ts">
import { computed } from "vue";

interface Props {
    collapsed: boolean;
}

const props = defineProps<Props>();

const logoStyle = computed(() => ({
    width: props.collapsed ? "64px" : "200px",
    overflow: "hidden",
}));
</script>

<template>
    <div class="logo" :style="logoStyle">
        <span v-if="!props.collapsed" class="logo__text">MyApp</span>
        <span v-else class="logo__icon">M</span>
    </div>
</template>

<style scoped>
.logo {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 48px;
    transition: width 0.3s;
}

.logo__text {
    font-size: 18px;
    font-weight: bold;
}

.logo__icon {
    font-size: 24px;
    font-weight: bold;
}
</style>

2. 在 App.vue 中使用 Layout

<script setup lang="ts">
import { Layout } from "@tker/layout";
</script>

<template>
    <layout>
        <router-view />
    </layout>
</template>

3. 在 main.ts 中初始化

import { createApp } from "vue";

import App from "./App.vue";
import { setupLayout } from "./layout";
import { setupRouter } from "./router";

const app = createApp(App);
setupRouter(app);
setupLayout(); // 在 createApp 之后调用

app.mount("#app");

4. 路由配置

Layout 会自动监听 vue-router,路由配置无需特殊处理:

import type { RouteRecordRaw } from "vue-router";

const routes: RouteRecordRaw[] = [
    {
        path: "/",
        children: [
            { path: "", redirect: "/dashboard" },
            { path: "/dashboard", name: "Dashboard", component: Dashboard },
            { path: "/system/user", name: "User", component: User },
            // ...
        ],
    },
];

这种组织方式的优点

  1. 职责分离:layout 配置独立于业务组件
  2. 易于维护:菜单数据、适配器集中管理
  3. 初始化顺序清晰main.ts 中统一调用 setupLayout()
  4. 便于扩展:新增自定义适配器只需在 layout/ 目录添加文件

工作原理

  1. adapterManager(单例):管理所有适配器组件,初始化时注入
  2. layoutManager(单例):管理菜单、面包屑、标签页、布局模式等数据
  3. Layout 组件
    • 自动监听 vue-router 路由变化
    • 根据路由 path 自动设置 activePath
    • 自动生成面包屑和打开标签页
    • 内部维护 pathParamsMap 存储地址栏参数

导航行为

点击菜单、Tab、面包屑时会触发实际的页面跳转(router.push),并自动保持原有地址栏参数:

内部参数映射(pathParamsMap)

Layout 内部维护一个 path -> fullPath 的映射表,自动存储和同步地址栏参数:

用户访问 /user?id=1 → pathParamsMap['/user'] = '/user?id=1'
用户跳转到 /user?id=2 → pathParamsMap['/user'] = '/user?id=2'(更新)
关闭 Tab → 同步清除 pathParamsMap['/user']

跳转逻辑

  • 菜单点击:查询 pathParamsMap,有参数则用 fullPath 跳转,否则用 path
  • Tab 点击:查询 pathParamsMap,用对应的 fullPath 跳转
  • 面包屑点击:检查是否具体页面(无 children),再查询 pathParamsMap 跳转

对适配器透明

适配器只需 emit path,框架内部自动处理参数:

  • TabAdapterEmits.select → emit path
  • 跳转时框架调用 getFullPath(path) 获取完整路径

API

适配器注入

@tker/layout 导入:

import {
    addLogoAdapter,
    addMenuAdapter,
    addBreadcrumbAdapter,
    addTabAdapter,
    addToolbarAdapter,
    addUserAvatarAdapter,
} from "@tker/layout";

| 方法 | 参数 | 说明 | |------|------|------| | addLogoAdapter | (component: Component) | 注入 Logo 适配器 | | addMenuAdapter | (component: Component) | 注入菜单适配器 | | addBreadcrumbAdapter | (component: Component) | 注入面包屑适配器 | | addTabAdapter | (component: Component) | 注入标签页适配器 | | addToolbarAdapter | (component: Component) | 注入工具栏适配器 | | addUserAvatarAdapter | (component: Component) | 注入用户头像适配器 |

配置和数据管理

@tker/layout 导入:

import {
    setMenus,
    setHomePath,
    setLayoutMode,
    setMaxTabs,
    setExpandedWidth,
    setCollapsedWidth,
    toggleCollapse,
    closeTab,
    closeOtherTabs,
    closeAllTabs,
    setTabFixed,
    setBreadcrumb,
    resetBreadcrumb,
} from "@tker/layout";

| 方法 | 参数 | 说明 | |------|------|------| | setMenus | (data: MenuItem[]) | 设置菜单数据(必须) | | setLayoutMode | (mode: 'side-menu' \| 'top-menu') | 设置布局模式(可选,默认 side-menu) | | setHomePath | (path: string) | 设置首页路径,首页 tab 固定在第一位且不可关闭 | | setMaxTabs | (max: number) | 设置最大标签页数量(可选,默认 10) | | setExpandedWidth | (width: string) | 设置侧边栏展开宽度(可选,默认 '200px') | | setCollapsedWidth | (width: string) | 设置侧边栏收起宽度(可选,默认 '64px') | | toggleCollapse | () => void | 切换菜单折叠状态 |

标签页管理(可选)

Layout 会自动根据路由打开标签页,以下方法用于特殊场景:

| 方法 | 参数 | 说明 | |------|------|------| | closeTab | (path: string) | 关闭指定标签页 | | closeOtherTabs | (path?: string) | 关闭其他标签页 | | closeAllTabs | () => void | 关闭所有标签页 | | setTabFixed | (path: string, fixed: boolean) | 设置标签页固定状态 |

面包屑管理(可选)

Layout 会自动根据路由生成面包屑,以下方法用于特殊场景:

| 方法 | 参数 | 说明 | |------|------|------| | setBreadcrumb | (data: BreadcrumbItem[]) | 手动设置面包屑 | | resetBreadcrumb | () => void | 重置为自动生成的面包屑 |

布局模式

支持两种布局模式:

| 模式 | 说明 | |------|------| | side-menu | 侧边栏菜单布局(默认) | | top-menu | 顶部菜单布局 |

import { setLayoutMode } from "@tker/layout";

setLayoutMode("top-menu");

数据类型

以下类型定义中的 Component 需从 vue 导入:

import type { Component } from "vue";

MenuItem

interface MenuItem {
    path: string;                   // 路径(唯一标识)
    title?: string;                 // 标题
    icon?: Component | string;      // 图标
    children?: MenuItem[];          // 子菜单
    [key: string]: any;             // 其他自定义字段
}

TabItem

interface TabItem {
    path: string;                   // 菜单路径(不含参数)
    title: string;                  // 标题
    icon?: Component | string;      // 图标
    fixed?: boolean;                // 是否固定(固定标签不可关闭)
}

注意:Tab 不存储 fullPath,参数由框架内部的 pathParamsMap 维护。

BreadcrumbItem

interface BreadcrumbItem {
    path: string;   // 路径
    title: string;  // 标题
}

自定义适配器

框架不提供任何内置适配器,所有适配器均需用户自行实现。以下是各适配器的接口定义和实现示例。


LogoAdapter

作用:显示系统 Logo,位于顶栏左侧。当菜单折叠时,Logo 宽度会相应变化。

接收的 Props

| 属性 | 类型 | 说明 | |------|------|------| | collapsed | boolean | 菜单是否折叠。用途:根据折叠状态显示不同内容(如折叠时显示简短标识,展开时显示完整 Logo) | | width | string | 当前宽度(如 '200px' 或 '64px')。用途:直接设置 Logo 容器宽度,无需自行计算 |

无 Events

Logo 适配器不需要发出事件。

实现示例

<script setup lang="ts">
interface Props {
    collapsed: boolean;
    width: string;
}

const props = defineProps<Props>();
</script>

<template>
    <div class="logo" :style="{ width: props.width, overflow: 'hidden' }">
        <!-- 折叠时显示简短标识,展开时显示完整内容 -->
        <img v-if="!props.collapsed" src="/logo-full.png" alt="Logo" />
        <img v-else src="/logo-mini.png" alt="Logo" />
    </div>
</template>

<style scoped>
.logo {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 48px;
    transition: width 0.3s;
}

.logo img {
    max-width: 100%;
    max-height: 32px;
}
</style>

MenuAdapter

作用:显示侧边栏或顶部菜单,用于导航。

接收的 Props

| 属性 | 类型 | 说明 | |------|------|------| | menuData | MenuItem[] | 菜单数据。用途:渲染菜单树结构,每个 MenuItem 包含 path(唯一标识)、title、icon、children 等 | | activePath | string | 当前激活的菜单路径。用途:高亮当前选中的菜单项 | | collapsed | boolean | 菜单是否折叠。用途:折叠时隐藏菜单文字,只显示图标;仅 side 模式有效 | | mode | 'side' \| 'top' | 菜单模式。用途side 为垂直菜单(侧边栏),top 为水平菜单(顶栏) | | width | string | 当前宽度(如 '200px' 或 '64px')。用途:直接设置菜单容器宽度,折叠状态下即为收起宽度 |

需要发出的 Events

| 事件 | 参数 | 说明 | |------|------|------| | select | path: string | 用户点击菜单项时触发。必须发出,框架会根据此事件更新路由、面包屑、标签页 | | toggleCollapse | 无 | 用户点击折叠按钮时触发。可选,用于切换侧边栏折叠状态 |

实现示例(使用 NaiveUI NMenu)

<script setup lang="ts">
import type { Component } from "vue";
import { computed, h } from "vue";
import { NMenu } from "naive-ui";

import { getMenuItemIcon, getMenuItemTitle } from "@tker/layout";

interface MenuItem {
    path: string;
    title?: string;
    icon?: Component | string;
    children?: MenuItem[];
    [key: string]: any;
}

interface Props {
    menuData: MenuItem[];
    activePath: string;
    collapsed: boolean;
    mode: 'side' | 'top';
    width: string;
}

interface Emits {
    (e: 'select', path: string): void;
    (e: 'toggleCollapse'): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

// 将 MenuItem 转换为 NMenu 的 options 格式
function transformMenuOptions(items: MenuItem[]): any[] {
    return items.map((item) => {
        const option: any = {
            key: item.path,
            label: getMenuItemTitle(item),
        };

        const icon = getMenuItemIcon(item);
        if (icon) {
            option.icon = typeof icon === "string" ? () => icon : () => h(icon as any);
        }

        if (item.children?.length) {
            option.children = transformMenuOptions(item.children);
        }

        return option;
    });
}

const menuOptions = computed(() => transformMenuOptions(props.menuData));

// 框架 mode ('side' | 'top') 转换为 NMenu mode ('vertical' | 'horizontal')
const naiveMode = computed(() =>
    props.mode === "side" ? "vertical" : "horizontal",
);

// 解析 width 为数值(去掉 px)
const collapsedWidthValue = computed(() => {
    const width = props.width || "64px";
    return parseInt(width.replace("px", ""), 10) || 64;
});

function handleSelect(key: string) {
    emit("select", key); // 必须发出,框架会处理路由跳转
}

function handleToggleCollapse() {
    emit("toggleCollapse"); // 可选,切换折叠状态
}
</script>

<template>
    <div class="menu" :class="{ 'menu--collapsed': collapsed }">
        <n-menu
            :options="menuOptions"
            :value="activePath"
            :mode="naiveMode"
            :collapsed="collapsed"
            :collapsed-width="collapsedWidthValue"
            @update:value="handleSelect"
        />

        <!-- 折叠按钮(仅 side 模式需要) -->
        <button
            v-if="mode === 'side'"
            class="menu__collapse-btn"
            @click="handleToggleCollapse"
        >
            {{ collapsed ? "展开" : "折叠" }}
        </button>
    </div>
</template>

<style scoped>
.menu {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.menu--collapsed {
    overflow: hidden;
}

.menu__collapse-btn {
    padding: 8px;
    border: none;
    background: transparent;
    cursor: pointer;
    text-align: center;
}
</style>

BreadcrumbAdapter

作用:显示面包屑导航,位于顶栏左侧(side-menu 模式下)。

接收的 Props

| 属性 | 类型 | 说明 | |------|------|------| | breadcrumbData | BreadcrumbItem[] | 面包屑数据。用途:渲染面包屑路径,数组按层级顺序排列,最后一项为当前页面 |

需要发出的 Events

| 事件 | 参数 | 说明 | |------|------|------| | select | path: string | 用户点击面包屑项时触发。必须发出,框架会根据此事件更新路由。注意:最后一项(当前页面)通常不可点击 |

实现示例(使用 NaiveUI NBreadcrumb)

<script setup lang="ts">
import { NBreadcrumb, NBreadcrumbItem } from "naive-ui";

interface BreadcrumbItem {
    path: string;
    title: string;
}

interface Props {
    breadcrumbData: BreadcrumbItem[];
}

interface Emits {
    (e: 'select', path: string): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

function handleClick(path: string) {
    emit("select", path); // 必须发出,框架会处理路由跳转
}
</script>

<template>
    <n-breadcrumb>
        <n-breadcrumb-item
            v-for="(item, index) in props.breadcrumbData"
            :key="item.path"
            @click="index < props.breadcrumbData.length - 1 && handleClick(item.path)"
        >
            {{ item.title }}
        </n-breadcrumb-item>
    </n-breadcrumb>
</template>

TabAdapter

作用:显示标签页,位于内容区顶部(side-menu 模式下),用于快速切换已打开的页面。

接收的 Props

| 属性 | 类型 | 说明 | |------|------|------| | tabData | TabItem[] | 标签页数据。用途:渲染标签页列表,每个 TabItem 包含 path、title、icon、fixed | | activeTab | string | 当前激活的标签页路径。用途:高亮当前选中的标签页 |

需要发出的 Events

| 事件 | 参数 | 说明 | |------|------|------| | select | path: string | 用户点击标签页时触发。必须发出,框架会通过 pathParamsMap 获取 fullPath 进行跳转(保持原有参数) | | close | path: string | 用户关闭标签页时触发。必须发出,框架会移除该标签页并同步清除 pathParamsMap | | closeOther | path?: string | 用户关闭其他标签页时触发。可选,path 为保留的标签页 | | closeAll | 无 | 用户关闭所有标签页时触发。可选,框架会关闭所有非固定标签页 | | setFixed | path: string, fixed: boolean | 用户设置标签页固定状态时触发。可选,固定标签不可关闭 |

实现示例(使用 NaiveUI NTabs)

<script setup lang="ts">
import type { Component } from "vue";
import { NTabPane, NTabs } from "naive-ui";

interface TabItem {
    path: string;
    title: string;
    icon?: Component | string;
    fixed?: boolean;
}

interface Props {
    tabData: TabItem[];
    activeTab: string;
}

interface Emits {
    (e: 'select', path: string): void;
    (e: 'close', path: string): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

function handleSelect(path: string) {
    emit("select", path); // 只 emit path,框架自动处理参数
}

function handleClose(path: string) {
    emit("close", path);
}
</script>

<template>
    <n-tabs
        :value="props.activeTab"
        type="card"
        closable
        @update:value="handleSelect"
        @close="(name: string) => handleClose(name)"
    >
        <n-tab-pane
            v-for="tab in props.tabData"
            :key="tab.path"
            :name="tab.path"
            :tab="tab.title"
            :closable="!tab.fixed"
        />
    </n-tabs>
</template>

ToolbarAdapter

作用:显示工具栏,位于顶栏右侧,用于放置快捷操作按钮(如刷新、全屏等)。

无 Props

Toolbar 适配器不接收任何 props,完全由用户自定义内容。

无 Events

Toolbar 适配器不需要发出事件,操作由用户自行处理。

实现示例

<script setup lang="ts">
import { NButton, NSpace } from "naive-ui";

function handleRefresh() {
    // 用户自行处理刷新逻辑
}

function handleFullscreen() {
    // 用户自行处理全屏逻辑
}
</script>

<template>
    <n-space>
        <n-button size="small" @click="handleRefresh">刷新</n-button>
        <n-button size="small" @click="handleFullscreen">全屏</n-button>
    </n-space>
</template>

UserAvatarAdapter

作用:显示用户头像和下拉菜单,位于顶栏最右侧。

无 Props

UserAvatar 适配器不接收任何 props。

可选的 Events

框架不监听 UserAvatar 适配器的任何事件,以下事件仅供参考,用户可选择 emit 或直接在组件内处理逻辑:

| 事件 | 参数 | 说明 | |------|------|------| | settings | 无 | 用户点击"设置"时触发。可选,用户可直接在组件内调用设置逻辑 | | logout | 无 | 用户点击"退出登录"时触发。可选,用户可直接在组件内调用退出逻辑 |

实现示例(使用 NaiveUI NDropdown + NAvatar)

<script setup lang="ts">
import { useRouter } from "vue-router";

import { NAvatar, NDropdown } from "naive-ui";

const router = useRouter();

const dropdownOptions = [
    { label: "个人设置", key: "settings" },
    { label: "退出登录", key: "logout" },
];

function handleSelect(key: string) {
    if (key === "settings") {
        router.push("/settings"); // 直接处理
    } else if (key === "logout") {
        // 直接调用退出逻辑
        console.log("退出登录");
    }
}
</script>

<template>
    <n-dropdown :options="dropdownOptions" @select="handleSelect">
        <n-avatar round size="small">
            <slot>U</slot>
        </n-avatar>
    </n-dropdown>
</template>

内置 NaiveUI 适配器

框架提供以下内置适配器,可从 @tker/layout 直接导入:

import {
    NaiveBreadcrumbAdapter,
    NaiveMenuAdapter,
    NaiveTabAdapter,
} from "@tker/layout";

| 适配器 | 说明 | |--------|------| | NaiveMenuAdapter | 菜单适配器,使用 NMenu | | NaiveBreadcrumbAdapter | 面包屑适配器,使用 NBreadcrumb | | NaiveTabAdapter | 标签页适配器,使用 NTabs |

注意:Logo、Toolbar、UserAvatar 适配器与业务紧密相关,框架不提供内置实现,需用户自行开发。

License

MIT