feat: skills

This commit is contained in:
sunlei 2026-05-16 17:30:35 +08:00
parent 6ecada0d3d
commit d0e212a283
21 changed files with 4212 additions and 0 deletions

297
skills/SKILL.md Normal file
View File

@ -0,0 +1,297 @@
---
name: vben
description: Vben Admin 5.0 前端框架开发技能。用于开发基于 Vue3、Vite、TypeScript 的中后台管理系统。
TRIGGER when: 编写/修改 Vben Admin 项目代码、配置路由菜单、设置权限控制、主题定制、组件开发、API对接、状态管理、国际化配置。
DO NOT trigger when: 编写后端代码、纯前端通用开发(与 Vben 框架无关)。
---
# Vben Admin 开发技能
基于 Vben Admin 5.0 文档的专业开发指导技能,帮助快速开发中后台管理系统。
## 项目结构
采用 Monorepo 架构,核心目录:
```
├── apps/ # 应用目录
│ ├── web-antd/ # Ant Design Vue 应用
│ ├── web-ele/ # Element Plus 应用
│ ├── web-naive/ # Naive UI 应用
│ └── backend-mock/ # Mock 后端服务
├── packages/ # 共享包
│ ├── @core/ # 核心包UI组件、布局等
│ ├── effects/ # 副作用包权限、请求、hooks等
│ ├── stores/ # 状态管理
│ ├── locales/ # 国际化
│ └── utils/ # 工具函数
└── internal/ # 内部工具配置
```
## 常用命令
```bash
# 开发
pnpm dev:antd # 启动 Ant Design 应用
pnpm dev:ele # 启动 Element Plus 应用
pnpm dev:naive # 启动 Naive UI 应用
# 构建
pnpm build # 构建所有应用
pnpm build:antd # 构建指定应用
# 其他
pnpm lint # 代码检查
pnpm check:type # 类型检查
pnpm reinstall # 重新安装依赖
```
## 核心开发指南
### 路由与菜单
路由配置位于 `src/router/routes/modules/` 目录:
```ts
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'mdi:home',
title: $t('page.home.title'),
authority: ['admin'], // 权限控制
order: 1000, // 菜单排序
keepAlive: true, // 开启缓存
hideInMenu: false, // 菜单中隐藏
hideInTab: false, // 标签页中隐藏
},
name: 'Home',
path: '/home',
component: () => import('#/views/home/index.vue'),
},
];
export default routes;
```
### 权限控制
三种模式在 `preferences.ts` 中配置:
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
app: {
accessMode: 'frontend', // 'frontend' | 'backend' | 'mixed'
},
});
```
按钮级权限:
```vue
<script setup>
import { AccessControl, useAccess } from '@vben/access';
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
</script>
<template>
<!-- 权限码方式 -->
<AccessControl :codes="['AC_100100']" type="code">
<Button>有权限可见</Button>
</AccessControl>
<!-- 角色方式 -->
<Button v-if="hasAccessByRoles(['admin'])">管理员可见</Button>
<!-- 指令方式 -->
<Button v-access:code="'AC_100100'">有权限可见</Button>
</template>
```
### 偏好设置配置
在应用目录的 `preferences.ts` 中配置:
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
app: {
layout: 'sidebar-nav', // 布局方式
locale: 'zh-CN', // 语言
dynamicTitle: true, // 动态标题
watermark: false, // 水印
loginExpiredMode: 'page', // 登录过期模式
},
theme: {
mode: 'dark', // 主题模式
builtinType: 'default', // 内置主题
colorPrimary: 'hsl(212 100% 45%)', // 主题色
},
sidebar: {
collapsed: false, // 侧边栏折叠
width: 224, // 侧边栏宽度
},
tabbar: {
enable: true, // 标签页
keepAlive: true, // 缓存
},
});
```
### API 请求
请求配置在 `src/api/request.ts`
```ts
import { requestClient } from '#/api/request';
// GET 请求
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}
// POST 请求
export async function saveUserApi(user: UserInfo) {
return requestClient.post<UserInfo>('/user', user);
}
```
代理配置在 `vite.config.mts`
```ts
export default defineConfig(async () => {
return {
vite: {
server: {
proxy: {
'/api': {
target: 'http://localhost:5320/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
},
};
});
```
### 国际化
使用 `$t()` 函数:
```ts
import { $t } from '#/locales';
meta: {
title: $t('page.home.title'),
}
```
语言文件在 `packages/locales/` 目录。
### 主题定制
CSS 变量覆盖:
```css
:root {
--primary: 212 100% 45%;
--sidebar: 0 0% 100%;
--header: 0 0% 100%;
}
.dark {
--background: 222.34deg 10.43% 12.27%;
--sidebar: 222.34deg 10.43% 12.27%;
}
```
### 登录接口对接
需要的接口:
```ts
// src/api/core/auth.ts
export async function loginApi(data: LoginParams) {
return requestClient.post<LoginResult>('/auth/login', data);
}
// src/api/core/user.ts
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}
// src/api/core/auth.ts (可选)
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}
```
## 环境变量
```bash
# .env.development
VITE_PORT=5555
VITE_GLOB_API_URL=/api
VITE_NITRO_MOCK=true
# .env.production
VITE_GLOB_API_URL=https://api.example.com
VITE_COMPRESS=gzip
VITE_ROUTER_HISTORY=hash
```
## 别名配置
使用 `#` 开头的路径别名:
```ts
// package.json
{
"imports": {
"#/*": "./src/*"
}
}
// 使用
import { useAuthStore } from '#/store';
```
## 详细参考文档
需要更多细节时,查阅以下参考文件:
### 核心功能 (references/core/)
- **路由菜单**: `references/core/route.md` - 路由配置、Meta属性、多级菜单
- **权限控制**: `references/core/access.md` - 三种权限模式、按钮级权限
- **偏好设置**: `references/core/preferences.md` - 完整配置项说明
- **主题定制**: `references/core/theme.md` - CSS变量、内置主题、自定义主题
- **API请求**: `references/core/api.md` - 请求配置、拦截器、多接口地址
- **国际化**: `references/core/locale.md` - 语言配置、新增语言包
- **登录对接**: `references/core/login.md` - 登录接口、Token刷新
- **图标使用**: `references/core/icons.md` - Iconify图标、SVG图标
### 业务组件 (references/components/business/)
- **Page页面**: `references/components/business/page.md` - 页面布局容器、标题区、内容区
- **表单组件**: `references/components/business/form.md` - Vben Form表单配置、校验、联动
- **表格组件**: `references/components/business/table.md` - Vben Vxe Table表格配置、搜索、远程加载
- **模态框**: `references/components/business/modal.md` - Vben Modal配置、拖拽、全屏
- **抽屉**: `references/components/business/drawer.md` - Vben Drawer配置、组件抽离
- **轻量提示框**: `references/components/business/alert.md` - alert、confirm、prompt调用
- **API组件包装器**: `references/components/business/api-component.md` - 远程数据自动加载
### 通用组件 (references/components/common/)
- **数字动画**: `references/components/common/count-to.md` - CountToAnimator数字滚动动画
- **省略文本**: `references/components/common/ellipsis-text.md` - EllipsisText文本省略展开
### 功能配置 (references/features/)
- **常用功能**: `references/features/features.md` - 水印、缓存、动态标题等
### 构建部署 (references/deployment/)
- **构建部署**: `references/deployment/deploy.md` - 构建配置、Nginx、Docker
- **常见问题**: `references/deployment/faq.md` - 依赖安装、打包部署、错误排查

View File

@ -0,0 +1,150 @@
# Vben Alert 轻量提示框
提供纯 JavaScript 调用的轻量提示框,适合快速创建 `alert`、`confirm`、`prompt` 这类简单交互。
## Alert 提示框
```ts
import { alert } from '@vben/common-ui';
// 基础用法
await alert('操作成功');
// 带标题
await alert('操作成功', '提示');
// 完整配置
await alert({
title: '提示',
content: '操作成功',
icon: 'success', // 'error' | 'info' | 'question' | 'success' | 'warning'
confirmText: '确定',
centered: true,
});
```
## Confirm 确认框
```ts
import { confirm } from '@vben/common-ui';
// 基础用法
const result = await confirm('确定要删除吗?');
if (result) {
// 用户点击确认
}
// 完整配置
const result = await confirm({
title: '确认删除',
content: '删除后数据无法恢复,确定要删除吗?',
icon: 'warning',
confirmText: '删除',
cancelText: '取消',
beforeClose: async ({ isConfirm }) => {
if (isConfirm) {
// 返回 false 阻止关闭
return await doDelete();
}
return true;
},
});
```
## Prompt 输入框
```ts
import { prompt } from '@vben/common-ui';
// 基础用法
const value = await prompt('请输入名称:');
if (value) {
console.log('用户输入:', value);
}
// 带默认值
const value = await prompt({
title: '请输入名称',
defaultValue: '默认名称',
});
// 自定义输入组件
const value = await prompt({
title: '请选择类型',
component: Select,
componentProps: {
options: [
{ label: '类型A', value: 'a' },
{ label: '类型B', value: 'b' },
],
},
defaultValue: 'a',
});
```
## useAlertContext
在自定义组件内获取弹窗上下文:
```vue
<script setup lang="ts">
import { useAlertContext } from '@vben/common-ui';
const { doConfirm, doCancel } = useAlertContext();
function handleConfirm() {
// 触发确认操作
doConfirm();
}
function handleCancel() {
// 触发取消操作
doCancel();
}
</script>
```
## Props 类型
```ts
type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
interface AlertProps {
title?: string;
content: Component | string;
icon?: Component | IconType;
confirmText?: string;
cancelText?: string;
showCancel?: boolean;
centered?: boolean;
bordered?: boolean;
buttonAlign?: 'center' | 'end' | 'start';
overlayBlur?: number;
beforeClose?: (scope: { isConfirm: boolean }) => boolean | Promise<boolean>;
footer?: Component | string;
}
interface PromptProps<T = any> extends AlertProps {
component?: Component;
componentProps?: Record<string, any>;
defaultValue?: T;
modelPropName?: string;
}
```
## 使用场景
- 简单的确认提示
- 删除操作确认
- 快速输入收集
- 不需要复杂布局的弹窗
## 与 Modal 的区别
| 特性 | Alert | Modal |
|------|-------|-------|
| 调用方式 | 纯JS调用 | 组件式 |
| 复杂度 | 简单 | 可复杂 |
| 自定义内容 | 有限 | 完全自定义 |
| 表单支持 | prompt有限 | 完整支持 |
| 适用场景 | 快速确认/提示 | 复杂弹窗业务 |

