@htdtcha/core-web

后台管理系统通用脚手架核心包 - 前端(Vue3 + Element Plus,.vue 源码形式发布,消费方 Vite 编译)
556
Install
npm install @htdtcha/core-web
Latest Version:0.0.3
Vue:3
Last Updated:Sep 24, 2026
Links: npm
Maintainer: htdtcha

@htdtcha/core-web

后台管理系统通用脚手架核心包(前端)——登录鉴权 / 动态菜单路由 / 布局 / 权限指令 / 通用组件 开箱即用。

npm version

这是什么

一个前后端分离后台管理系统的前端核心。你只需要写业务页面,所有通用机制由本包提供:

能力 说明
🔐 登录鉴权 密码 / 邮箱验证码双 Tab 登录,token 本地持久化,New-Token 响应头滑动续期,401 自动回登录页
🗂️ 动态路由 菜单存 DB、按角色下发,登录后 addRoute 动态注册;F5 刷新自动重建
🛡️ 权限控制 v-permission 指令 + <AuthButton> 组件(无权限自动隐藏)+ 路由级 meta.perms 拦截(越权地址 → 403)
🧩 布局 侧边栏(可折叠)+ 顶栏面包屑 + 用户下拉 + 标签页,≤768px 自动抽屉化
📦 通用组件 PageContainer(页面容器)/ AuthButton(权限按钮)/ AuthWrapper(权限块)
🌐 HTTP 基座 axios 实例,自动带 token、自动解包 { code, msg, data }、统一错误提示
🎨 主题 Element Plus 暗色主题(html.dark),品牌名 / Logo / 版权可后台配置

技术栈:Vue 3.4+ · Vue Router 4 · Pinia 2 · Element Plus 2 · axios(消费方 Vite 5 / 6 / 7 / 8 均可)

⚠️ 本包以 .vue 源码形式发布,不做编译:动态路由要用 import.meta.glob 在构建期扫描业务页面,发布编译产物会导致业务页面无法纳入路由白名单。因此消费方必须按下文配置 optimizeDeps;包内所有自引用均为相对路径,不需要配置任何别名。

快速开始

1. 用官方命令创建 Vue 3 项目

npm create vue@latest my-admin-web     # 官方脚手架 create-vue
cd my-admin-web

交互提示按下表回答,TypeScript / Vue Router / Pinia 三项必须选 No:

提问 选择 原因
Project name 自定义,如 my-admin-web —
Add TypeScript? No 本包以 JS 源码发布、不附类型声明,TS 项目的 vue-tsc 会报 Could not find a declaration file for module '@htdtcha/core-web'(非要用 TS 见「常见问题排查」)
Add JSX support? No 用不到
Add Vue Router? No 路由由本包提供(含按角色动态注册菜单),重复引入会与包内 router 实例打架
Add Pinia? No 状态由本包提供(user / app / permission 三个 store)
Add Vitest / Cypress / Playwright / ESLint / Prettier 按需 与本包无关,选不选都能跑

生成出来是一个标准 Vue 3 + Vite 项目,自带 vite.config.js、index.html、public/favicon.ico、src/App.vue 等示例文件——下面几步都在它基础上改,不用从零手写。

💡 官方脚手架不支持「非交互 + 纯 JS」组合:npm create vue@latest x -- --default 会带上 TypeScript,而 --no-typescript 这类取反参数会被直接拒绝(ERR_PARSE_ARGS_UNKNOWN_OPTION)。想跳过交互就只能接受默认的 TS 项目,再自己补一句模块声明(见「常见问题排查」)。

2. 安装依赖

npm install        # 模板自带的 vue / vite / @vitejs/plugin-vue
npm install @htdtcha/core-web vue-router pinia element-plus @element-plus/icons-vue axios dayjs

第二行是本包 + 它的 peer 依赖。vue / vue-router / pinia / element-plus 必须只有一份(由你的项目提供),装双份会出现组件实例不一致、样式重复、getActivePinia() 报错等问题。

3. 改 vite.config.js(3 处必配 + 1 段代理)

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' // 模板自带,保留即可

export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: {
      // 模板自带:项目内别名(业务模块用)
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  // ★ 必配 1:optimizeDeps 的两个键缺一不可
  optimizeDeps: {
    // (a) 禁止 Vite 预打包 .vue 源码包。不配会报「.vue 被当二进制 / Invalid tag」,
    //     或让包被复制成多份实例
    exclude: ['@htdtcha/core-web'],

    // (b) 把本包的依赖显式拉进预打包。原因:Vite 的依赖扫描器不会爬进被
    //     exclude 的包,只能靠你项目自身的源码发现依赖——业务模块还没写的
    //     时候什么也发现不了,所有依赖都会以 node_modules 裸源码直出,
    //     其中 dayjs 是 UMD,浏览器裸加载会报
    //     "does not provide an export named 'default'"(详见「常见问题排查」第 1 条)。
    //     下面 7 个就是本包源码里直接 import 的全部三方依赖。
    include: [
      'vue',
      'vue-router',
      'pinia',
      'element-plus',
      '@element-plus/icons-vue',
      'axios',
      'dayjs'
    ]
  },
  // ★ 必配 2:把 /api 代理到后端(core-server 默认 3000)
  server: {
    proxy: {
      '/api': { target: 'http://localhost:3000', changeOrigin: true },
      '/uploads': { target: 'http://localhost:3000', changeOrigin: true }
    }
  }
})

