flowable-devops/frontend/apps/web-antd/CLAUDE.md
dengqichen d42166d2c0 提交
2025-10-13 16:25:13 +08:00

814 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vben Admin Web-Antd AI 开发规则手册
## 项目概述
本项目基于 **Vben Admin 5.x** 框架开发,使用 Vue 3 + TypeScript + Ant Design Vue。项目采用 **monorepo** 结构,当前应用位于 `apps/web-antd`,依赖 `packages` 目录下的框架核心包。
### 技术栈
- **框架**: Vue 3.5+ (Composition API)
- **语言**: TypeScript 5.8+
- **UI 库**: Ant Design Vue 4.x
- **状态管理**: Pinia
- **路由**: Vue Router 4.x
- **构建工具**: Vite 6.x
- **包管理器**: pnpm (必须使用,不能用 npm/yarn)
- **图标**: Iconify + Lucide
---
## 项目结构
```
apps/web-antd/
├── src/
│ ├── api/ # API 请求封装
│ │ ├── core/ # 核心 API (auth, menu, user)
│ │ ├── request.ts # 请求客户端配置
│ │ └── index.ts # 导出所有 API
│ ├── router/ # 路由配置
│ │ ├── routes/ # 路由定义
│ │ │ ├── core/ # 核心路由 (login, 404等)
│ │ │ └── modules/ # 业务路由模块
│ │ └── index.ts # 路由实例
│ ├── store/ # Pinia 状态管理
│ ├── views/ # 页面组件
│ │ ├── _core/ # 核心页面 (login, 404)
│ │ ├── dashboard/ # 仪表盘
│ │ └── demos/ # 示例页面
│ ├── layouts/ # 布局组件(如需自定义)
│ ├── locales/ # 国际化配置
│ ├── adapter/ # 组件适配器
│ ├── app.vue # 根组件
│ ├── main.ts # 入口文件
│ └── preferences.ts # 应用偏好设置
├── package.json
└── vite.config.ts
```
---
## 核心开发规范
### 1. 命名规范
#### 文件命名
- **组件文件**: PascalCase`UserProfile.vue`, `DataTable.vue`
- **工具文件**: kebab-case`format-date.ts`, `use-table.ts`
- **API 文件**: kebab-case`user-api.ts`, `order-api.ts`
- **路由文件**: kebab-case`user-management.ts`
#### 变量命名
- **组件变量**: PascalCase`const UserTable = defineComponent(...)`
- **普通变量**: camelCase`const userInfo = ref({})`
- **常量**: UPPER_SNAKE_CASE`const API_BASE_URL = '...'`
- **类型/接口**: PascalCase`interface UserInfo { ... }`
### 2. Vue 组件规范
**必须使用 Composition API + `<script setup>`**:
```vue
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import type { UserInfo } from '#/api/types';
// 定义组件名(可选,但推荐)
defineOptions({ name: 'UserProfile' });
// Props 定义
interface Props {
userId: string;
showActions?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showActions: true,
});
// Emits 定义
const emit = defineEmits<{
update: [user: UserInfo];
delete: [id: string];
}>();
// 响应式数据
const loading = ref(false);
const userInfo = ref<UserInfo | null>(null);
// 计算属性
const displayName = computed(() => {
return userInfo.value?.name || 'Unknown';
});
// 方法
async function fetchUser() {
loading.value = true;
try {
const data = await getUserApi(props.userId);
userInfo.value = data;
} finally {
loading.value = false;
}
}
// 生命周期
onMounted(() => {
fetchUser();
});
</script>
<template>
<div class="user-profile">
<a-spin :spinning="loading">
<h3>{{ displayName }}</h3>
</a-spin>
</div>
</template>
<style scoped>
.user-profile {
padding: 16px;
}
</style>
```
### 3. 导入路径别名
使用 `#/` 别名指向 `src/` 目录:
```typescript
// ✅ 正确
import { useAuthStore } from '#/store';
import type { UserInfo } from '#/api/types';
import { $t } from '#/locales';
// ❌ 错误 - 不要使用相对路径
import { useAuthStore } from '../../../store';
```
### 4. 框架组件导入
框架提供的组件从 `@vben/*` 包导入:
```typescript
// UI 组件
import { Button, Modal, Drawer } from '@vben/common-ui';
import { Page, Card } from '@vben/common-ui';
import { VbenForm, VbenTable } from '@vben/common-ui';
// 布局组件
import { BasicLayout, PageWrapper } from '@vben/layouts';
// 图标
import { SvgIcon, IconSvg } from '@vben/icons';
// Hooks
import { usePreferences, useAppConfig } from '@vben/hooks';
// 工具函数
import { formatDate, formatMoney } from '@vben/utils';
// 权限
import { useAccess } from '@vben/access';
// Store
import { useAccessStore, useUserStore } from '@vben/stores';
// 请求
import { RequestClient } from '@vben/request';
```
---
## 开发模式详解
### 1. 创建新页面
#### 步骤 1: 创建视图组件
`src/views/` 下创建页面组件:
```vue
<!-- src/views/user/user-list.vue -->
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { VbenTable } from '@vben/common-ui';
import type { VbenTableColumn } from '@vben/common-ui';
import { getUserListApi } from '#/api/user';
defineOptions({ name: 'UserList' });
const loading = ref(false);
const dataSource = ref([]);
const columns: VbenTableColumn[] = [
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '状态', dataIndex: 'status', key: 'status' },
];
async function fetchData() {
loading.value = true;
try {
const data = await getUserListApi();
dataSource.value = data;
} finally {
loading.value = false;
}
}
onMounted(() => {
fetchData();
});
</script>
<template>
<div class="p-5">
<VbenTable
:columns="columns"
:data-source="dataSource"
:loading="loading"
/>
</div>
</template>
```
#### 步骤 2: 添加路由配置
`src/router/routes/modules/` 下创建路由模块:
```typescript
// src/router/routes/modules/user.ts
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:users',
title: '用户管理',
order: 10, // 菜单排序
},
name: 'User',
path: '/user',
children: [
{
name: 'UserList',
path: '/user/list',
component: () => import('#/views/user/user-list.vue'),
meta: {
icon: 'lucide:user-cog',
title: '用户列表',
},
},
{
name: 'UserDetail',
path: '/user/detail/:id',
component: () => import('#/views/user/user-detail.vue'),
meta: {
hideInMenu: true, // 不在菜单中显示
title: '用户详情',
activeMenu: '/user/list', // 激活父菜单
},
},
],
},
];
export default routes;
```
**路由配置要点:**
- 必须 `export default routes`
- `component` 使用动态导入 `() => import(...)`
- 使用 `#/views/` 别名
- 图标使用 Iconify 格式,如 `lucide:users`
- `meta.order` 控制菜单顺序(数字越小越靠前)
#### 步骤 3: 添加国际化(可选)
```typescript
// src/locales/lang/zh-CN/user.ts
export default {
userManagement: '用户管理',
userList: '用户列表',
userName: '用户名',
email: '邮箱',
};
```
### 2. API 请求规范
#### 在 `src/api/` 下创建 API 模块:
```typescript
// src/api/user.ts
import { requestClient } from './request';
export interface UserInfo {
id: string;
username: string;
email: string;
status: 'active' | 'inactive';
}
export interface UserListParams {
page?: number;
pageSize?: number;
keyword?: string;
}
/**
* 获取用户列表
*/
export async function getUserListApi(params?: UserListParams) {
return requestClient.get<UserInfo[]>('/user/list', { params });
}
/**
* 获取用户详情
*/
export async function getUserDetailApi(id: string) {
return requestClient.get<UserInfo>(`/user/${id}`);
}
/**
* 创建用户
*/
export async function createUserApi(data: Partial<UserInfo>) {
return requestClient.post<UserInfo>('/user', data);
}
/**
* 更新用户
*/
export async function updateUserApi(id: string, data: Partial<UserInfo>) {
return requestClient.put<UserInfo>(`/user/${id}`, data);
}
/**
* 删除用户
*/
export async function deleteUserApi(id: string) {
return requestClient.delete(`/user/${id}`);
}
```
**API 规范:**
- 使用 `requestClient` 发起请求已配置拦截器、token、错误处理
- 函数名以 `Api` 结尾
- 使用 TypeScript 定义请求参数和返回类型
- 添加 JSDoc 注释说明用途
### 3. 状态管理 (Pinia)
#### 创建 Store
```typescript
// src/store/modules/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { UserInfo } from '#/api/types';
import { getUserDetailApi } from '#/api/user';
export const useUserStore = defineStore('user', () => {
// State
const userInfo = ref<UserInfo | null>(null);
const loading = ref(false);
// Getters
const isLoggedIn = computed(() => !!userInfo.value);
const userName = computed(() => userInfo.value?.username || 'Guest');
// Actions
async function fetchUserInfo() {
loading.value = true;
try {
const data = await getUserDetailApi('me');
userInfo.value = data;
} finally {
loading.value = false;
}
}
function clearUser() {
userInfo.value = null;
}
return {
// State
userInfo,
loading,
// Getters
isLoggedIn,
userName,
// Actions
fetchUserInfo,
clearUser,
};
});
```
**使用 Store**
```vue
<script setup lang="ts">
import { useUserStore } from '#/store';
const userStore = useUserStore();
// 读取状态
console.log(userStore.userName);
// 调用方法
userStore.fetchUserInfo();
</script>
```
### 4. 使用框架表单组件 (VbenForm)
```vue
<script setup lang="ts">
import { ref } from 'vue';
import { VbenForm } from '@vben/common-ui';
import type { VbenFormSchema } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const formSchema: VbenFormSchema[] = [
{
component: 'Input',
label: '用户名',
fieldName: 'username',
required: true,
rules: [{ required: true, message: '请输入用户名' }],
},
{
component: 'Input',
label: '邮箱',
fieldName: 'email',
required: true,
componentProps: {
type: 'email',
},
rules: [
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' },
],
},
{
component: 'Select',
label: '状态',
fieldName: 'status',
componentProps: {
options: [
{ label: '启用', value: 'active' },
{ label: '禁用', value: 'inactive' },
],
},
},
{
component: 'DatePicker',
label: '出生日期',
fieldName: 'birthday',
},
];
const formValue = ref({});
async function handleSubmit(values: any) {
console.log('提交数据:', values);
message.success('保存成功');
}
</script>
<template>
<div class="p-5">
<VbenForm
v-model="formValue"
:schema="formSchema"
@submit="handleSubmit"
/>
</div>
</template>
```
### 5. 使用框架表格组件 (VbenTable)
```vue
<script setup lang="ts">
import { ref } from 'vue';
import { VbenTable } from '@vben/common-ui';
import type { VbenTableColumn } from '@vben/common-ui';
import { Button, Tag } from 'ant-design-vue';
const columns: VbenTableColumn[] = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 200,
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
customRender: ({ text }) => {
const color = text === 'active' ? 'green' : 'red';
return <Tag color={color}>{text}</Tag>;
},
},
{
title: '操作',
key: 'action',
width: 200,
customRender: ({ record }) => (
<>
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>删除</Button>
</>
),
},
];
const dataSource = ref([
{ id: '1', username: 'admin', email: 'admin@example.com', status: 'active' },
{ id: '2', username: 'user', email: 'user@example.com', status: 'inactive' },
]);
function handleEdit(record: any) {
console.log('编辑:', record);
}
function handleDelete(id: string) {
console.log('删除:', id);
}
</script>
<template>
<VbenTable
:columns="columns"
:data-source="dataSource"
:pagination="{ pageSize: 10 }"
/>
</template>
```
### 6. 权限控制
```vue
<script setup lang="ts">
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
// 检查权限
const canEdit = hasAccessByCodes(['user:edit']);
const canDelete = hasAccessByCodes(['user:delete']);
</script>
<template>
<div>
<!-- 使用 v-access 指令控制显示 -->
<a-button v-access="'user:create'">创建用户</a-button>
<a-button v-access="'user:edit'" v-if="canEdit">编辑</a-button>
<a-button v-access="'user:delete'" v-if="canDelete">删除</a-button>
</div>
</template>
```
### 7. 使用图标
```vue
<script setup lang="ts">
import { IconSvg } from '@vben/icons';
import { SvgUserIcon, SvgSettingsIcon } from '@vben/icons';
</script>
<template>
<div>
<!-- 方式 1: 使用 IconSvg 组件 + Iconify 名称 -->
<IconSvg icon="lucide:user" class="text-xl" />
<IconSvg icon="mdi:settings" :size="24" />
<!-- 方式 2: 使用预定义的 SVG 组件 -->
<SvgUserIcon class="text-xl" />
<SvgSettingsIcon :size="24" />
</div>
</template>
```
**常用图标集:**
- `lucide:*` - Lucide 图标(推荐)
- `mdi:*` - Material Design Icons
- `carbon:*` - Carbon Icons
- `heroicons:*` - Heroicons
浏览图标https://icon-sets.iconify.design/
---
## 样式规范
### 1. 使用 Tailwind CSS
优先使用 Tailwind CSS 工具类:
```vue
<template>
<div class="p-5 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-4">标题</h2>
<div class="flex items-center gap-4">
<a-button type="primary">按钮</a-button>
</div>
</div>
</template>
```
### 2. Scoped 样式
需要自定义样式时使用 `<style scoped>`
```vue
<style scoped>
.custom-class {
/* 自定义样式 */
}
/* 深度选择器 - 影响子组件 */
:deep(.ant-btn) {
border-radius: 8px;
}
/* 插槽选择器 */
:slotted(.slot-content) {
color: red;
}
</style>
```
### 3. 响应式设计
使用 Tailwind 响应式前缀:
```vue
<template>
<!-- 移动端单列平板双列桌面三列 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</div>
</template>
```
---
## 常见问题和注意事项
### 1. ❌ 不要做的事
- **不要使用 Options API**(必须用 Composition API
- **不要使用相对路径导入**(使用 `#/` 别名)
- **不要直接修改 `packages/` 下的框架代码**
- **不要在组件中直接操作 DOM**(使用 Vue 的响应式系统)
- **不要使用 yarn/npm**(必须用 pnpm
- **不要在路由配置中使用 `require`**(使用 `import()`
### 2. ✅ 最佳实践
- **组件拆分**: 单个组件不超过 300 行,复杂组件拆分成子组件
- **类型安全**: 所有 API 响应、组件 Props、函数参数都定义 TypeScript 类型
- **错误处理**: 使用 `try-catch` 处理异步错误
- **加载状态**: 异步操作要有 loading 状态
- **国际化**: 所有用户可见的文本使用 `$t()` 函数
- **权限控制**: 敏感操作添加权限检查
### 3. 性能优化
```vue
<script setup lang="ts">
import { computed, ref } from 'vue';
// ✅ 使用 computed 缓存计算结果
const filteredList = computed(() => {
return list.value.filter(item => item.status === 'active');
});
// ✅ 大列表使用虚拟滚动
import { VirtualList } from '@vben/common-ui';
</script>
<template>
<!-- 使用 v-show 而不是 v-if频繁切换的元素 -->
<div v-show="visible">内容</div>
<!-- 长列表使用 key -->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
</template>
```
### 4. 调试技巧
```typescript
// 开发环境打印日志
if (import.meta.env.DEV) {
console.log('调试信息:', data);
}
// 使用 Vue Devtools
// Chrome 扩展Vue.js devtools
```
---
## 开发命令
```bash
# 启动开发服务器
pnpm run dev:antd
# 构建生产版本
pnpm run build:antd
# 类型检查
pnpm run check:type
# 代码格式化
pnpm run format
# 代码检查
pnpm run lint
# 单元测试
pnpm run test:unit
```
---
## API Mock 数据(开发阶段)
如果后端接口未就绪,可以使用 Mock 数据:
```typescript
// src/api/user.ts
import { requestClient } from './request';
export async function getUserListApi() {
// 开发环境使用 mock 数据
if (import.meta.env.DEV) {
return Promise.resolve([
{ id: '1', username: 'admin', email: 'admin@example.com', status: 'active' },
{ id: '2', username: 'user', email: 'user@example.com', status: 'inactive' },
]);
}
// 生产环境调用真实 API
return requestClient.get('/user/list');
}
```
---
## 环境变量配置
配置文件位置:
- `.env` - 所有环境
- `.env.development` - 开发环境
- `.env.production` - 生产环境
```bash
# .env.development
VITE_APP_TITLE=DevOps 管理系统
VITE_API_URL=http://localhost:8080/api
```
使用方式:
```typescript
import { useAppConfig } from '@vben/hooks';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
console.log('API地址:', apiURL);
```
---
## 总结
遵循本手册的规范,可以确保:
1. ✅ 代码风格统一,易于维护
2. ✅ 充分利用框架能力,避免重复造轮子
3. ✅ 类型安全,减少运行时错误
4. ✅ 性能优化,用户体验良好
5. ✅ 团队协作顺畅,代码可读性强
**开发新功能前,请先参考 `playground` 和 `demos` 目录下的示例代码!**