View File

@ -0,0 +1,205 @@
# Vben ApiComponent API组件包装器
用于包装其它组件,为目标组件提供自动获取远程数据的能力。
## 基础用法
包装 Select 组件,自动获取远程选项:
```vue
<script setup lang="ts">
import { ApiComponent } from '@vben/common-ui';
import { Select } from 'ant-design-vue';
async function fetchOptions() {
const res = await getUserListApi();
return res.data;
}
</script>
<template>
<ApiComponent
v-model="selectedValue"
:api="fetchOptions"
:component="Select"
label-field="name"
value-field="id"
/>
</template>
```
## 包装级联选择器
```vue
<script setup lang="ts">
import { ApiComponent } from '@vben/common-ui';
import { Cascader } from 'ant-design-vue';
async function fetchTreeData() {
const res = await getRegionTreeApi();
return res.data;
}
</script>
<template>
<ApiComponent
v-model="selectedRegion"
:api="fetchTreeData"
:component="Cascader"
:immediate="false"
children-field="children"
loading-slot="suffixIcon"
visible-event="onDropdownVisibleChange"
/>
</template>
```
## 请求参数
```vue
<script setup lang="ts">
const params = ref({ type: 'user' });
async function fetchOptions(params) {
const res = await getOptionsApi(params);
return res.data;
}
</script>
<template>
<ApiComponent
v-model="value"
:api="fetchOptions"
:params="params"
:component="Select"
/>
</template>
```
## 请求前后处理
```vue
<script setup lang="ts">
async function beforeFetch(params) {
// 请求前处理参数
return { ...params, status: 1 };
}
async function afterFetch(data) {
// 请求后处理数据
return data.map(item => ({
...item,
label: `${item.name} (${item.code})`,
}));
}
</script>
<template>
<ApiComponent
v-model="value"
:api="fetchOptions"
:component="Select"
:before-fetch="beforeFetch"
:after-fetch="afterFetch"
/>
</template>
```
## 自动选择选项
```vue
<template>
<!-- 自动选择第一个选项 -->
<ApiComponent
v-model="value"
:api="fetchOptions"
:component="Select"
auto-select="first"
/>
<!-- 有且仅有一个选项时自动选择 -->
<ApiComponent
v-model="value"
:api="fetchOptions"
:component="Select"
auto-select="one"
/>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| modelValue | 当前值 | `any` | - |
| component | 目标组件 | `Component` | - |
| api | 获取数据的函数 | `(arg?) => Promise<any>` | - |
| params | 传递给api的参数 | `object` | - |
| resultField | 从结果中提取数组的字段名 | `string` | - |
| labelField | label字段名 | `string` | `label` |
| valueField | value字段名 | `string` | `value` |
| childrenField | 子级数据字段名 | `string` | - |
| optionsPropName | 目标组件接收options的属性名 | `string` | `options` |
| modelPropName | 目标组件的双向绑定属性名 | `string` | `modelValue` |
| immediate | 是否立即调用api | `boolean` | `true` |
| alwaysLoad | 每次显示时重新请求 | `boolean` | `false` |
| beforeFetch | 请求前的回调 | `(params) => any` | - |
| afterFetch | 请求后的回调 | `(data) => any` | - |
| options | 直接传入选项数据 | `OptionsItem[]` | - |
| visibleEvent | 触发请求的事件名 | `string` | - |
| loadingSlot | 显示loading的插槽名 | `string` | - |
| numberToString | 将value从数字转为string | `boolean` | `false` |
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'` | `false` |
## Methods 方法
| 方法 | 描述 | 类型 |
|------|------|------|
| getComponentRef | 获取被包装组件的实例 | `() => T` |
| updateParam | 设置接口请求参数 | `(params) => void` |
| getOptions | 获取已加载的选项数据 | `() => OptionsItem[]` |
| getValue | 获取当前值 | `() => any` |
## 并发和缓存
使用 Tanstack Query 包装接口请求,实现并发控制和缓存:
```ts
import { useQuery } from '@tanstack/vue-query';
function useUserOptions() {
return useQuery({
queryKey: ['user-options'],
queryFn: () => getUserListApi(),
staleTime: 5 * 60 * 1000, // 5分钟缓存
});
}
```
## 适配器配置
在应用适配器中预包装组件:
```ts
// src/adapter/component.ts
import { ApiComponent } from '@vben/common-ui';
import { Select, TreeSelect } from 'ant-design-vue';
const components = {
ApiSelect: (props, { attrs, slots }) => {
return h(ApiComponent, {
...props,
...attrs,
component: Select,
}, slots);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(ApiComponent, {
...props,
...attrs,
component: TreeSelect,
childrenField: 'children',
}, slots);
},
};
```

View File

