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

18 KiB
Raw Blame History

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. 命名规范

文件命名

  • 组件文件: PascalCaseUserProfile.vue, DataTable.vue
  • 工具文件: kebab-caseformat-date.ts, use-table.ts
  • API 文件: kebab-caseuser-api.ts, order-api.ts
  • 路由文件: kebab-caseuser-management.ts

变量命名

  • 组件变量: PascalCaseconst UserTable = defineComponent(...)
  • 普通变量: camelCaseconst userInfo = ref({})
  • 常量: UPPER_SNAKE_CASEconst API_BASE_URL = '...'
  • 类型/接口: PascalCaseinterface UserInfo { ... }

2. Vue 组件规范

必须使用 Composition API + <script setup>:

<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/ 目录:

// ✅ 正确
import { useAuthStore } from '#/store';
import type { UserInfo } from '#/api/types';
import { $t } from '#/locales';

// ❌ 错误 - 不要使用相对路径
import { useAuthStore } from '../../../store';

4. 框架组件导入

框架提供的组件从 @vben/* 包导入:

// 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/ 下创建页面组件:

<!-- 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/ 下创建路由模块:

// 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: 添加国际化(可选)

// src/locales/lang/zh-CN/user.ts
export default {
  userManagement: '用户管理',
  userList: '用户列表',
  userName: '用户名',
  email: '邮箱',
};

2. API 请求规范

src/api/ 下创建 API 模块:

// 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

// 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

<script setup lang="ts">
import { useUserStore } from '#/store';

const userStore = useUserStore();

// 读取状态
console.log(userStore.userName);

// 调用方法
userStore.fetchUserInfo();
</script>

4. 使用框架表单组件 (VbenForm)

<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)

<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. 权限控制

<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. 使用图标

<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 工具类:

<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>

<style scoped>
.custom-class {
  /* 自定义样式 */
}

/* 深度选择器 - 影响子组件 */
:deep(.ant-btn) {
  border-radius: 8px;
}

/* 插槽选择器 */
:slotted(.slot-content) {
  color: red;
}
</style>

3. 响应式设计

使用 Tailwind 响应式前缀:

<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. 性能优化

<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. 调试技巧

// 开发环境打印日志
if (import.meta.env.DEV) {
  console.log('调试信息:', data);
}

// 使用 Vue Devtools
// Chrome 扩展Vue.js devtools

开发命令

# 启动开发服务器
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 数据:

// 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 - 生产环境
# .env.development
VITE_APP_TITLE=DevOps 管理系统
VITE_API_URL=http://localhost:8080/api

使用方式:

import { useAppConfig } from '@vben/hooks';

const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
console.log('API地址:', apiURL);

总结

遵循本手册的规范,可以确保:

  1. 代码风格统一,易于维护
  2. 充分利用框架能力,避免重复造轮子
  3. 类型安全,减少运行时错误
  4. 性能优化,用户体验良好
  5. 团队协作顺畅,代码可读性强

开发新功能前,请先参考 playgrounddemos 目录下的示例代码!