mirror of
https://github.com/KwiTsukasa/kt-template-admin.git
synced 2026-05-27 16:35:47 +08:00
feat: skills
This commit is contained in:
parent
6ecada0d3d
commit
d0e212a283
297
skills/SKILL.md
Normal file
297
skills/SKILL.md
Normal 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` - 依赖安装、打包部署、错误排查
|
||||
150
skills/references/components/business/alert.md
Normal file
150
skills/references/components/business/alert.md
Normal 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有限 | 完整支持 |
|
||||
| 适用场景 | 快速确认/提示 | 复杂弹窗业务 |
|
||||
205
skills/references/components/business/api-component.md
Normal file
205
skills/references/components/business/api-component.md
Normal 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);
|
||||
},
|
||||
};
|
||||
```
|
||||
224
skills/references/components/business/drawer.md
Normal file
224
skills/references/components/business/drawer.md
Normal 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',
|
||||
});
|
||||
```
|
||||
361
skills/references/components/business/form.md
Normal file
361
skills/references/components/business/form.md
Normal 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;
|
||||
```
|
||||
252
skills/references/components/business/modal.md
Normal file
252
skills/references/components/business/modal.md
Normal 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>
|
||||
```
|
||||
87
skills/references/components/business/page.md
Normal file
87
skills/references/components/business/page.md
Normal 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`
|
||||
266
skills/references/components/business/table.md
Normal file
266
skills/references/components/business/table.md
Normal 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 };
|
||||
```
|
||||
128
skills/references/components/common/count-to.md
Normal file
128
skills/references/components/common/count-to.md
Normal 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`
|
||||
- 等等...
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 统计数据展示
|
||||
- 仪表盘数字
|
||||
- 倒计时效果
|
||||
- 金融数字展示
|
||||
113
skills/references/components/common/ellipsis-text.md
Normal file
113
skills/references/components/common/ellipsis-text.md
Normal 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>
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 表格长文本列
|
||||
- 列表项描述
|
||||
- 评论内容展示
|
||||
- 日志信息展示
|
||||
158
skills/references/core/access.md
Normal file
158
skills/references/core/access.md
Normal 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
|
||||
},
|
||||
}
|
||||
```
|
||||
182
skills/references/core/api.md
Normal file
182
skills/references/core/api.md
Normal 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` 配置。
|
||||
126
skills/references/core/icons.md
Normal file
126
skills/references/core/icons.md
Normal 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>
|
||||
```
|
||||
141
skills/references/core/locale.md
Normal file
141
skills/references/core/locale.md
Normal 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');
|
||||
```
|
||||
185
skills/references/core/login.md
Normal file
185
skills/references/core/login.md
Normal 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>
|
||||
```
|
||||
207
skills/references/core/preferences.md
Normal file
207
skills/references/core/preferences.md
Normal 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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**注意**:修改配置后需清空浏览器缓存才能生效。
|
||||
202
skills/references/core/route.md
Normal file
202
skills/references/core/route.md
Normal 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' },
|
||||
});
|
||||
```
|
||||
236
skills/references/core/theme.md
Normal file
236
skills/references/core/theme.md
Normal 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',
|
||||
});
|
||||
```
|
||||
186
skills/references/deployment/deploy.md
Normal file
186
skills/references/deployment/deploy.md
Normal 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
|
||||
209
skills/references/deployment/faq.md
Normal file
209
skills/references/deployment/faq.md
Normal 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>
|
||||
```
|
||||
297
skills/references/features/features.md
Normal file
297
skills/references/features/features.md
Normal 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,
|
||||
},
|
||||
});
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user