就这样,不需要给本包配任何别名。

为什么 include 必须配? exclude 只是「不要预打包这个包」,它不会让 Vite 继续去预打包这个包的依赖——Vite 改为完全依赖你项目自身源码去「发现」依赖。一旦项目里没有任何文件直接 import vue / element-plus / dayjs(典型场景:刚建好项目、业务模块还没写),Vite 就一个依赖都不预打包,全部裸源码直出。纯 ESM 的包裸跑没问题,但 dayjs 是 UMD,于是首屏直接报错。

生产分包提示:

  • Vite 8(Rolldown 内核):默认分包已经够用,构建不会出现跨 chunk 循环;要精细控制用 build.rolldownOptions.output.codeSplitting。
  • Vite ≤7(Rollup 内核):若手动写 manualChunks,请用 function-form + 反向 catch-all(小名单 vue|vue-router|pinia|axios → vendor,其余第三方全部 → element-plus chunk)。枚举 element-plus 生态的写法会漏包,触发 Circular chunk: element-plus -> vendor -> element-plus,运行时表现为 Cannot access 'X' before initialization(TDZ)。

4. 改 index.html(模板已生成,改 3 处)

模板生成的是 <html lang=""> + <title>Vite App</title>,改成:

<!DOCTYPE html>
<html lang="zh-CN" class="dark">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>后台管理系统</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
改动 说明
class="dark" 必须:Element Plus 暗色主题的开关,缺了整体配色不对
<title> 换成你的系统名(后台「参数配置 → 系统名称」也会覆盖显示,但首屏标题取这里的值)
src="/src/main.js" 选了 TypeScript 的模板这里是 main.ts,按你的入口文件名改
<link rel="icon"> 模板自带 + public/favicon.ico,保留(删了浏览器会 404)

5. 清理模板示例文件 + 替换 src/main.js

# 模板的示例页面与样式不再需要(App.vue 由本包提供)
rm -rf src/components src/assets src/App.vue

Windows PowerShell:Remove-Item -Recurse -Force src/components, src/assets, src/App.vue

src/main.js 全部内容(3 行):

import { createAdmin } from '@htdtcha/core-web'
import { businessViews } from './views'

createAdmin({ businessViews }).mount('#app')

6. src/views.js(业务视图白名单,必须由项目侧提供)

/**
 * import.meta.glob 的路径解析发生在「定义它的文件」所在位置,
 * 所以业务页面的 glob 必须写在业务项目代码里,再传给 createAdmin。
 */
export const businessViews = import.meta.glob('./*/views/**/*.vue')

7. 写你的第一个业务模块

见下文「业务模块(module-first)」。

8. 装后端 + 建菜单

本包需配合 @htdtcha/core-server 使用(后端初始化见该包 README)。后端起好后,菜单有两条路:

A. 手动建:在菜单管理里新增一条——类型 C(菜单)、路径 /biz/customer(父目录 /biz「业务示例」)、组件填 customer/index,再在角色管理里勾上 biz:customer:* 权限点。

B. 用业务种子自动建(推荐):在业务项目里放 src/customer/seeders/001_customer_demo.js(文件里幂等插入菜单与权限点),然后

npx admin-seed --scope business    # 只跑业务种子;不加参数则系统+业务全跑

种子按「先查存在性再插入」幂等创建菜单与权限点,重跑无副作用。

业务模块(module-first)

业务模块自包含,直接放在项目 src/ 下,不要 src/business/ 之类的中间层:

src/
├── views.js                    # 视图白名单 glob(必须)
├── main.js
└── customer/                   # 业务模块(与后端 src/customer/ 对称)
    ├── api.js                  # 接口封装(用包导出的 request)
    └── views/
        └── index.vue           # 页面(菜单 component 填 customer/index)

src/customer/api.js 示例:

import { request } from '@htdtcha/core-web'

/** 客户列表(分页) */
export function listCustomer(params) {
  return request.get('/customer', { params })
}

/** 新增客户 */
export function createCustomer(data) {
  return request.post('/customer', data)
}

src/customer/views/index.vue 示例(用包导出的通用组件与权限组件):

<script setup>
import { PageContainer, AuthButton, request } from '@htdtcha/core-web'
import { listCustomer } from '../api'
</script>

<template>
  <PageContainer title="客户管理">
    <AuthButton auth="biz:customer:add" type="primary" @click="openCreate">新增客户</AuthButton>
    <!-- ... -->
  </PageContainer>