@ -0,0 +1,224 @@
# Vben Drawer 抽屉
框架提供的抽屉组件支持自动高度、loading等功能。
## 基础用法
```vue
<script setup lang="ts">
import { useVbenDrawer } from '#/adapter';
const [Drawer, drawerApi] = useVbenDrawer({
title: '标题',
onConfirm: () => {
console.log('确认');
drawerApi.close();
},
});
</script>
<template>
<Button @click="drawerApi.open()">打开</Button>
<Drawer>
抽屉内容
</Drawer>
</template>
```
## 组件抽离
```vue
<!-- parent.vue -->
<script setup lang="ts">
import { useVbenDrawer } from '#/adapter';
import ChildForm from './child-form.vue';
const [Drawer, drawerApi] = useVbenDrawer({
connectedComponent: ChildForm,
onConfirm: () => {
const data = drawerApi.getData();
console.log(data);
},
});
</script>
<template>
<Button @click="drawerApi.open()">打开</Button>
<Drawer />
</template>
<!-- child-form.vue -->
<script setup lang="ts">
import { useVbenDrawer } from '#/adapter';
const [Form, formApi] = useVbenForm({...});
const [Drawer, drawerApi] = useVbenDrawer({
onOpenChange: (isOpen) => {
if (isOpen) {
const data = drawerApi.getData();
formApi.setValues(data);
}
},
onConfirm: async () => {
const values = await formApi.validateAndSubmitForm();
drawerApi.setData(values);
drawerApi.close();
},
});
</script>
```
## 弹出位置
```vue
<script setup lang="ts">
// 左侧弹出
const [Drawer, drawerApi] = useVbenDrawer({
placement: 'left',
});
// 右侧弹出(默认)
const [Drawer, drawerApi] = useVbenDrawer({
placement: 'right',
});
// 顶部弹出
const [Drawer, drawerApi] = useVbenDrawer({
placement: 'top',
});
// 底部弹出
const [Drawer, drawerApi] = useVbenDrawer({
placement: 'bottom',
});
</script>
```
## Loading 状态
```vue
<script setup lang="ts">
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: async () => {
drawerApi.setState({ loading: true });
try {
await saveData();
drawerApi.close();
} finally {
drawerApi.setState({ loading: false });
}
},
});
</script>
```
## Lock 锁定状态
```vue
<script setup lang="ts">
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: async () => {
drawerApi.lock();
try {
await saveData();
drawerApi.close();
} finally {
drawerApi.unlock();
}
},
});
</script>
```
## 挂载到内容区域
```vue
<script setup lang="ts">
const [Drawer, drawerApi] = useVbenDrawer({
appendToMain: true, // 挂载到内容区域,不遮挡导航菜单
});
</script>
<template>
<!-- 需要设置 auto-content-height -->
<Page auto-content-height>
<Drawer />
</Page>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| title | 标题 | `string` | - |
| titleTooltip | 标题提示 | `string` | - |
| description | 描述信息 | `string` | - |
| isOpen | 打开状态 | `boolean` | `false` |
| loading | 加载状态 | `boolean` | `false` |
| closable | 显示关闭按钮 | `boolean` | `true` |
| closeIconPlacement | 关闭按钮位置 | `'left' \| 'right'` | `right` |
| modal | 显示遮罩 | `boolean` | `true` |
| header | 显示header | `boolean` | `true` |
| footer | 显示footer | `boolean` | `true` |
| confirmLoading | 确认按钮loading | `boolean` | `false` |
| closeOnClickModal | 点击遮罩关闭 | `boolean` | `true` |
| closeOnPressEscape | ESC关闭 | `boolean` | `true` |
| confirmText | 确认按钮文本 | `string` | `确认` |
| cancelText | 取消按钮文本 | `string` | `取消` |
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
| placement | 弹出位置 | `'left' \| 'right' \| 'top' \| 'bottom'` | `right` |
| class | drawer的class | `string` | - |
| zIndex | ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
| connectedComponent | 连接组件 | `Component` | - |
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| appendToMain | 挂载到内容区 | `boolean` | `false` |
## Event 事件
| 事件名 | 描述 | 类型 |
|--------|------|------|
| onBeforeClose | 关闭前触发 | `() => boolean \| Promise<boolean>` |
| onCancel | 取消按钮触发 | `() => void` |
| onConfirm | 确认按钮触发 | `() => void` |
| onOpenChange | 打开/关闭时触发 | `(isOpen: boolean) => void` |
| onOpened | 打开动画完毕 | `() => void` |
| onClosed | 关闭动画完毕 | `() => void` |
## 插槽
| 插槽名 | 描述 |
|--------|------|
| default | 抽屉内容 |
| prepend-footer | 取消按钮左侧 |
| center-footer | 取消和确认中间 |
| append-footer | 确认按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
## drawerApi 方法
| 方法 | 描述 | 类型 |
|------|------|------|
| open | 打开抽屉 | `() => void` |
| close | 关闭抽屉 | `() => void` |
| setState | 设置状态 | `(state) => drawerApi` |
| setData | 设置共享数据 | `<T>(data: T) => drawerApi` |
| getData | 获取共享数据 | `<T>() => T` |
| useStore | 获取响应式状态 | - |
| lock | 锁定抽屉 | `(isLock?: boolean) => drawerApi` |
| unlock | 解锁抽屉 | `() => drawerApi` |
## 设置默认属性
```ts
// apps/<app>/src/bootstrap.ts
import { setDefaultDrawerProps } from '@vben/common-ui';
setDefaultDrawerProps({
zIndex: 2000,
placement: 'left',
});
```

View File

@ -0,0 +1,361 @@
# Vben Form 表单
框架提供的表单组件,基于 [vee-validate](https://vee-validate.logaretm.com/v4/) 进行表单验证支持多UI框架适配。
## 基础用法
```vue
<script setup lang="ts">
import { useVbenForm } from '#/adapter/form';
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'Input',
fieldName: 'name',
label: '姓名',
rules: 'required',
},
{
component: 'InputNumber',
fieldName: 'age',
label: '年龄',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
},
],
});
</script>
<template>
<Form />
</template>
```
## 表单提交
```vue
<script setup lang="ts">
import { useVbenForm } from '#/adapter/form';
const [Form, formApi] = useVbenForm({
schema: [...],
handleSubmit: async (values) => {
console.log('提交数据:', values);
// 调用API保存数据
},
handleReset: () => {
console.log('重置表单');
},
});
// 手动提交
async function submit() {
const values = await formApi.validateAndSubmitForm();
console.log(values);
}
</script>
```
## 表单校验
### 预定义规则
```ts
const schema = [
{
component: 'Input',
fieldName: 'name',
label: '姓名',
rules: 'required', // 必填
},
{
component: 'Select',
fieldName: 'type',
label: '类型',
rules: 'selectRequired', // 下拉必选
},
];
```
### Zod 校验
```ts
import { z } from '#/adapter/form';
const schema = [
{
component: 'Input',
fieldName: 'email',
label: '邮箱',
rules: z.string().email({ message: '请输入正确的邮箱' }),
},
{
component: 'Input',
fieldName: 'password',
label: '密码',
rules: z.string().min(6, { message: '密码至少6位' }),
},
{
component: 'Input',
fieldName: 'phone',
label: '手机号',
rules: z.string().regex(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }),
},
];
```
## 表单联动
```ts
const schema = [
{
component: 'Select',
fieldName: 'type',
label: '类型',
componentProps: {
options: [
{ label: '个人', value: 'personal' },
{ label: '企业', value: 'company' },
],
},
},
{
component: 'Input',
fieldName: 'companyName',
label: '企业名称',
dependencies: {
triggerFields: ['type'],
// 显示条件
show: (values) => values.type === 'company',
// 必填条件
required: (values) => values.type === 'company',
// 动态组件参数
componentProps: (values) => ({
placeholder: values.type === 'company' ? '请输入企业名称' : '',
}),
},
},
];
```
## 查询表单
```vue
<script setup lang="ts">
import { useVbenForm } from '#/adapter/form';
const [Form, formApi] = useVbenForm({
schema: [...],
// 查询表单不触发验证
handleSubmit: (values) => {
emit('search', values);
},
// 字段变化时提交(防抖)
submitOnChange: true,
// 显示折叠按钮
showCollapseButton: true,
collapsedRows: 1,
});
</script>
```
## 表单操作
```vue
<script setup lang="ts">
const [Form, formApi] = useVbenForm({...});
// 获取表单值
async function getValues() {
const values = await formApi.getValues();
console.log(values);
}
// 设置表单值
async function setValues() {
await formApi.setValues({
name: '张三',
age: 18,
});
}
// 设置单个字段值
formApi.setFieldValue('name', '李四');
// 重置表单
formApi.resetForm();
// 验证表单
try {
await formApi.validate();
} catch (errors) {
console.log('验证失败:', errors);
}
// 更新schema
formApi.updateSchema([
{
fieldName: 'name',
label: '新标签',
},
]);
// 获取字段组件实例
const inputRef = formApi.getFieldComponentRef('name');
</script>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| layout | 表单布局 | `'horizontal' \| 'vertical' \| 'inline'` | `horizontal` |
| schema | 表单配置 | `FormSchema[]` | - |
| commonConfig | 通用配置 | `FormCommonConfig` | - |
| showDefaultActions | 显示默认操作按钮 | `boolean` | `true` |
| showCollapseButton | 显示折叠按钮 | `boolean` | `false` |
| collapsedRows | 折叠时显示的行数 | `number` | `1` |
| handleSubmit | 提交回调 | `(values) => void` | - |
| handleReset | 重置回调 | `() => void` | - |
| handleValuesChange | 值变化回调 | `(values, fieldsChanged) => void` | - |
| submitOnEnter | 回车提交 | `boolean` | `false` |
| submitOnChange | 字段变化提交 | `boolean` | `false` |
## FormSchema 配置
```ts
interface FormSchema {
component: Component | string; // 组件
componentProps?: object; // 组件参数
defaultValue?: any; // 默认值
dependencies?: FormItemDependencies; // 依赖联动
description?: string; // 描述
fieldName: string; // 字段名
help?: string; // 帮助信息
hide?: boolean; // 隐藏
label?: string; // 标签
rules?: string | ZodSchema; // 校验规则
suffix?: string; // 后缀
}
```
## 组件类型
```ts
type ComponentType =
| 'Input' // 输入框
| 'InputNumber' // 数字输入框
| 'InputPassword' // 密码输入框
| 'Textarea' // 文本域
| 'Select' // 下拉选择
| 'TreeSelect' // 树选择
| 'RadioGroup' // 单选组
| 'CheckboxGroup' // 多选组
| 'Checkbox' // 复选框
| 'Switch' // 开关
| 'DatePicker' // 日期选择
| 'RangePicker' // 日期范围
| 'TimePicker' // 时间选择
| 'Upload' // 上传
| 'Rate' // 评分
| 'AutoComplete' // 自动完成
| 'Divider' // 分割线
| 'Space'; // 间距
```
## 时间字段映射
```ts
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'RangePicker',
fieldName: 'timeRange',
label: '时间范围',
},
],
// 将 timeRange 映射到 startTime 和 endTime
fieldMappingTime: [
['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss'],
],
});
```
## 插槽
| 插槽名 | 描述 |
|--------|------|
| reset-before | 重置按钮之前 |
| submit-before | 提交按钮之前 |
| expand-before | 展开按钮之前 |
| expand-after | 展开按钮之后 |
| {fieldName} | 字段自定义插槽 |
## 自定义组件
```vue
<script setup lang="ts">
import MyCustomComponent from './MyCustomComponent.vue';
const [Form, formApi] = useVbenForm({
schema: [
{
component: MyCustomComponent,
fieldName: 'custom',
label: '自定义组件',
componentProps: {
placeholder: '请输入',
},
},
],
});
</script>
<template>
<Form>
<template #custom="slotProps">
<MyCustomComponent v-bind="slotProps" />
</template>
</Form>
</template>
```
## 适配器配置
```ts
// src/adapter/form.ts
import { setupVbenForm, useVbenForm as useForm } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm({
config: {
baseModelPropName: 'value',
emptyStateValue: null,
modelPropNameMap: {
Checkbox: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
},
});
export const useVbenForm = useForm;
```

View File

@ -0,0 +1,252 @@
# Vben Modal 模态框
框架提供的模态框组件支持拖拽、全屏、自动高度、loading等功能。
## 基础用法
```vue
<script setup lang="ts">
import { useVbenModal } from '#/adapter';
const [Modal, modalApi] = useVbenModal({
title: '标题',
onConfirm: () => {
console.log('确认');
modalApi.close();
},
});
</script>
<template>
<Button @click="modalApi.open()">打开</Button>
<Modal>
弹窗内容
</Modal>
</template>
```
## 组件抽离
```vue
<!-- parent.vue -->
<script setup lang="ts">
import { useVbenModal } from '#/adapter';
import ChildForm from './child-form.vue';
const [Modal, modalApi] = useVbenModal({
connectedComponent: ChildForm,
onConfirm: () => {
const data = modalApi.getData();
console.log(data);
},
});
</script>
<template>
<Button @click="modalApi.open()">打开</Button>
<Modal />
</template>
<!-- child-form.vue -->
<script setup lang="ts">
import { useVbenModal } from '#/adapter';
const [Form, formApi] = useVbenForm({...});
const [Modal, modalApi] = useVbenModal({
onOpenChange: (isOpen) => {
if (isOpen) {
const data = modalApi.getData();
formApi.setValues(data);
}
},
onConfirm: async () => {
const values = await formApi.validateAndSubmitForm();
modalApi.setData(values);
modalApi.close();
},
});
</script>
```
## 拖拽功能
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
draggable: true,
});
</script>
```
## 全屏功能
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
fullscreen: true, // 默认全屏
fullscreenButton: true, // 显示全屏按钮
});
</script>
```
## Loading 状态
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
onConfirm: async () => {
modalApi.setState({ loading: true });
try {
await saveData();
modalApi.close();
} finally {
modalApi.setState({ loading: false });
}
},
});
</script>
```
## Lock 锁定状态
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
onConfirm: async () => {
modalApi.lock();
try {
await saveData();
modalApi.close();
} finally {
modalApi.unlock();
}
},
});
</script>
```
## 动画类型
```vue
<script setup lang="ts">
// 滑动动画(默认)
const [Modal, modalApi] = useVbenModal({
animationType: 'slide',
});
// 缩放动画
const [Modal, modalApi] = useVbenModal({
animationType: 'scale',
});
</script>
```
## 挂载到内容区域
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
appendToMain: true, // 挂载到内容区域,不遮挡导航菜单
});
</script>
<template>
<!-- 需要设置 auto-content-height -->
<Page auto-content-height>
<Modal />
</Page>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| title | 标题 | `string` | - |
| titleTooltip | 标题提示 | `string` | - |
| description | 描述信息 | `string` | - |
| isOpen | 打开状态 | `boolean` | `false` |
| loading | 加载状态 | `boolean` | `false` |
| fullscreen | 全屏显示 | `boolean` | `false` |
| fullscreenButton | 显示全屏按钮 | `boolean` | `true` |
| draggable | 可拖拽 | `boolean` | `false` |
| closable | 显示关闭按钮 | `boolean` | `true` |
| centered | 居中显示 | `boolean` | `false` |
| modal | 显示遮罩 | `boolean` | `true` |
| header | 显示header | `boolean` | `true` |
| footer | 显示footer | `boolean` | `true` |
| confirmLoading | 确认按钮loading | `boolean` | `false` |
| confirmDisabled | 禁用确认按钮 | `boolean` | `false` |
| closeOnClickModal | 点击遮罩关闭 | `boolean` | `true` |
| closeOnPressEscape | ESC关闭 | `boolean` | `true` |
| confirmText | 确认按钮文本 | `string` | `确认` |
| cancelText | 取消按钮文本 | `string` | `取消` |
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
| class | modal的class宽度 | `string` | - |
| contentClass | 内容区class | `string` | - |
| footerClass | 底部区class | `string` | - |
| headerClass | 顶部区class | `string` | - |
| bordered | 显示border | `boolean` | `false` |
| zIndex | ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
| animationType | 动画类型 | `'slide' \| 'scale'` | `slide` |
| connectedComponent | 连接组件 | `Component` | - |
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| appendToMain | 挂载到内容区 | `boolean` | `false` |
## Event 事件
| 事件名 | 描述 | 类型 |
|--------|------|------|
| onBeforeClose | 关闭前触发 | `() => boolean \| Promise<boolean>` |
| onCancel | 取消按钮触发 | `() => void` |
| onConfirm | 确认按钮触发 | `() => void` |
| onOpenChange | 打开/关闭时触发 | `(isOpen: boolean) => void` |
| onOpened | 打开动画完毕 | `() => void` |
| onClosed | 关闭动画完毕 | `() => void` |
## 插槽
| 插槽名 | 描述 |
|--------|------|
| default | 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| center-footer | 取消和确认中间 |
| append-footer | 确认按钮右侧 |
## modalApi 方法
| 方法 | 描述 | 类型 |
|------|------|------|
| open | 打开弹窗 | `() => void` |
| close | 关闭弹窗 | `() => void` |
| setState | 设置状态 | `(state) => modalApi` |
| setData | 设置共享数据 | `<T>(data: T) => modalApi` |
| getData | 获取共享数据 | `<T>() => T` |
| useStore | 获取响应式状态 | - |
| lock | 锁定弹窗 | `(isLock?: boolean) => modalApi` |
| unlock | 解锁弹窗 | `() => modalApi` |
## 设置默认属性
```ts
// apps/<app>/src/bootstrap.ts
import { setDefaultModalProps } from '@vben/common-ui';
setDefaultModalProps({
zIndex: 2000,
draggable: true,
fullscreenButton: false,
});
```
## 设置宽度
```vue
<script setup lang="ts">
const [Modal, modalApi] = useVbenModal({
class: 'w-[600px]', // Tailwind CSS
});
</script>
```

