Instruction file imported from jfwangncs/JfYu.Template (
.github/instructions/vue.instructions.md). Copyright stays with the author.
Tech Stack
- 基于 pnpm workspaces + Turborepo 的 Vue 3 + TypeScript + Vite monorepo 项目。
- 提供多个 UI 组件库版本(Ant Design Vue、Element Plus、Naive UI、TDesign),共享同一套使用tailwindcss+shadcn-vue的UI组件库核心框架。
- 要求 Node ≥ 20.19.0,pnpm ≥ 10。
- 使用 prettier + eslint + stylelint 进行代码检查和格式化。
- 使用 vitest 进行单元测试。
- 使用 commitlint + czg + lefthook 进行提交规范。
- 使用 vsh 进行代码检查和格式化。
- 使用 turbo 进行构建,vite 进行开发,vue-tsc 进行类型检查。
# 其他检查
pnpm check:circular # 循环依赖扫描
pnpm check:dep # depcheck 依赖检查
pnpm check:cspell # 拼写检查
# 清理
pnpm clean # 删除 dist、node_modules 等产物
pnpm reinstall # clean + 重新安装
# 交互式规范提交
pnpm commit # czg 提交向导
Turbo 任务通过 dependsOn: ["^build"] 级联,构建某个应用时会自动先构建其所有依赖包。
Monorepo 目录结构
apps/
web-antd/ # Ant Design Vue 应用
packages/
@core/ # 框架核心(不依赖具体 UI 库)
base/ # 共享工具、缓存、颜色处理、类型定义
composables/ # 核心 Vue composable
preferences/ # PreferenceManager 类(响应式、持久化配置)
ui-kit/ # UI 组件片段:form-ui、layout-ui、menu-ui、popup-ui、shadcn-ui、tabs-ui
effects/ # 高层模块,可依赖 @core 和 UI 库
access/ # 路由/菜单生成与权限指令
common-ui/ # 通用 UI 组件(ApiComponent、IconPicker、VCropper、Tippy 等)
hooks/ # useAppConfig 等
layouts/ # BasicLayout、登录页、各类 widgets
plugins/ # Motion 等插件
request/ # RequestClient(axios 封装 + 拦截器体系)
constants/ # 全局常量(LOGIN_PATH 等)
icons/ # Iconify 图标封装
locales/ # vue-i18n 初始化、loadLocalesMap 工具
preferences/ # 对外暴露 @core/preferences 的公共 API
stores/ # Pinia 全局 store:useAccessStore、useUserStore、useTabbarStore
styles/ # 全局 CSS / TailwindCSS 基础样式
types/ # 共享 TypeScript 类型
utils/ # 共享工具函数(mergeRouteModules、mapTree 等)
internal/
lint-configs/ # ESLint、Prettier、Stylelint、commitlint 配置包
node-utils/ # 构建时 Node 工具
tailwind-config/ # 共享 Tailwind 配置
tsconfig/ # 基础 tsconfig
vite-config/ # 共享 Vite 配置工厂 + 插件集合
scripts/
vsh/ # CLI 工具(lint、check-dep、check-circular、publint)
turbo-run/ # 交互式 turbo 运行器
核心架构说明
应用启动流程
每个应用的 src/main.ts 调用 bootstrap(namespace)(位于 src/bootstrap.ts),依次执行:
- 初始化组件适配器(
src/adapter/component/index.ts)——将通用表单组件名映射到具体 UI 库的组件。 - 调用
initSetupVbenForm()(src/adapter/form.ts)配置通用表单系统。 - 依次初始化 i18n、Pinia stores、权限指令、Tippy、路由、MotionPlugin,最后挂载到
#app。
偏好设置系统
@vben/preferences 导出单例 preferences(PreferenceManager)。它是响应式的,自动持久化到 localStorage(以应用 namespace 为前缀),并驱动主题 CSS 变量的更新。各应用在 src/preferences.ts 中调用 defineOverridesPreferences() 覆盖默认值,无需修改核心代码。
权限/访问系统
@vben/access(packages/effects/access)支持三种访问模式:
- frontend:根据用户角色过滤静态路由。
- backend:从接口(
getAllMenusApi)获取菜单并动态注册路由。 - mixed:同时使用以上两种方式。
路由守卫(src/router/guard.ts)在登录后首次导航时调用 generateAccess(),将结果存入 useAccessStore,再重定向到目标页。v-access 指令和 <AccessControl> 组件用于按权限码或角色控制 UI 元素显示。
请求客户端
@vben/request 将 Axios 封装为 RequestClient。每个应用在 src/api/request.ts 中创建自己的实例,挂载以下拦截器:
- 请求拦截:自动附加 Bearer Token 和 Accept-Language 头。
defaultResponseInterceptor:解包{ code, data, message }响应格式。authenticateResponseInterceptor:处理 401,自动刷新 token 或跳转登录。errorMessageResponseInterceptor:调用message.error()显示错误。
在 API 文件中从 #/api/request 引入 requestClient(自动解包响应)或 baseRequestClient(原始响应)。
路由组织
src/router/routes/modules/*.ts:需要权限验证的动态路由。src/router/routes/core/:始终可访问的路由(登录页、404 等)。mergeRouteModules(import.meta.glob(...))用于聚合路由模块文件。
适配器模式
每个 UI 库应用在 src/adapter/ 下提供适配器,将 @vben/common-ui 的通用 form/modal/drawer 组件桥接到具体组件库。这是 web-antd、web-ele 等应用之间的主要差异所在。
全局 Pinia Store
useAccessStore:token、路由、菜单、锁屏、登录过期状态。useUserStore:用户信息、角色、homePath。useTabbarStore:已打开标签页管理。
所有 store 通过 @vben/stores 的 initStores(app, { namespace }) 统一初始化。
开发约定
- 路径别名:
#/*指向各应用的./src/*(在package.json#imports中定义)。 - 依赖版本管理:内部包使用
workspace:*,第三方包使用catalog:(版本集中在pnpm-workspace.yaml#catalog中管理)。 - 提交规范:遵循 Conventional Commits(
feat、fix、chore、docs、refactor、perf、test、ci、style、types、revert),由 lefthook + commitlint 强制执行。 - pre-commit 钩子(lefthook):自动对暂存文件执行 prettier + eslint + stylelint,推荐使用
pnpm commit(czg)提交。 - 新增页面:在
src/views/下创建.vue文件,在src/router/routes/modules/下添加路由模块;若使用 backend 模式,还需确保后端接口返回对应菜单数据。 - 国际化:统一使用
$t('key'),locale 文件位于packages/locales/,项目级国际化文件位于src/locales/langs。
VxeGrid 标准开发模式
文件结构
每个列表页包含三个文件:
src/views/<domain>/<name>/
data.ts # 列配置 + 表单 schema
index.vue # 列表主页(VxeGrid)
modules/
form.vue # 新建/编辑抽屉(仅 CRUD 模块)
data.ts 规范
useColumns(onActionClick)— 列定义,包含CellOperation操作列useGridFormSchema()— 搜索栏 schema(供formOptions使用)useFormSchema()— 抽屉表单 schema(仅 CRUD 模块)
CellOperation 必须提供 nameField 和 nameTitle,否则 Popconfirm 无法显示被删除项的名称:
{
cellRender: {
name: "CellOperation",
attrs: {
nameField: "key", // 用于 Popconfirm 描述的字段名
nameTitle: $t("system.xxx.key"),
onClick: onActionClick,
},
},
}
index.vue — 服务端分页(CRUD / 只读日志)
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useColumns(onActionClick),
height: "auto",
proxyConfig: {
ajax: {
query: async ({ page }, formValues) =>
getList({ pageIndex: page.currentPage, pageSize: page.pageSize, ...formValues }),
},
},
rowConfig: { keyField: "id" },
toolbarConfig: { custom: true, refresh: true, search: true },
},
formOptions: { schema: useGridFormSchema(), submitOnChange: true },
});
index.vue — 客户端分页(非分页 API,如 Redis Cache)
当后端一次性返回所有数据(无分页参数)时,在 query 函数中手动分页和过滤:
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useColumns(onActionClick),
height: "auto",
pagerConfig: { pageSize: 20, pageSizes: [10, 20, 50, 100] },
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const all = await getAllItems();
const filtered = formValues?.searchKey
? all.filter(i => i.key.includes(formValues.searchKey))
: all;
const start = (page.currentPage - 1) * page.pageSize;
return { items: filtered.slice(start, start + page.pageSize), total: filtered.length };
},
},
},
rowConfig: { keyField: "key" }, // 非 id 主键时需指定 keyField
toolbarConfig: { custom: true, refresh: true, search: true },
checkboxConfig: { key: "key" }, // 批量选择时需指定 key 字段
},
formOptions: { schema: useGridFormSchema(), submitOnChange: true },
});
注意:
pagerConfig: { enabled: false }可完全禁用分页;不指定pagerConfig则使用默认分页。
批量删除按钮
批量操作按钮用 <Button> + <IconifyIcon>,不要用 <vxe-button>:
<template #toolbar-actions>
<Button danger type="primary" @click="handleBatchDelete">
<IconifyIcon icon="ant-design:delete-outlined" />
{{ $t("common.batchDelete") }}
</Button>
</template>
获取勾选行:
const rows = gridApi.grid?.getCheckboxRecords();
工具栏按钮
- 创建/功能按钮:用
<Button>fromant-design-vue+<IconifyIcon>或@vben/icons中的 Lucide 组件 - 不要用
<vxe-button>作为工具栏按钮 - 刷新按钮:始终设置
refresh: trueintoolbarConfig,不要手动添加刷新按钮到#toolbar-actions
只读(日志)页面工具栏
只读页面没有 #toolbar-actions slot,需要显式设置 table-title 才能让工具栏渲染:
<Grid :table-title="$t('system.xxx.list')" />
<Page> 高度
所有使用 height: 'auto' 的 VxeGrid 必须包裹在 <Page auto-content-height> 中,否则会触发无限 ResizeObserver 循环。