</template>

菜单 component 字段 → 视图文件的映射规则(双源解析)

路由注册时按下面的顺序查表,两个源都没命中就 console.warn 并跳过(防注入:非法路径无法注册成路由):

来源 先/后 菜单 component 填 实际解析到
包内系统页 先 user/index 包内 src/views/user/index.vue
包内系统页 先 log/loginLog 包内 src/views/log/loginLog.vue
业务项目页 后 customer/index 项目 src/customer/views/index.vue

业务页的 glob key 形如 ./customer/views/index.vue,由包内 resolveView() 按 ./{第一段}/views/{其余段}.vue 规则拼出。所以业务模块必须是 views/ 子目录这一层结构。

包导出 API

createAdmin(options)

创建并配置好 Vue 应用实例(已挂 pinia / router / ElementPlus / 图标 / v-permission 指令,并启动公开配置拉取与响应式监听)。返回 app,直接 .mount('#app')。

参数 类型 必填 说明
options.businessViews object 否 项目 views.js 导出的 import.meta.glob 结果。不传则只有包内系统页可用

其它导出

import {
  // HTTP 基座(业务 api.js 用)
  request,
  // token 管理
  getToken, setToken, removeToken,
  // 权限判断(有权限点返回 true)
  hasPerm,
  // store
  useAppStore, useUserStore, usePermissionStore,
  // 路由
  createAdminRouter, getRouter, buildRoutesFromMenus, resolveView,
  // 通用组件
  PageContainer, AuthButton, AuthWrapper,
  // 认证 API
  login, sendCode, logout, refresh, getProfile, updateProfile, changePassword, getMyMenus
} from '@htdtcha/core-web'

权限指令 / 组件

<!-- 指令:无权限时元素从 DOM 移除 -->
<el-button v-permission="'biz:customer:add'">新增</el-button>

<!-- 组件:无权限时不渲染(更省心,无需自己包 template) -->
<AuthButton auth="biz:customer:add" type="primary" @click="openCreate">新增客户</AuthButton>

与后端对接约定

项 约定
接口前缀 baseURL = '/api/v1',业务路径直接写 /customer(不要写 /biz/customer)
响应体 { code, msg, data };code === 200 时 request 直接返回 data 本身,调用方无需 .data.data
业务错误 后端业务错误走 HTTP 200 + body.code ≠ 200,由拦截器统一 ElMessage 提示并 reject
续期 响应头 New-Token 自动写回本地(滑动过期)
401 清 token + 跳登录页
菜单 GET /api/v1/auth/menus 返回菜单树,字段 menuName / menuType(M|C|F) / path / component / icon / perms / sort / visible
权限 GET /api/v1/auth/profile 返回 perms: string[]、roles: string[]
公开配置 GET /api/v1/system/config/public 返回 sys.name / sys.logo / sys.copyright(免登录,登录页即用)

常见问题排查

现象 原因 / 对策
1️⃣ 控制台 Uncaught SyntaxError: The requested module '/node_modules/dayjs/dayjs.min.js?v=xxx' does not provide an export named 'default'(栈顶是 element-plus/es/components/time-picker/src/utils.mjs) 漏配 optimizeDeps.include。exclude 让 Vite 扫描器不再爬本包,若项目源码此刻也没有别处直接 import 这些三方包(业务模块还没建时就是),Vite 就零预打包 → 全部裸源码直出 → dayjs(main 是 UMD 的 dayjs.min.js,无 ESM 导出)裸加载报错。按「步骤 3 必配 1 (b)」补上那 7 项即可。仅 dev 出现,vite build 不受影响
2️⃣ .vue 文件报 Invalid tag / 二进制解析错误 漏配 optimizeDeps.exclude: ['@htdtcha/core-web']
3️⃣ 业务页面 404,控制台 component「xxx」不在视图白名单内,已跳过 src/views.js 没写 / 没传给 createAdmin / 业务页不在 src/{module}/views/ 下
4️⃣ Cannot access 'X' before initialization(TDZ) Rollup 线(Vite ≤7)的 manualChunks 用了 object-form 或枚举式写法;改成 function-form 反向 catch-all,或直接用 Vite 8 的默认分包
5️⃣ Could not find a declaration file for module '@htdtcha/core-web' 你选了 TypeScript 模板。加 src/shims-core-web.d.ts:declare module '@htdtcha/core-web'(或重建成 JS 项目)
6️⃣ 控制台警告「重复安装 element-plus」 peer 依赖被装成了直接依赖;确保 element-plus/vue 由消费方单份提供

配套包

包 作用
@htdtcha/core-server 后端核心:认证 / 权限 / 菜单 / 字典 / 配置 / 日志 / 定时任务 / 迁移

License

MIT

Version History

Version Released Vue License
0.0.3 ^3.4.27
0.0.2 ^3.4.27
0.0.1 ^3.4.27