View File

@ -0,0 +1,87 @@
# Page 页面组件
`Page` 是页面内容区最常用的顶层布局容器,内置了标题区、内容区和底部区三部分结构。
## 基础用法
```vue
<script setup lang="ts">
import { Page } from '@vben/common-ui';
</script>
<template>
<Page title="页面标题" description="页面描述">
<!-- 页面内容 -->
</Page>
</template>
```
## 自动高度
```vue
<template>
<!-- 开启自动高度计算,内容区会自动扣减头部和底部高度 -->
<Page title="页面标题" auto-content-height>
<div class="h-full overflow-auto">
<!-- 内容 -->
</div>
</Page>
</template>
```
## 完整示例
```vue
<script setup lang="ts">
import { Page } from '@vben/common-ui';
</script>
<template>
<Page
title="用户管理"
description="管理系统用户信息"
auto-content-height
>
<template #extra>
<Button type="primary">新增用户</Button>
</template>
<!-- 页面内容 -->
<div class="p-4">
<Table />
</div>
<template #footer>
<Pagination />
</template>
</Page>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| title | 页面标题 | `string` | - |
| description | 页面描述 | `string` | - |
| contentClass | 内容区域的class | `string` | - |
| headerClass | 头部区域的class | `string` | - |
| footerClass | 底部区域的class | `string` | - |
| autoContentHeight | 自动计算内容区高度 | `boolean` | `false` |
| heightOffset | 额外扣减的高度偏移量 | `number` | `0` |
## 插槽
| 插槽名 | 描述 |
|--------|------|
| default | 页面内容 |
| title | 页面标题 |
| description | 页面描述 |
| extra | 页面头部右侧内容 |
| footer | 页面底部内容 |
## 注意事项
- 如果 `title`、`description`、`extra` 三者都没有提供有效内容,头部区域不会渲染
- 开启 `autoContentHeight` 时,内容区需要设置 `overflow-auto` 来处理滚动
- 配合 Modal/Drawer 的 `appendToMain` 属性使用时,需要开启 `autoContentHeight`

View File

@ -0,0 +1,266 @@
# Vben Vxe Table 表格
基于 [vxe-table](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 和 `Vben Form` 做了二次封装,用于构建带搜索表单的列表页面。
## 基础用法
```vue
<script setup lang="ts">
import { useVbenVxeGrid } from '#/adapter/vxe-table';
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ type: 'seq', width: 50 },
{ field: 'name', title: '名称' },
{ field: 'age', title: '年龄' },
],
data: [
{ name: '张三', age: 18 },
{ name: '李四', age: 20 },
],
},
});
</script>
<template>
<Grid />
</template>
```
## 远程加载
```vue
<script setup lang="ts">
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getUserListApi } from '#/api';
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ type: 'seq', width: 50 },
{ field: 'name', title: '名称' },
{ field: 'age', title: '年龄' },
],
proxyConfig: {
ajax: {
query: async ({ page }) => {
const res = await getUserListApi({
page: page.currentPage,
pageSize: page.pageSize,
});
return {
items: res.data.list,
total: res.data.total,
};
},
},
},
},
});
</script>
```
## 搜索表单
```vue
<script setup lang="ts">
import { useVbenVxeGrid } from '#/adapter/vxe-table';
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: [
{
component: 'Input',
fieldName: 'name',
label: '名称',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
},
],
},
gridOptions: {
toolbarConfig: {
search: true, // 显示搜索面板开关按钮
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await getUserListApi({
...formValues,
page: page.currentPage,
pageSize: page.pageSize,
});
return res;
},
},
},
columns: [...],
},
});
</script>
```
## 树形表格
```ts
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [...],
treeConfig: {
transform: true,
parentField: 'parentId',
rowField: 'id',
},
},
});
```
## 固定列
```ts
const columns = [
{ field: 'name', title: '名称', fixed: 'left', width: 100 },
{ field: 'age', title: '年龄' },
{ field: 'address', title: '地址' },
{ field: 'action', title: '操作', fixed: 'right', width: 100 },
];
```
## 单元格编辑
```ts
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
editConfig: {
mode: 'cell', // 或 'row'
trigger: 'click',
},
columns: [
{
field: 'name',
title: '名称',
editRender: { name: 'input' },
},
],
},
});
```
## 自定义渲染器
```ts
// 适配器配置
import { h } from 'vue';
import { Image, Button } from 'ant-design-vue';
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(Button, { size: 'small', type: 'link' }, {
default: () => props?.text,
});
},
});
// 使用
const columns = [
{
field: 'avatar',
title: '头像',
cellRender: { name: 'CellImage' },
},
{
field: 'link',
title: '链接',
cellRender: { name: 'CellLink', props: { text: '查看' } },
},
];
```
## GridApi 方法
| 方法名 | 描述 | 类型 |
|--------|------|------|
| setLoading | 设置loading状态 | `(loading: boolean) => void` |
| setGridOptions | 更新gridOptions | `(options) => void` |
| reload | 重新加载,重置分页 | `(params?) => void` |
| query | 重新查询,保留分页 | `(params?) => void` |
| grid | vxe-grid实例 | `VxeGridInstance` |
| formApi | 搜索表单API | `FormApi` |
| toggleSearchForm | 切换搜索表单状态 | `(show?: boolean) => boolean` |
## Props 属性
| 属性名 | 描述 | 类型 |
|--------|------|------|
| tableTitle | 表格标题 | `string` |
| tableTitleHelp | 表格标题帮助信息 | `string` |
| class | 外层容器的class | `string` |
| gridClass | vxe-grid的class | `string` |
| gridOptions | vxe-grid配置 | `VxeTableGridOptions` |
| gridEvents | vxe-grid事件 | `VxeGridListeners` |
| formOptions | 搜索表单配置 | `VbenFormProps` |
| showSearchForm | 是否显示搜索表单 | `boolean` |
| separator | 搜索表单与表格的分隔条 | `boolean \| SeparatorOptions` |
## 插槽
| 插槽名 | 描述 |
|--------|------|
| toolbar-actions | 工具栏左侧区域 |
| toolbar-tools | 工具栏右侧区域 |
| table-title | 自定义表格标题 |
| form-* | 搜索表单插槽转发 |
## 适配器配置
```ts
// src/adapter/vxe-table.ts
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
},
showOverflow: true,
size: 'small',
},
});
},
useVbenForm,
});
export { useVbenVxeGrid };
```

View File

@ -0,0 +1,128 @@
# Vben CountToAnimator 数字动画
用于展示数字滚动动画效果。
## 基础用法
```vue
<script setup lang="ts">
import { CountToAnimator } from '@vben/common-ui';
</script>
<template>
<CountToAnimator :start-val="0" :end-val="2024" :duration="1500" />
</template>
```
## 自定义格式
```vue
<template>
<!-- 带前缀和后缀 -->
<CountToAnimator
:start-val="0"
:end-val="9999"
prefix="¥"
suffix="元"
/>
<!-- 带小数位 -->
<CountToAnimator
:start-val="0"
:end-val="99.99"
:decimals="2"
/>
<!-- 自定义分隔符 -->
<CountToAnimator
:start-val="0"
:end-val="1000000"
separator=","
/>
</template>
```
## 手动控制
```vue
<script setup lang="ts">
import { ref } from 'vue';
import { CountToAnimator } from '@vben/common-ui';
const countRef = ref();
function handleStart() {
countRef.value?.reset();
}
</script>
<template>
<CountToAnimator
ref="countRef"
:start-val="0"
:end-val="9999"
:autoplay="false"
/>
<Button @click="handleStart">开始动画</Button>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| startVal | 起始值 | `number` | `0` |
| endVal | 结束值 | `number` | `2021` |
| duration | 动画持续时间ms | `number` | `1500` |
| autoplay | 是否自动播放 | `boolean` | `true` |
| prefix | 前缀 | `string` | `''` |
| suffix | 后缀 | `string` | `''` |
| separator | 千分位分隔符 | `string` | `','` |
| decimal | 小数点分隔符 | `string` | `'.'` |
| decimals | 保留小数位数 | `number` | `0` |
| color | 文本颜色 | `string` | `''` |
| useEasing | 是否启用过渡预设 | `boolean` | `true` |
| transition | 过渡预设名称 | `string` | `'linear'` |
## Events 事件
| 事件名 | 描述 | 类型 |
|--------|------|------|
| started | 动画开始时触发 | `() => void` |
| finished | 动画结束时触发 | `() => void` |
## Methods 方法
| 方法名 | 描述 | 类型 |
|--------|------|------|
| reset | 重置并重新执行动画 | `() => void` |
## 过渡预设
```vue
<template>
<CountToAnimator
:end-val="1000"
transition="easeOutQuart"
/>
</template>
```
可用的过渡预设:
- `linear`
- `easeInQuad`
- `easeOutQuad`
- `easeInOutQuad`
- `easeInCubic`
- `easeOutCubic`
- `easeInOutCubic`
- `easeOutQuart`
- `easeOutExpo`
- 等等...
## 使用场景
- 统计数据展示
- 仪表盘数字
- 倒计时效果
- 金融数字展示

