@htdtcha/core-web
@htdtcha/core-web
后台管理系统通用脚手架核心包(前端)——登录鉴权 / 动态菜单路由 / 布局 / 权限指令 / 通用组件 开箱即用。
这是什么
一个前后端分离后台管理系统的前端核心。你只需要写业务页面,所有通用机制由本包提供:
| 能力 | 说明 |
|---|---|
| 🔐 登录鉴权 | 密码 / 邮箱验证码双 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-pluschunk)。枚举 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 |