From d0e212a283ea2cb504f14612cc94ba98afe4f189 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 16 May 2026 17:30:35 +0800 Subject: [PATCH] feat: skills --- skills/SKILL.md | 297 ++++++++++++++ .../references/components/business/alert.md | 150 ++++++++ .../components/business/api-component.md | 205 ++++++++++ .../references/components/business/drawer.md | 224 +++++++++++ skills/references/components/business/form.md | 361 ++++++++++++++++++ .../references/components/business/modal.md | 252 ++++++++++++ skills/references/components/business/page.md | 87 +++++ .../references/components/business/table.md | 266 +++++++++++++ .../references/components/common/count-to.md | 128 +++++++ .../components/common/ellipsis-text.md | 113 ++++++ skills/references/core/access.md | 158 ++++++++ skills/references/core/api.md | 182 +++++++++ skills/references/core/icons.md | 126 ++++++ skills/references/core/locale.md | 141 +++++++ skills/references/core/login.md | 185 +++++++++ skills/references/core/preferences.md | 207 ++++++++++ skills/references/core/route.md | 202 ++++++++++ skills/references/core/theme.md | 236 ++++++++++++ skills/references/deployment/deploy.md | 186 +++++++++ skills/references/deployment/faq.md | 209 ++++++++++ skills/references/features/features.md | 297 ++++++++++++++ 21 files changed, 4212 insertions(+) create mode 100644 skills/SKILL.md create mode 100644 skills/references/components/business/alert.md create mode 100644 skills/references/components/business/api-component.md create mode 100644 skills/references/components/business/drawer.md create mode 100644 skills/references/components/business/form.md create mode 100644 skills/references/components/business/modal.md create mode 100644 skills/references/components/business/page.md create mode 100644 skills/references/components/business/table.md create mode 100644 skills/references/components/common/count-to.md create mode 100644 skills/references/components/common/ellipsis-text.md create mode 100644 skills/references/core/access.md create mode 100644 skills/references/core/api.md create mode 100644 skills/references/core/icons.md create mode 100644 skills/references/core/locale.md create mode 100644 skills/references/core/login.md create mode 100644 skills/references/core/preferences.md create mode 100644 skills/references/core/route.md create mode 100644 skills/references/core/theme.md create mode 100644 skills/references/deployment/deploy.md create mode 100644 skills/references/deployment/faq.md create mode 100644 skills/references/features/features.md diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 0000000..7524c87 --- /dev/null +++ b/skills/SKILL.md @@ -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 + + + +``` + +### 偏好设置配置 + +在应用目录的 `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('/user/info'); +} + +// POST 请求 +export async function saveUserApi(user: UserInfo) { + return requestClient.post('/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('/auth/login', data); +} + +// src/api/core/user.ts +export async function getUserInfoApi() { + return requestClient.get('/user/info'); +} + +// src/api/core/auth.ts (可选) +export async function getAccessCodesApi() { + return requestClient.get('/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` - 依赖安装、打包部署、错误排查 diff --git a/skills/references/components/business/alert.md b/skills/references/components/business/alert.md new file mode 100644 index 0000000..8a77a7b --- /dev/null +++ b/skills/references/components/business/alert.md @@ -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 + +``` + +## 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; + footer?: Component | string; +} + +interface PromptProps extends AlertProps { + component?: Component; + componentProps?: Record; + defaultValue?: T; + modelPropName?: string; +} +``` + +## 使用场景 + +- 简单的确认提示 +- 删除操作确认 +- 快速输入收集 +- 不需要复杂布局的弹窗 + +## 与 Modal 的区别 + +| 特性 | Alert | Modal | +|------|-------|-------| +| 调用方式 | 纯JS调用 | 组件式 | +| 复杂度 | 简单 | 可复杂 | +| 自定义内容 | 有限 | 完全自定义 | +| 表单支持 | prompt有限 | 完整支持 | +| 适用场景 | 快速确认/提示 | 复杂弹窗业务 | diff --git a/skills/references/components/business/api-component.md b/skills/references/components/business/api-component.md new file mode 100644 index 0000000..9d81085 --- /dev/null +++ b/skills/references/components/business/api-component.md @@ -0,0 +1,205 @@ +# Vben ApiComponent API组件包装器 + +用于包装其它组件,为目标组件提供自动获取远程数据的能力。 + +## 基础用法 + +包装 Select 组件,自动获取远程选项: + +```vue + + + +``` + +## 包装级联选择器 + +```vue + + + +``` + +## 请求参数 + +```vue + + + +``` + +## 请求前后处理 + +```vue + + + +``` + +## 自动选择选项 + +```vue + +``` + +## Props 属性 + +| 属性名 | 描述 | 类型 | 默认值 | +|--------|------|------|--------| +| modelValue | 当前值 | `any` | - | +| component | 目标组件 | `Component` | - | +| api | 获取数据的函数 | `(arg?) => Promise` | - | +| 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); + }, +}; +``` diff --git a/skills/references/components/business/drawer.md b/skills/references/components/business/drawer.md new file mode 100644 index 0000000..0287cb6 --- /dev/null +++ b/skills/references/components/business/drawer.md @@ -0,0 +1,224 @@ +# Vben Drawer 抽屉 + +框架提供的抽屉组件,支持自动高度、loading等功能。 + +## 基础用法 + +```vue + + + +``` + +## 组件抽离 + +```vue + + + + + + + +``` + +## 弹出位置 + +```vue + +``` + +## Loading 状态 + +```vue + +``` + +## Lock 锁定状态 + +```vue + +``` + +## 挂载到内容区域 + +```vue + + + +``` + +## 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` | +| 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 | 设置共享数据 | `(data: T) => drawerApi` | +| getData | 获取共享数据 | `() => T` | +| useStore | 获取响应式状态 | - | +| lock | 锁定抽屉 | `(isLock?: boolean) => drawerApi` | +| unlock | 解锁抽屉 | `() => drawerApi` | + +## 设置默认属性 + +```ts +// apps//src/bootstrap.ts +import { setDefaultDrawerProps } from '@vben/common-ui'; + +setDefaultDrawerProps({ + zIndex: 2000, + placement: 'left', +}); +``` diff --git a/skills/references/components/business/form.md b/skills/references/components/business/form.md new file mode 100644 index 0000000..64b5441 --- /dev/null +++ b/skills/references/components/business/form.md @@ -0,0 +1,361 @@ +# Vben Form 表单 + +框架提供的表单组件,基于 [vee-validate](https://vee-validate.logaretm.com/v4/) 进行表单验证,支持多UI框架适配。 + +## 基础用法 + +```vue + + + +``` + +## 表单提交 + +```vue + +``` + +## 表单校验 + +### 预定义规则 + +```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 + +``` + +## 表单操作 + +```vue + +``` + +## 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 + + + +``` + +## 适配器配置 + +```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; +``` diff --git a/skills/references/components/business/modal.md b/skills/references/components/business/modal.md new file mode 100644 index 0000000..bd7ecd6 --- /dev/null +++ b/skills/references/components/business/modal.md @@ -0,0 +1,252 @@ +# Vben Modal 模态框 + +框架提供的模态框组件,支持拖拽、全屏、自动高度、loading等功能。 + +## 基础用法 + +```vue + + + +``` + +## 组件抽离 + +```vue + + + + + + + +``` + +## 拖拽功能 + +```vue + +``` + +## 全屏功能 + +```vue + +``` + +## Loading 状态 + +```vue + +``` + +## Lock 锁定状态 + +```vue + +``` + +## 动画类型 + +```vue + +``` + +## 挂载到内容区域 + +```vue + + + +``` + +## 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` | +| 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 | 设置共享数据 | `(data: T) => modalApi` | +| getData | 获取共享数据 | `() => T` | +| useStore | 获取响应式状态 | - | +| lock | 锁定弹窗 | `(isLock?: boolean) => modalApi` | +| unlock | 解锁弹窗 | `() => modalApi` | + +## 设置默认属性 + +```ts +// apps//src/bootstrap.ts +import { setDefaultModalProps } from '@vben/common-ui'; + +setDefaultModalProps({ + zIndex: 2000, + draggable: true, + fullscreenButton: false, +}); +``` + +## 设置宽度 + +```vue + +``` diff --git a/skills/references/components/business/page.md b/skills/references/components/business/page.md new file mode 100644 index 0000000..6c8b604 --- /dev/null +++ b/skills/references/components/business/page.md @@ -0,0 +1,87 @@ +# Page 页面组件 + +`Page` 是页面内容区最常用的顶层布局容器,内置了标题区、内容区和底部区三部分结构。 + +## 基础用法 + +```vue + + + +``` + +## 自动高度 + +```vue + +``` + +## 完整示例 + +```vue + + + +``` + +## 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` diff --git a/skills/references/components/business/table.md b/skills/references/components/business/table.md new file mode 100644 index 0000000..2c83bbe --- /dev/null +++ b/skills/references/components/business/table.md @@ -0,0 +1,266 @@ +# Vben Vxe Table 表格 + +基于 [vxe-table](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 和 `Vben Form` 做了二次封装,用于构建带搜索表单的列表页面。 + +## 基础用法 + +```vue + + + +``` + +## 远程加载 + +```vue + +``` + +## 搜索表单 + +```vue + +``` + +## 树形表格 + +```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 }; +``` diff --git a/skills/references/components/common/count-to.md b/skills/references/components/common/count-to.md new file mode 100644 index 0000000..cc683cd --- /dev/null +++ b/skills/references/components/common/count-to.md @@ -0,0 +1,128 @@ +# Vben CountToAnimator 数字动画 + +用于展示数字滚动动画效果。 + +## 基础用法 + +```vue + + + +``` + +## 自定义格式 + +```vue + +``` + +## 手动控制 + +```vue + + + +``` + +## 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 + +``` + +可用的过渡预设: +- `linear` +- `easeInQuad` +- `easeOutQuad` +- `easeInOutQuad` +- `easeInCubic` +- `easeOutCubic` +- `easeInOutCubic` +- `easeOutQuart` +- `easeOutExpo` +- 等等... + +## 使用场景 + +- 统计数据展示 +- 仪表盘数字 +- 倒计时效果 +- 金融数字展示 diff --git a/skills/references/components/common/ellipsis-text.md b/skills/references/components/common/ellipsis-text.md new file mode 100644 index 0000000..06bf0a5 --- /dev/null +++ b/skills/references/components/common/ellipsis-text.md @@ -0,0 +1,113 @@ +# Vben EllipsisText 省略文本 + +用于展示超长文本,支持省略、Tooltip 提示以及点击展开收起。 + +## 基础用法 + +```vue + + + +``` + +## 可折叠文本 + +```vue + +``` + +## 自定义 Tooltip + +```vue + +``` + +## 仅省略时显示 Tooltip + +```vue + +``` + +## 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 + +``` + +## 使用场景 + +- 表格长文本列 +- 列表项描述 +- 评论内容展示 +- 日志信息展示 diff --git a/skills/references/core/access.md b/skills/references/core/access.md new file mode 100644 index 0000000..ecb5f33 --- /dev/null +++ b/skills/references/core/access.md @@ -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 + + + +``` + +### API 方式 + +```vue + + + +``` + +### 指令方式 + +```vue + +``` + +## 菜单可见但禁止访问 + +```ts +{ + meta: { + menuVisibleWithForbidden: true, // 菜单可见,访问跳转403 + }, +} +``` diff --git a/skills/references/core/api.md b/skills/references/core/api.md new file mode 100644 index 0000000..5c198ed --- /dev/null +++ b/skills/references/core/api.md @@ -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('/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) { + return requestClient.put(`/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 { + code: number; // 0 表示成功 + data: T; + message: string; +} +``` + +如需自定义,修改 `defaultResponseInterceptor` 配置。 diff --git a/skills/references/core/icons.md b/skills/references/core/icons.md new file mode 100644 index 0000000..f022237 --- /dev/null +++ b/skills/references/core/icons.md @@ -0,0 +1,126 @@ +# 图标使用 + +框架支持多种图标使用方式。 + +## Iconify 图标 + +推荐使用 [Iconify](https://iconify.design/),支持100+图标集。 + +### 基础用法 + +```vue + + + +``` + +### 图标大小 + +```vue + +``` + +### 图标颜色 + +```vue + +``` + +## 路由菜单图标 + +```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 + +``` + +## Tailwind CSS 图标 + +```vue + +``` + +## 图标搜索 + +- [Iconify 图标搜索](https://icon-sets.iconify.design/) +- [Material Design Icons](https://pictogrammers.com/library/mdi/) +- [Lucide Icons](https://lucide.dev/icons/) + +## 自定义图标组件 + +```vue + + + +``` diff --git a/skills/references/core/locale.md b/skills/references/core/locale.md new file mode 100644 index 0000000..210531c --- /dev/null +++ b/skills/references/core/locale.md @@ -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 + + + + +``` + +## 语言配置 + +在 `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'); +``` diff --git a/skills/references/core/login.md b/skills/references/core/login.md new file mode 100644 index 0000000..dd31a26 --- /dev/null +++ b/skills/references/core/login.md @@ -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('/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('/user/info'); +} +``` + +### 3. 获取权限码接口(可选) + +```ts +// src/api/core/auth.ts +export async function getAccessCodesApi() { + return requestClient.get('/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 + + + +``` diff --git a/skills/references/core/preferences.md b/skills/references/core/preferences.md new file mode 100644 index 0000000..880533c --- /dev/null +++ b/skills/references/core/preferences.md @@ -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, + }, +}); +``` + +**注意**:修改配置后需清空浏览器缓存才能生效。 diff --git a/skills/references/core/route.md b/skills/references/core/route.md new file mode 100644 index 0000000..9e025d4 --- /dev/null +++ b/skills/references/core/route.md @@ -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 + +``` + +## 多级路由示例 + +```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' }, +}); +``` diff --git a/skills/references/core/theme.md b/skills/references/core/theme.md new file mode 100644 index 0000000..a74cfbf --- /dev/null +++ b/skills/references/core/theme.md @@ -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', +}); +``` diff --git a/skills/references/deployment/deploy.md b/skills/references/deployment/deploy.md new file mode 100644 index 0000000..abf52b9 --- /dev/null +++ b/skills/references/deployment/deploy.md @@ -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 diff --git a/skills/references/deployment/faq.md b/skills/references/deployment/faq.md new file mode 100644 index 0000000..b63d2e3 --- /dev/null +++ b/skills/references/deployment/faq.md @@ -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 + +``` + +**正确示例:** + +```vue + +``` + +> **提示:** +> - 如果想使用多个根标签,可以禁用路由切换动画 +> - 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 + +``` diff --git a/skills/references/features/features.md b/skills/references/features/features.md new file mode 100644 index 0000000..003b69a --- /dev/null +++ b/skills/references/features/features.md @@ -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 + +``` + +## 页面缓存 + +保持页面状态,切换路由时不销毁组件。 + +### 路由级别缓存 + +```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, + }, +}); +```