View File

@ -0,0 +1,113 @@
# Vben EllipsisText 省略文本
用于展示超长文本支持省略、Tooltip 提示以及点击展开收起。
## 基础用法
```vue
<script setup lang="ts">
import { EllipsisText } from '@vben/common-ui';
</script>
<template>
<EllipsisText :max-width="200">
这是一段很长的文本内容,超出部分会被省略显示...
</EllipsisText>
</template>
```
## 可折叠文本
```vue
<template>
<EllipsisText :line="2" expand>
这是一段很长的文本内容默认显示2行点击可以展开查看全部内容。
展开后再次点击可以收起。
</EllipsisText>
</template>
```
## 自定义 Tooltip
```vue
<template>
<EllipsisText>
这是一段文本
<template #tooltip>
<div>自定义提示内容</div>
</template>
</EllipsisText>
</template>
```
## 仅省略时显示 Tooltip
```vue
<template>
<!-- 只有文本被截断时才显示 Tooltip -->
<EllipsisText tooltip-when-ellipsis>
这是一段文本
</EllipsisText>
</template>
```
## Props 属性
| 属性名 | 描述 | 类型 | 默认值 |
|--------|------|------|--------|
| expand | 是否支持点击展开/收起 | `boolean` | `false` |
| line | 文本最大显示行数 | `number` | `1` |
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
| placement | 提示浮层位置 | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` |
| tooltip | 是否启用文本提示 | `boolean` | `true` |
| tooltipWhenEllipsis | 是否仅在文本被截断时显示提示 | `boolean` | `false` |
| ellipsisThreshold | 文本截断检测阈值 | `number` | `3` |
| tooltipBackgroundColor | 提示背景色 | `string` | `''` |
| tooltipColor | 提示文字颜色 | `string` | `''` |
| tooltipFontSize | 提示文字大小px | `number` | `14` |
| tooltipMaxWidth | 提示内容最大宽度px | `number` | - |
| tooltipOverlayStyle | 提示内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
## Events 事件
| 事件名 | 描述 | 类型 |
|--------|------|------|
| expandChange | 展开状态变化时触发 | `(isExpand: boolean) => void` |
## 插槽
| 插槽名 | 描述 |
|--------|------|
| default | 文本内容 |
| tooltip | 自定义提示内容 |
## 表格中使用
```vue
<script setup lang="ts">
import { EllipsisText } from '@vben/common-ui';
const columns = [
{
field: 'description',
title: '描述',
slots: {
default: ({ row }) => {
return h(EllipsisText, {
maxWidth: 200,
line: 2,
expand: true,
}, () => row.description);
},
},
},
];
</script>
```
## 使用场景
- 表格长文本列
- 列表项描述
- 评论内容展示
- 日志信息展示

View File

@ -0,0 +1,158 @@
# 权限控制详细配置
## 权限模式
### 前端访问控制frontend
路由权限在前端固定配置,适合角色较固定的系统。
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
accessMode: 'frontend',
},
});
// 路由配置
{
meta: {
authority: ['super', 'admin'], // 指定角色
},
}
// 登录时设置用户角色
authStore.setUserInfo({
...userInfo,
roles: ['super', 'admin'], // 必须是数组
});
```
### 后端访问控制backend
通过接口动态生成路由表。
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
accessMode: 'backend',
},
});
// src/router/access.ts
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
return await generateAccessible(preferences.app.accessMode, {
fetchMenuListAsync: async () => {
return await getAllMenus(); // 后端返回菜单数据
},
});
}
```
后端菜单数据格式:
```ts
const menus = [
{
name: 'Dashboard',
path: '/dashboard',
component: '/dashboard/index', // 视图路径
meta: {
title: '仪表盘',
icon: 'mdi:view-dashboard',
noBasicLayout: false, // 是否不使用基础布局
},
},
];
```
### 混合访问控制mixed
同时使用前端和后端权限控制。
```ts
export const overridesPreferences = defineOverridesPreferences({
app: {
accessMode: 'mixed',
},
});
```
## 按钮权限控制
### 获取权限码
```ts
// src/store/auth.ts
const accessCodes = await getAccessCodes();
accessStore.setAccessCodes(accessCodes);
// 返回格式: ['AC_100100', 'AC_100110', 'AC_100120']
```
### 组件方式
```vue
<script setup>
import { AccessControl } from '@vben/access';
</script>
<template>
<!-- 权限码方式 -->
<AccessControl :codes="['AC_100100']" type="code">
<Button>有权限可见</Button>
</AccessControl>
<!-- 角色方式 -->
<AccessControl :codes="['super', 'admin']">
<Button>管理员可见</Button>
</AccessControl>
</template>
```
### API 方式
```vue
<script setup>
import { useAccess } from '@vben/access';
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
</script>
<template>
<!-- 权限码判断 -->
<Button v-if="hasAccessByCodes(['AC_100100'])">有权限可见</Button>
<!-- 角色判断 -->
<Button v-if="hasAccessByRoles(['admin'])">管理员可见</Button>
<!-- 多权限满足其一 -->
<Button v-if="hasAccessByCodes(['AC_100100', 'AC_100110'])">
任一权限可见
</Button>
</template>
```
### 指令方式
```vue
<template>
<!-- 权限码指令 -->
<Button v-access:code="'AC_100100'">单个权限码</Button>
<Button v-access:code="['AC_100100', 'AC_100110']">多个权限码</Button>
<!-- 角色指令 -->
<Button v-access:role="'super'">单个角色</Button>
<Button v-access:role="['super', 'admin']">多个角色</Button>
</template>
```
## 菜单可见但禁止访问
```ts
{
meta: {
menuVisibleWithForbidden: true, // 菜单可见访问跳转403
},
}
```

View File

@ -0,0 +1,182 @@
# API 请求与服务端交互
## 请求客户端配置
配置文件位于 `src/api/request.ts`
```ts
import { RequestClient } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({ baseURL });
// 请求拦截器 - 添加 Token
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = `Bearer ${accessStore.accessToken}`;
return config;
},
});
// 响应拦截器 - 处理返回数据
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 0,
}),
);
// 响应拦截器 - Token 过期处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: true,
formatToken: (token) => token ? `Bearer ${token}` : null,
}),
);
// 响应拦截器 - 错误处理
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg, error) => {
message.error(msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
```
## API 定义示例
```ts
// src/api/user.ts
import { requestClient } from '#/api/request';
interface UserInfo {
id: number;
username: string;
realName: string;
roles: string[];
}
// GET 请求
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}
// POST 请求
export async function loginApi(data: { username: string; password: string }) {
return requestClient.post<{ accessToken: string }>('/auth/login', data);
}
// PUT 请求
export async function updateUserApi(user: Partial<UserInfo>) {
return requestClient.put<UserInfo>(`/user/${user.id}`, user);
}
// DELETE 请求
export async function deleteUserApi(id: number) {
return requestClient.delete(`/user/${id}`);
}
// 带参数的 GET 请求
export async function getUserListApi(params: { page: number; size: number }) {
return requestClient.get<{ list: UserInfo[]; total: number }>('/user/list', {
params,
});
}
```
## 扩展配置
```ts
type ExtendOptions = {
// 参数序列化方式
paramsSerializer?: 'brackets' | 'comma' | 'indices' | 'repeat';
// 响应返回方式
// 'raw' - 原始 AxiosResponse
// 'body' - 响应体(不检查 code
// 'data' - 解构后的 data 字段(默认)
responseReturn?: 'body' | 'data' | 'raw';
};
```
## 多接口地址
```ts
const { apiURL, otherApiURL } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
export const requestClient = createRequestClient(apiURL);
export const otherRequestClient = createRequestClient(otherApiURL);
```
## 代理配置
开发环境代理配置在 `vite.config.mts`
```ts
export default defineConfig(async () => {
return {
vite: {
server: {
proxy: {
'/api': {
target: 'http://localhost:5320/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
ws: true,
},
},
},
},
};
});
```
## 刷新 Token
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
enableRefreshToken: true,
},
});
// src/api/request.ts
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
```
## 接口返回格式
默认接口返回格式:
```ts
interface HttpResponse<T = any> {
code: number; // 0 表示成功
data: T;
message: string;
}
```
如需自定义,修改 `defaultResponseInterceptor` 配置。

View File

@ -0,0 +1,126 @@
# 图标使用
框架支持多种图标使用方式。
## Iconify 图标
推荐使用 [Iconify](https://iconify.design/)支持100+图标集。
### 基础用法
```vue
<script setup lang="ts">
import { Icon } from '@vben/icons';
</script>
<template>
<!-- 使用 Iconify 图标 -->
<Icon icon="mdi:home" />
<Icon icon="carbon:user" />
<Icon icon="ant-design:setting" />
<Icon icon="lucide:search" />
</template>
```
### 图标大小
```vue
<template>
<Icon icon="mdi:home" class="size-4" />
<Icon icon="mdi:home" class="size-6" />
<Icon icon="mdi:home" class="size-8" />
</template>
```
### 图标颜色
```vue
<template>
<Icon icon="mdi:home" class="text-primary" />
<Icon icon="mdi:home" class="text-red-500" />
</template>
```
## 路由菜单图标
```ts
const routes = [
{
meta: {
icon: 'mdi:home',
title: '首页',
},
name: 'Home',
path: '/home',
},
{
meta: {
icon: 'carbon:user',
title: '用户管理',
},
name: 'User',
path: '/user',
},
];
```
## 常用图标集
| 图标集 | 前缀 | 示例 |
|--------|------|------|
| Material Design | `mdi:` | `mdi:home` |
| Carbon | `carbon:` | `carbon:user` |
| Ant Design | `ant-design:` | `ant-design:setting` |
| Lucide | `lucide:` | `lucide:search` |
| Font Awesome | `fa:` | `fa:home` |
| Remix Icon | `ri:` | `ri:home-line` |
## SVG 图标
### 全局注册 SVG 图标
```ts
// 将 SVG 文件放入 src/assets/icons/ 目录
// 文件名即为图标名
```
```vue
<template>
<svg-icon name="custom-icon" />
</template>
```
## Tailwind CSS 图标
```vue
<template>
<div class="i-mdi-home"></div>
<div class="i-carbon-user text-xl"></div>
</template>
```
## 图标搜索
- [Iconify 图标搜索](https://icon-sets.iconify.design/)
- [Material Design Icons](https://pictogrammers.com/library/mdi/)
- [Lucide Icons](https://lucide.dev/icons/)
## 自定义图标组件
```vue
<script setup lang="ts">
import { Icon } from '@vben/icons';
defineProps<{
icon: string;
size?: number;
}>();
</script>
<template>
<Icon
:icon="icon"
:class="size ? `size-${size}` : 'size-5'"
/>
</template>
```

View File

@ -0,0 +1,141 @@
# 国际化
项目使用 [Vue i18n](https://kazupon.github.io/vue-i18n/) 进行国际化处理。
## 基础用法
```ts
// 路由配置中使用
import { $t } from '#/locales';
const routes = [
{
meta: {
title: $t('page.home.title'),
},
name: 'Home',
path: '/home',
component: () => import('#/views/home/index.vue'),
},
];
```
```vue
<!-- 组件中使用 -->
<script setup lang="ts">
import { $t } from '#/locales';
</script>
<template>
<div>{{ $t('common.confirm') }}</div>
</template>
```
## 语言配置
`preferences.ts` 中设置默认语言:
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
app: {
locale: 'zh-CN', // 'en-US' | 'zh-CN' | ...
},
});
```
## 支持的语言
```ts
type SupportedLanguagesType =
| 'en-US'
| 'zh-CN'
| 'zh-TW'
| 'ko-KR'
| 'ru-RU'
| 'ja-JP';
```
## 语言包位置
```
packages/locales/
├── langs/ # 语言包文件
│ ├── en-US.json
│ ├── zh-CN.json
│ └── ...
└── src/ # 国际化相关代码
```
## 新增语言包
1. 在 `packages/locales/langs/` 目录下新建语言文件,如 `ja-JP.json`
2. 在 `packages/types/src.ts` 中添加类型定义
3. 在 `preferences.ts` 中配置 `locale: 'ja-JP'`
## 语言包结构示例
```json
{
"common": {
"confirm": "确认",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"search": "搜索",
"reset": "重置"
},
"page": {
"home": {
"title": "首页",
"welcome": "欢迎"
}
},
"ui": {
"formRules": {
"required": "请输入{0}",
"selectRequired": "请选择{0}"
},
"placeholder": {
"input": "请输入",
"select": "请选择"
}
}
}
```
## 远程加载语言包
```ts
// 支持从远程服务器加载语言包
import { setI18nLanguage } from '@vben/locales';
async function loadLocaleMessages(locale: string) {
const messages = await fetch(`/locales/${locale}.json`).then(res => res.json());
setI18nLanguage(locale, messages);
}
```
## 切换语言
框架内置了语言切换组件,可通过偏好设置开启:
```ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
languageToggle: true, // 显示语言切换按钮
},
});
```
## 在代码中切换
```ts
import { useLocale } from '@vben/locales';
const { changeLocale } = useLocale();
// 切换到英文
changeLocale('en-US');
```

View File

@ -0,0 +1,185 @@
# 登录对接
对接自定义后端登录接口。
## 需要实现的接口
### 1. 登录接口
```ts
// src/api/core/auth.ts
import { requestClient } from '#/api/request';
export interface LoginParams {
password: string;
username: string;
}
export interface LoginResult {
accessToken: string;
refreshToken?: string;
}
export async function loginApi(data: LoginParams) {
return requestClient.post<LoginResult>('/auth/login', data);
}
```
### 2. 获取用户信息接口
```ts
// src/api/core/user.ts
import { requestClient } from '#/api/request';
export interface UserInfo {
id: number;
username: string;
realName: string;
avatar: string;
roles: string[];
}
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}
```
### 3. 获取权限码接口(可选)
```ts
// src/api/core/auth.ts
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}
```
### 4. 刷新Token接口可选
```ts
// src/api/core/auth.ts
export async function refreshTokenApi() {
return requestClient.post<{ accessToken: string }>('/auth/refresh-token');
}
```
## 登录页配置
```ts
// src/router/routes/core.ts
const routes = [
{
meta: {
title: 'Login',
},
name: 'Login',
path: '/login',
component: () => import('#/views/_core/authentication/login.vue'),
},
];
```
## 权限模式配置
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
// 前端模式:路由权限在前端定义
// 后端模式:路由从后端获取
// 混合模式:前端定义路由,后端返回权限码
accessMode: 'frontend',
},
});
```
## 登录过期处理
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
// 'page' - 跳转到登录页
// 'modal' - 显示登录过期弹窗
loginExpiredMode: 'page',
},
});
```
## Token刷新配置
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
enableRefreshToken: true,
},
});
// src/api/request.ts
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data.accessToken;
accessStore.setAccessToken(newToken);
return newToken;
}
```
## 自定义登录页布局
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
// 'panel-left' | 'panel-right' | 'panel-top'
authPageLayout: 'panel-right',
},
});
```
## 登录表单配置
登录表单在 `packages/@core/layouts/src/authentication/login.vue` 中定义,可以通过覆盖组件或修改适配器来自定义:
```ts
// 自定义登录表单字段
const loginFormSchema: VbenFormSchema[] = [
{
component: 'Input',
componentProps: {
placeholder: '请输入用户名',
},
fieldName: 'username',
label: '用户名',
rules: 'required',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
rules: 'required',
},
];
```
## 第三方登录
如需对接第三方登录,可在登录页添加第三方登录按钮:
```vue
<template>
<div class="third-party-login">
<Button @click="handleWechatLogin">微信登录</Button>
<Button @click="handleGithubLogin">GitHub登录</Button>
</div>
</template>
<script setup lang="ts">
function handleWechatLogin() {
// 跳转到微信扫码页或打开微信登录弹窗
}
</script>
```

View File

@ -0,0 +1,207 @@
# 偏好设置完整配置
## App 配置
```ts
interface AppPreferences {
accessMode: 'frontend' | 'backend' | 'mixed'; // 权限模式
authPageLayout: AuthPageLayoutType; // 登录页布局
checkUpdatesInterval: number; // 检查更新间隔
colorGrayMode: boolean; // 灰色模式
colorWeakMode: boolean; // 色弱模式
compact: boolean; // 紧凑模式
contentCompact: 'wide' | 'full'; // 内容紧凑模式
contentCompactWidth: number; // 内容宽度
contentPadding: number; // 内容内边距
defaultAvatar: string; // 默认头像
defaultHomePath: string; // 默认首页路径
dynamicTitle: boolean; // 动态标题
enableCheckUpdates: boolean; // 检查更新
enablePreferences: boolean; // 显示偏好设置
enableCopyPreferences: boolean; // 复制偏好设置按钮
enableRefreshToken: boolean; // 刷新Token
isMobile: boolean; // 移动端模式
layout: LayoutType; // 布局方式
locale: 'zh-CN' | 'en-US'; // 语言
loginExpiredMode: 'page' | 'modal'; // 登录过期模式
name: string; // 应用名
preferencesButtonPosition: string; // 偏好设置按钮位置
watermark: boolean; // 水印
zIndex: number; // z-index
}
```
## Theme 配置
```ts
interface ThemePreferences {
builtinType: BuiltinThemeType; // 内置主题
colorDestructive: string; // 错误色
colorPrimary: string; // 主题色
colorSuccess: string; // 成功色
colorWarning: string; // 警告色
mode: 'light' | 'dark'; // 主题模式
radius: string; // 圆角
semiDarkHeader: boolean; // 半深色顶栏
semiDarkSidebar: boolean; // 半深色侧边栏
}
```
内置主题列表:
- `default`, `violet`, `pink`, `rose`, `sky-blue`, `deep-blue`
- `green`, `deep-green`, `orange`, `yellow`
- `zinc`, `neutral`, `slate`, `gray`, `custom`
## Sidebar 配置
```ts
interface SidebarPreferences {
autoActivateChild: boolean; // 点击目录自动激活子菜单
collapsed: boolean; // 折叠状态
collapsedButton: boolean; // 折叠按钮可见
collapsedShowTitle: boolean; // 折叠时显示标题
collapseWidth: number; // 折叠宽度
enable: boolean; // 启用侧边栏
expandOnHover: boolean; // 悬停展开
extraCollapse: boolean; // 扩展区域折叠
extraCollapsedWidth: number; // 扩展区域折叠宽度
fixedButton: boolean; // 固定按钮
hidden: boolean; // 隐藏侧边栏
mixedWidth: number; // 混合布局宽度
width: number; // 侧边栏宽度
}
```
## Tabbar 配置
```ts
interface TabbarPreferences {
draggable: boolean; // 拖拽
enable: boolean; // 启用标签页
height: number; // 高度
keepAlive: boolean; // 缓存
maxCount: number; // 最大数量
middleClickToClose: boolean; // 中键关闭
persist: boolean; // 持久化
showIcon: boolean; // 显示图标
showMaximize: boolean; // 最大化按钮
showMore: boolean; // 更多按钮
styleType: TabsStyleType; // 样式类型
wheelable: boolean; // 滚轮响应
}
```
## Header 配置
```ts
interface HeaderPreferences {
enable: boolean; // 启用顶栏
height: number; // 高度
hidden: boolean; // 隐藏
menuAlign: 'start' | 'center' | 'end'; // 菜单对齐
mode: 'fixed' | 'static'; // 显示模式
}
```
## Breadcrumb 配置
```ts
interface BreadcrumbPreferences {
enable: boolean; // 启用面包屑
hideOnlyOne: boolean; // 仅一个时隐藏
showHome: boolean; // 显示首页
showIcon: boolean; // 显示图标
styleType: 'normal' | 'background'; // 样式
}
```
## Navigation 配置
```ts
interface NavigationPreferences {
accordion: boolean; // 手风琴模式
split: boolean; // 分割mixed-nav布局
styleType: 'rounded' | 'plain'; // 样式
}
```
## Widget 配置
```ts
interface WidgetPreferences {
fullscreen: boolean; // 全屏按钮
globalSearch: boolean; // 全局搜索
languageToggle: boolean; // 语言切换
lockScreen: boolean; // 锁屏
notification: boolean; // 通知
refresh: boolean; // 刷新按钮
sidebarToggle: boolean; // 侧边栏切换
themeToggle: boolean; // 主题切换
}
```
## Copyright 配置
```ts
interface CopyrightPreferences {
companyName: string; // 公司名
companySiteLink: string; // 公司链接
date: string; // 日期
enable: boolean; // 启用版权
icp: string; // 备案号
icpLink: string; // 备案链接
settingShow: boolean; // 设置面板显示
}
```
## Transition 配置
```ts
interface TransitionPreferences {
enable: boolean; // 启用动画
loading: boolean; // 加载动画
name: PageTransitionType; // 动画名称
progress: boolean; // 进度条
}
```
## 配置示例
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
app: {
layout: 'sidebar-nav',
locale: 'zh-CN',
dynamicTitle: true,
accessMode: 'frontend',
defaultHomePath: '/dashboard',
},
theme: {
mode: 'light',
builtinType: 'default',
colorPrimary: 'hsl(212 100% 45%)',
},
sidebar: {
collapsed: false,
width: 224,
},
tabbar: {
enable: true,
keepAlive: true,
styleType: 'chrome',
},
header: {
enable: true,
height: 50,
},
widget: {
fullscreen: true,
refresh: true,
themeToggle: true,
},
});
```
**注意**:修改配置后需清空浏览器缓存才能生效。

View File

@ -0,0 +1,202 @@
# 路由与菜单详细配置
## 路由类型
### 核心路由
框架内置路由,位于 `src/router/routes/core/`包含根路由、登录路由、404路由等。
### 静态路由
位于 `src/router/routes/index/`,项目启动时已确定的路由。
### 动态路由
位于 `src/router/routes/modules/`,根据用户权限动态生成。
## 路由 Meta 配置
```ts
interface RouteMeta {
// 页面标题(必填)
title: string;
// 菜单/标签页图标
icon?: string;
// 激活图标
activeIcon?: string;
// 菜单排序(仅一级菜单有效)
order?: number;
// 开启 KeepAlive 缓存
keepAlive?: boolean;
// 在菜单中隐藏
hideInMenu?: boolean;
// 在标签页中隐藏
hideInTab?: boolean;
// 在面包屑中隐藏
hideInBreadcrumb?: boolean;
// 子菜单在菜单中隐藏
hideChildrenInMenu?: boolean;
// 权限控制
authority?: string[];
// 忽略权限,直接访问
ignoreAccess?: boolean;
// 菜单可见但禁止访问跳转403
menuVisibleWithForbidden?: boolean;
// 固定标签页
affixTab?: boolean;
// 固定标签页顺序
affixTabOrder?: number;
// 标签页最大打开数量
maxNumOfOpenTab?: number;
// 徽标
badge?: string;
// 徽标类型 'dot' | 'normal'
badgeType?: 'dot' | 'normal';
// 徽标颜色
badgeVariants?: 'default' | 'destructive' | 'primary' | 'success' | 'warning';
// 外链跳转路径
link?: string;
// 在新窗口打开
openInNewWindow?: boolean;
// iframe 地址
iframeSrc?: string;
// 激活指定菜单路径
activePath?: string;
// 不使用基础布局
noBasicLayout?: boolean;
// 完整路径作为 tab key
fullPathKey?: boolean;
// DOM 缓存(解决复杂页面切换卡顿)
domCached?: boolean;
}
```
## 新增页面示例
1. 添加路由文件 `src/router/routes/modules/home.ts`
```ts
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'mdi:home',
title: $t('page.home.title'),
order: 1000,
},
name: 'Home',
path: '/home',
redirect: '/home/index',
children: [
{
name: 'HomeIndex',
path: '/home/index',
component: () => import('#/views/home/index.vue'),
meta: {
icon: 'mdi:home',
title: $t('page.home.index'),
},
},
],
},
];
export default routes;
```
2. 添加页面组件 `src/views/home/index.vue`
```vue
<template>
<div>
<h1>Home Page</h1>
</div>
</template>
```
## 多级路由示例
```ts
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:baseline-view-in-ar',
title: '多级菜单',
},
name: 'Nested',
path: '/nested',
redirect: '/nested/menu1',
children: [
{
name: 'Menu1',
path: '/nested/menu1',
component: () => import('#/views/nested/menu1.vue'),
meta: { title: '菜单1' },
},
{
name: 'Menu2',
path: '/nested/menu2',
meta: { title: '菜单2' },
redirect: '/nested/menu2/menu2-1',
children: [
{
name: 'Menu21',
path: '/nested/menu2/menu2-1',
component: () => import('#/views/nested/menu2-1.vue'),
meta: { title: '菜单2-1' },
},
],
},
],
},
];
```
## 路由刷新
```ts
import { useRefresh } from '@vben/hooks';
const { refresh } = useRefresh();
refresh(); // 刷新当前路由
```
## 标签页控制
标签页使用唯一 key 标识,优先级:
1. 路由 query 参数 `pageKey`
2. 路由完整路径(`fullPathKey` 不为 false 时)
3. 路由 path`fullPathKey` 为 false 时)
```ts
// 使用 pageKey 打开多个标签页
router.push({
path: '/detail',
query: { pageKey: 'unique-id' },
});
```

View File

@ -0,0 +1,236 @@
# 主题定制详细配置
## CSS 变量
框架使用 CSS 变量实现主题定制,所有颜色使用 HSL 格式。
### 核心变量
```css
:root {
/* 基础背景色 */
--background: 0 0% 100%;
--background-deep: 216 20.11% 95.47%;
--foreground: 210 6% 21%;
/* 卡片 */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
/* 弹出层 */
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* 静默状态 */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* 主题色 */
--primary: 212 100% 45%;
--primary-foreground: 0 0% 98%;
/* 错误色 */
--destructive: 0 78% 68%;
--destructive-foreground: 0 0% 98%;
/* 成功色 */
--success: 144 57% 58%;
--success-foreground: 0 0% 98%;
/* 警告色 */
--warning: 42 84% 61%;
--warning-foreground: 0 0% 98%;
/* 次要色 */
--secondary: 240 5% 96%;
--secondary-foreground: 240 6% 10%;
/* 强调色 */
--accent: 240 5% 96%;
--accent-hover: 200deg 10% 90%;
--accent-foreground: 240 6% 10%;
/* 边框 */
--border: 240 5.9% 90%;
/* 输入框 */
--input: 240deg 5.88% 90%;
--input-placeholder: 217 10.6% 65%;
--input-background: 0 0% 100%;
/* 圆角 */
--radius: 0.5rem;
/* 遮罩 */
--overlay: 0deg 0% 0% / 30%;
/* 侧边栏 */
--sidebar: 0 0% 100%;
--sidebar-deep: 216 20.11% 95.47%;
/* 顶栏 */
--header: 0 0% 100%;
}
```
### 暗色模式变量
```css
.dark {
--background: 222.34deg 10.43% 12.27%;
--background-deep: 220deg 13.06% 9%;
--foreground: 0 0% 95%;
--card: 222.34deg 10.43% 12.27%;
--card-foreground: 210 40% 98%;
--sidebar: 222.34deg 10.43% 12.27%;
--sidebar-deep: 220deg 13.06% 9%;
--header: 222.34deg 10.43% 12.27%;
--border: 240 3.7% 15.9%;
--input: 0deg 0% 100% / 10%;
}
```
## 修改主题色
```ts
// preferences.ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
theme: {
colorPrimary: 'hsl(212 100% 45%)',
colorSuccess: 'hsl(144 57% 58%)',
colorWarning: 'hsl(42 84% 61%)',
colorDestructive: 'hsl(348 100% 61%)',
},
});
```
## 切换暗色模式
```ts
export const overridesPreferences = defineOverridesPreferences({
theme: {
mode: 'dark', // 'light' | 'dark'
},
});
```
## 内置主题
```ts
export const overridesPreferences = defineOverridesPreferences({
theme: {
builtinType: 'violet', // 使用紫色主题
},
});
```
可用主题:
- `default` - 默认蓝色
- `violet` - 紫色
- `pink` - 粉色
- `rose` - 玫瑰色
- `sky-blue` - 天蓝色
- `deep-blue` - 深蓝色
- `green` - 绿色
- `deep-green` - 深绿色
- `orange` - 橙色
- `yellow` - 黄色
- `zinc` - 锌灰色
- `neutral` - 中性色
- `slate` - 石板色
- `gray` - 灰色
- `custom` - 自定义
## 自定义主题
1. 在 `preferences.ts` 设置:
```ts
export const overridesPreferences = defineOverridesPreferences({
theme: {
builtinType: 'my-theme',
},
});
```
2. 在 CSS 文件中定义变量:
```css
/* light 模式 */
[data-theme='my-theme'] {
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
/* ... 其他变量 */
}
/* dark 模式 */
.dark[data-theme='my-theme'],
[data-theme='my-theme'] .dark {
--primary-foreground: 210 20% 98%;
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
/* ... 其他变量 */
}
```
## 特殊模式
### 灰色模式
```ts
export const overridesPreferences = defineOverridesPreferences({
app: {
colorGrayMode: true,
},
});
```
### 色弱模式
```ts
export const overridesPreferences = defineOverridesPreferences({
app: {
colorWeakMode: true,
},
});
```
## 自定义侧边栏/顶栏颜色
```css
:root {
--sidebar: 0 0% 100%;
--header: 0 0% 100%;
}
.dark {
--sidebar: 222.34deg 10.43% 12.27%;
--header: 222.34deg 10.43% 12.27%;
}
```
## 水印功能
```ts
export const overridesPreferences = defineOverridesPreferences({
app: {
watermark: true,
},
});
// 动态更新水印内容
import { useWatermark } from '@vben/hooks';
const { updateWatermark } = useWatermark();
await updateWatermark({
content: 'hello watermark',
});
```

View File

@ -0,0 +1,186 @@
# 构建与部署
## 构建
### 构建命令
```bash
# 构建所有应用
pnpm build
# 构建指定应用
pnpm build:antd
pnpm build:ele
pnpm build:naive
```
### 环境变量
```bash
# .env.production
VITE_APP_TITLE=Vben Admin
VITE_APP_NAMESPACE=vben-web-antd
VITE_BASE=/
VITE_GLOB_API_URL=https://api.example.com
VITE_COMPRESS=gzip # 压缩方式: none, brotli, gzip
VITE_PWA=false # PWA支持
VITE_ROUTER_HISTORY=hash # 路由模式: hash, history
VITE_INJECT_APP_LOADING=true # 注入全局loading
VITE_ARCHIVER=true # 生成dist.zip
```
## 预览
```bash
# 预览构建结果
pnpm preview
```
## 压缩
```bash
# gzip压缩
VITE_COMPRESS=gzip
# brotli压缩
VITE_COMPRESS=brotli
# 不压缩
VITE_COMPRESS=none
```
## 分析构建
```bash
# 分析构建产物
pnpm analyze
```
## 部署
### Nginx 配置
```nginx
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
# 开启gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1024;
# hash路由配置
location / {
try_files $uri $uri/ /index.html;
}
# history路由配置
# location / {
# try_files $uri $uri/ /index.html;
# }
# API代理
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
### Docker 部署
```dockerfile
# Dockerfile
FROM node:20-alpine as builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build:antd
FROM nginx:alpine
COPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
```yaml
# docker-compose.yml
version: '3'
services:
web:
build: .
ports:
- "80:80"
depends_on:
- backend
backend:
image: your-backend-image
ports:
- "8080:8080"
```
## 动态配置
打包后可通过修改 `_app.config.js` 动态修改配置:
```js
// dist/_app.config.js
window._VBEN_ADMIN_PRO_APP_CONF_ = {
VITE_GLOB_API_URL: 'https://api.example.com',
};
```
## PWA 支持
```bash
# 开启PWA
VITE_PWA=true
```
## 路由模式
```bash
# hash模式默认
VITE_ROUTER_HISTORY=hash
# history模式需要服务器配置
VITE_ROUTER_HISTORY=history
```
## CDN 部署
```bash
# 设置公共资源路径
VITE_BASE=https://cdn.example.com/
```
## 常见问题
### 1. 构建内存溢出
```bash
# 增加Node内存
NODE_OPTIONS=--max_old_space_size=4096 pnpm build
```
### 2. 静态资源404
- 检查 `VITE_BASE` 配置是否正确
- 确保Nginx配置了正确的root路径
### 3. 跨域问题
- 开发环境配置vite proxy
- 生产环境配置Nginx代理或后端开启CORS

View File

@ -0,0 +1,209 @@
# 常见问题
## 问题查找渠道
1. 对应模块的 GitHub 仓库 [issue](https://github.com/vbenjs/vue-vben-admin/issues) 搜索
2. 从 [Google](https://www.google.com) 搜索问题
3. 从 [百度](https://www.baidu.com) 搜索问题
4. 在列表找不到问题可以到 [issues](https://github.com/vbenjs/vue-vben-admin/issues) 提问
5. 需要讨论的问题到 [discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
---
## 依赖问题
### git pull 后依赖更新
在 Monorepo 项目下,需要养成每次 `git pull` 后执行 `pnpm install` 的习惯,因为经常会有新的依赖包加入。项目在 `lefthook.yml` 已配置自动执行,但有时会出现问题,建议手动执行。
### 依赖安装失败
- 尝试执行 `pnpm run reinstall`
- 切换手机热点进行依赖安装
- 配置国内镜像,在项目根目录创建 `.npmrc` 文件:
```bash
# .npmrc
registry = https://registry.npmmirror.com/
```
---
## 缓存更新问题
项目配置默认缓存在 `localStorage` 内,版本更新后可能有些配置没改变。
**解决方式:** 每次更新代码时修改 `package.json` 内的 `version` 版本号。因为 localStorage 的 key 是根据版本号来的,更新后版本不同前面的配置会失效,重新登录即可。
---
## 修改配置文件问题
修改 `.env` 等环境文件以及 `vite.config.ts` 文件时vite 会自动重启服务。自动重启有几率出现问题,请重新运行项目即可解决。
---
## 本地运行报错
由于 vite 在本地没有转换代码,且代码中用到了可选链等比较新的语法,本地开发需要使用版本较高的浏览器(**Chrome 90+**)。
---
## 页面切换后空白
开启路由切换动画,且页面组件存在多个根节点会导致此问题。
**错误示例:**
```vue
<template>
<!-- 注释也算一个节点 -->
<h1>text h1</h1>
<h2>text h2</h2>
</template>
```
**正确示例:**
```vue
<template>
<div>
<h1>text h1</h1>
<h2>text h2</h2>
</div>
</template>
```
> **提示:**
> - 如果想使用多个根标签,可以禁用路由切换动画
> - template 下面的根注释节点也算一个节点
---
## 本地开发正常,打包后不行
排查是否使用了 `ctx` 变量:
```ts
// ❌ 错误用法 - ctx 未暴露在实例类型内Vue 官方不推荐使用
import { getCurrentInstance } from 'vue';
getCurrentInstance().ctx.xxxx;
```
---
## 打包文件过大
- 使用精简版进行开发,完整版引用了较多库文件
- 开启 gzip体积约为原先 1/3
- 可同时开启 brotli 压缩,比 gzip 更好
**注意:**
- `gzip_static` 模块需要 nginx 另外安装,默认未安装
- 开启 `brotli` 也需要 nginx 另外安装模块
---
## 运行错误 - 路径问题
如果出现类似以下错误,请检查项目全路径(包含所有父级路径)**不能出现中文、日文、韩文**
```ts
[vite] Failed to resolve module import "ant-design-vue/dist/antd.css-vben-adminode_modulesant-design-vuedistantd.css"
```
---
## 控制台路由警告
如果页面能正常打开,以下警告可忽略:
```ts
[Vue Router warn]: No match found for location with path "xxxx"
```
后续 `vue-router` 可能会提供配置项来关闭警告。
---
## 启动报错 - Node.js 版本
出现以下错误时,检查 Node.js 版本是否符合要求:
```bash
TypeError: str.matchAll is not a function
```
---
## nginx 部署 MIME 类型问题
部署到 nginx 后,可能出现以下错误:
```bash
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "application/octet-stream".
```
**解决方式一:** nginx 配置
```bash
http {
# 如果有此项配置需要注释掉
# include mime.types;
types {
application/javascript js mjs;
}
}
```
**解决方式二:** 修改 nginx 的 `mime.types` 文件,将 `application/javascript js;` 改为 `application/javascript js mjs;`
---
## 项目更新
### 无法像 npm 插件一样更新
项目是完整的项目模版,不是插件或安装包,无法像插件一样更新。需要根据业务需求二次开发,自行手动合并升级。
### 更新建议
项目采用 Monorepo 方式管理,核心代码如 `packages/@core`、`packages/effects` 已抽离。只要业务代码没有修改这部分代码,可以直接拉取最新代码合并。
**建议:** 关注仓库动态积极合并,不要长时间积累,否则合并冲突过多。
### Git 更新流程
```bash
# 1. 添加公司 git 源地址
git remote add up gitUrl;
# 2. 提交代码到公司
git push up main
# 3. 同步公司代码
git pull up main
# 4. 同步开源最新代码
git pull origin main
```
---
## 移除百度统计代码
在对应应用的 `index.html` 文件中,删除以下代码:
```html
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
```

View File

@ -0,0 +1,297 @@
# 常用功能
## 动态标题
根据页面内容动态更新浏览器标题。
### 开启动态标题
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
dynamicTitle: true,
},
});
```
### 路由配置标题
```ts
const routes = [
{
meta: {
title: '用户管理',
},
name: 'User',
path: '/user',
},
];
```
### 页面内设置标题
```ts
import { useTabbar } from '@vben/tabbar';
const { setTitle } = useTabbar();
// 动态设置标题
setTitle('用户详情 - ID: 123');
```
## 水印
为页面添加水印保护。
### 开启水印
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
watermark: true,
},
});
```
### 自定义水印内容
```vue
<script setup lang="ts">
import { useWatermark } from '@vben/common-ui';
const { setWatermark } = useWatermark();
setWatermark({
content: '机密文档',
fontSize: 16,
color: 'rgba(0, 0, 0, 0.15)',
});
</script>
```
## 页面缓存
保持页面状态,切换路由时不销毁组件。
### 路由级别缓存
```ts
const routes = [
{
meta: {
keepAlive: true, // 开启缓存
},
name: 'UserList',
path: '/user/list',
},
];
```
### 全局配置
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
tabbar: {
enable: true,
keepAlive: true, // 全局开启缓存
persist: true, // 持久化标签页
},
});
```
## 页面加载进度条
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
transition: {
progress: true, // 显示页面加载进度条
loading: true, // 显示页面加载动画
},
});
```
## 页面过渡动画
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
transition: {
enable: true,
name: 'fade-slide', // 动画名称
},
});
```
### 可选动画
- `fade` - 淡入淡出
- `fade-slide` - 滑动淡入淡出
- `fade-bottom` - 底部滑入
- `fade-scale` - 缩放淡入
## 面包屑
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
breadcrumb: {
enable: true, // 显示面包屑
showHome: false, // 显示首页图标
showIcon: true, // 显示图标
hideOnlyOne: false, // 只有一个时隐藏
styleType: 'normal', // 样式类型
},
});
```
## 标签页
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
tabbar: {
enable: true, // 显示标签页
height: 38, // 标签页高度
showIcon: true, // 显示图标
showMore: true, // 显示更多按钮
showMaximize: true, // 显示最大化按钮
draggable: true, // 可拖拽
wheelable: true, // 滚轮切换
persist: true, // 持久化
keepAlive: true, // 缓存
maxCount: 0, // 最大数量0不限制
styleType: 'chrome', // 样式chrome | plain | card
},
});
```
## 页脚版权
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
footer: {
enable: true, // 显示页脚
fixed: false, // 固定在底部
height: 32, // 高度
},
copyright: {
enable: true,
companyName: 'My Company',
companySiteLink: 'https://example.com',
date: '2024',
icp: '备案号',
icpLink: 'https://beian.miit.gov.cn',
},
});
```
## 锁屏
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
lockScreen: true, // 显示锁屏按钮
},
shortcutKeys: {
globalLockScreen: true, // 锁屏快捷键
},
});
```
## 全局搜索
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
globalSearch: true, // 显示搜索按钮
},
shortcutKeys: {
globalSearch: true, // 搜索快捷键
},
});
```
## 通知中心
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
notification: true, // 显示通知图标
},
});
```
## 全屏
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
fullscreen: true, // 显示全屏按钮
},
});
```
## 刷新按钮
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
refresh: true, // 显示刷新按钮
},
});
```
## 主题切换
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
widget: {
themeToggle: true, // 显示主题切换按钮
},
});
```
## 检查更新
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
enableCheckUpdates: true, // 开启检查更新
checkUpdatesInterval: 1, // 检查间隔(小时)
},
});
```
## 色弱模式
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
colorWeakMode: true,
},
});
```
## 灰色模式
```ts
// preferences.ts
export const overridesPreferences = defineOverridesPreferences({
app: {
colorGrayMode: true,
},
});
```