基于 OpenClaw + Claude Code 的端到端研发自动化系统
本规范适用于基于 OpenClaw + Claude Code 的端到端研发自动化系统中的前端开发环节,覆盖以下技术栈:
本规范深度集成到 OpenClaw + Claude Code 的研发自动化流程中,具体节点如下:
project-root/
├── .github/ # GitHub 配置(Actions, Templates)
│ ├── workflows/
│ │ ├── ci.yml # CI 流水线配置
│ │ └── cd.yml # CD 部署配置
│ └── pull_request_template.md
│
├── .vscode/ # VSCode 工作区配置
│ ├── settings.json
│ ├── extensions.json
│ └── launch.json
│
├── public/ # 静态资源(不经过构建)
│ ├── favicon.ico
│ ├── robots.txt
│ └── manifest.json
│
├── src/ # 源代码目录
│ ├── main.tsx # 应用入口
│ ├── App.tsx # 根组件
│ ├── vite-env.d.ts # Vite 类型声明
│ │
│ ├── features/ 【核心】业务特性模块(按领域划分)
│ │ ├── user/ # 用户相关功能
│ │ │ ├── components/ # 用户模块专属组件
│ │ │ │ ├── UserProfile.tsx
│ │ │ │ ├── UserList.tsx
│ │ │ │ └── index.ts # 统一导出
│ │ │ ├── hooks/ # 用户模块自定义 Hooks
│ │ │ │ ├── useUser.ts
│ │ │ │ └── useUserList.ts
│ │ │ ├── services/ # 用户模块 API 服务
│ │ │ │ └── userService.ts
│ │ │ ├── types/ # 用户模块类型定义
│ │ │ │ └── user.types.ts
│ │ │ ├── store/ # 用户模块状态管理
│ │ │ │ └── userStore.ts
│ │ │ └── index.ts # 模块统一导出
│ │ │
│ │ ├── product/ # 产品相关功能
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ ├── types/
│ │ │ └── store/
│ │ │
│ │ └── order/ # 订单相关功能
│ │ └── ...
│ │
│ ├── components/ # 通用基础组件(跨业务复用)
│ │ ├── ui/ # 原子级 UI 组件
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.styles.ts
│ │ │ │ ├── Button.test.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Input/
│ │ │ ├── Modal/
│ │ │ └── ...
│ │ ├── layout/ # 布局组件
│ │ │ ├── Header.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Sidebar.tsx
│ │ └── index.ts
│ │
│ ├── pages/ # 页面组件(路由级别)
│ │ ├── HomePage.tsx
│ │ ├── LoginPage.tsx
│ │ ├── UserProfilePage.tsx
│ │ └── index.tsx # 路由配置
│ │
│ ├── hooks/ # 全局通用 Hooks
│ │ ├── useAuth.ts
│ │ ├── useDebounce.ts
│ │ ├── useLocalStorage.ts
│ │ └── index.ts
│ │
│ ├── services/ # 全局 API 服务层
│ │ ├── api.ts # Axios 实例配置
│ │ ├── interceptors.ts # 请求/响应拦截器
│ │ └── types.ts # API 响应类型
│ │
│ ├── store/ # 全局状态管理
│ │ ├── index.ts # Store 初始化
│ │ └── middleware.ts # 中间件配置
│ │
│ ├── styles/ # 全局样式
│ │ ├── globals.css # 全局 CSS 变量和重置
│ │ ├── themes.ts # 主题配置
│ │ └── mixins.ts # 样式混入
│ │
│ ├── utils/ # 工具函数
│ │ ├── format.ts
│ │ ├── validate.ts
│ │ ├── helpers.ts
│ │ └── index.ts
│ │
│ ├── constants/ # 常量定义
│ │ ├── routes.ts
│ │ ├── config.ts
│ │ └── index.ts
│ │
│ ├── types/ # 全局类型定义
│ │ ├── common.types.ts
│ │ ├── api.types.ts
│ │ └── index.ts
│ │
│ └── assets/ # 静态资源(经过构建)
│ ├── images/
│ ├── fonts/
│ └── icons/
│
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ ├── e2e/ # E2E 测试
│ ├── mocks/ # Mock 数据
│ └── setup.ts # 测试环境配置
│
├── docs/ # 项目文档
│ ├── architecture.md
│ ├── api-spec.md
│ └── deployment.md
│
├── scripts/ # 构建脚本
│ ├── generate-types.ts
│ └── optimize-assets.js
│
├── .env # 环境变量(本地)
├── .env.example # 环境变量模板
├── .eslintrc.cjs # ESLint 配置
├── .prettierrc # Prettier 配置
├── .stylelintrc # Stylelint 配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
├── vitest.config.ts # Vitest 配置
├── package.json # 依赖管理
├── README.md # 项目说明
└── CLAUDE.md 【重要】AI Coding 规范文件
CLAUDE.md 是 AI Coding 的核心配置文件,用于指导 Claude Code 按照项目规范生成代码。
# CLAUDE.md - AI Coding 规范配置文件
# 此文件定义了 AI 在生成代码时必须遵循的规则和约束
## 项目技术栈
- **Framework**: React 18.2+ with TypeScript 5.3+
- **Build Tool**: Vite 5.0+
- **State Management**: Zustand 4.4+
- **HTTP Client**: Axios 1.6+
- **UI Library**: Ant Design 5.12+
- **Styling**: TailwindCSS 3.4+ with CSS Modules
- **Testing**: Vitest + @testing-library/react
- **Package Manager**: pnpm 8.10+
## 代码生成规则
### 必须遵守
1. 所有组件必须使用 TypeScript,禁止使用 any 类型
2. 函数组件优先,使用箭头函数语法
3. Props 必须定义明确的 interface 或 type
4. 所有异步操作必须有错误处理
5. 组件必须有 displayName
6. 导出顺序:默认导出在前,命名导出在后
### 禁止行为
1. ❌ 禁止使用 class 组件
2. ❌ 禁止使用隐式 any
3. ❌ 禁止直接修改 state(必须使用 setState)
4. ❌ 禁止在 render 方法中创建对象/数组
5. ❌ 禁止使用 eval() 和 new Function()
## 文件命名规范
- 组件文件:PascalCase.tsx (例:UserProfile.tsx)
- Hook 文件:use*.ts (例:useAuth.ts)
- 服务文件:*.service.ts (例:user.service.ts)
- 类型文件:*.types.ts (例:user.types.ts)
- 样式文件:*.module.css 或 *.styles.ts
- 测试文件:*.test.tsx 或 *.test.ts
## 组件模板
\`\`\`tsx
import React from 'react';
import { FC } from 'react';
interface ComponentNameProps {
/** 属性描述 */
propName: string;
}
export const ComponentName: FC<ComponentNameProps> = ({ propName }) => {
// 组件逻辑
return (
<div className="component-name">
{propName}
</div>
);
};
ComponentName.displayName = 'ComponentName';
\`\`\`
## API 调用规范
1. 使用 services 层封装 API 调用
2. 所有 API 函数必须返回 Promise
3. 使用 try-catch 处理错误
4. 使用 Axios 拦截器统一处理认证和错误
## 状态管理规范
1. 优先使用 Zustand 进行状态管理
2. Store 文件放在 features/*/store/目录
3. 使用 selector 模式访问状态
4. 避免在组件中直接调用 setState
## 测试要求
1. 每个组件必须有单元测试
2. 测试覆盖率要求:语句>80%, 分支>70%
3. 使用 Testing Library 最佳实践
4. Mock 外部依赖(API、定时器等)
## Git 提交规范
遵循 Conventional Commits:
- feat: 新功能
- fix: 修复 bug
- docs: 文档更新
- style: 代码格式
- refactor: 重构
- test: 测试相关
- chore: 构建/工具相关
| 类型 | 命名规则 | 示例 | 说明 |
|---|---|---|---|
| 组件文件 | PascalCase.tsx | UserProfile.tsx |
React 组件必须使用大驼峰 |
| Hook 文件 | usePascalCase.ts | useAuth.ts, useUserProfile.ts |
自定义 Hook 必须以 use 开头 |
| 服务文件 | camelCase.service.ts | userService.ts, authService.ts |
服务层文件添加.service 后缀 |
| 类型定义 | camelCase.types.ts | user.types.ts |
类型定义文件添加.types 后缀 |
| 工具函数 | camelCase.ts | format.ts, validate.ts |
按功能分类组织 |
| 常量文件 | UPPER_CASE.ts 或 camelCase.ts | API_ENDPOINTS.ts |
纯常量用大写,配置用小写 |
| 样式文件 | camelCase.module.css | UserProfile.module.css |
CSS Modules 必须添加.module 后缀 |
| 测试文件 | camelCase.test.ts(x) | UserProfile.test.tsx |
测试文件与被测组件同名 |
| 目录名 | kebab-case 或 camelCase | user-profile/ 或 userProfile/ |
特性目录推荐 kebab-case |
// 变量命名
const userName = 'John'; // camelCase
const USER_ROLE = 'ADMIN'; // UPPER_CASE 常量
const UserList = () => {...}; // PascalCase 组件
// 函数命名
function getUserById(id: number) {...}
const handleClick = () => {...};
const useUserProfile = () => {...}; // Hook
// 布尔值变量
const isLoading = true;
const hasPermission = false;
const canEdit = true;
// 集合类型
const userList: User[] = [];
const userMap: Map<number, User> = new Map();
// 禁止的命名
const user_name = 'John'; // ❌ snake_case
const UserName = 'John'; // ❌ 变量用 PascalCase
const USER_NAME = 'John'; // ❌ 非常量用大写
let data: any; // ❌ 使用 any
const tmp = 'value'; // ❌ 无意义命名
const flag = true; // ❌ 不明确布尔值
// 函数命名
function get_user() {...} // ❌ snake_case
function GetData() {...} // ❌ 函数用 PascalCase
const clickHandle = () => {...}; // ❌ 事件处理应为 handleClick
// Interface 命名 - PascalCase
interface UserProfile {
id: number;
name: string;
}
// Type Alias 命名 - PascalCase
type UserRole = 'ADMIN' | 'USER' | 'GUEST';
// Props 类型命名 - 组件名 + Props
interface UserProfileProps {
userId: number;
showAvatar?: boolean;
}
// State 类型命名 - 组件名 + State
interface UserProfileState {
isLoading: boolean;
error: string | null;
}
// Event Handler 类型命名 - On + 事件名
type OnUserClick = (userId: number) => void;
// API Response 类型命名 - 功能名 + Response
interface UserListResponse {
data: User[];
total: number;
page: number;
}
// API Request 类型命名 - 功能名 + Request
interface CreateUserRequest {
name: string;
email: string;
}
/* BEM 命名规范 */
/* Block */
.user-profile { }
/* Element */
.user-profile__avatar { }
.user-profile__name { }
/* Modifier */
.user-profile--active { }
.user-profile__name--highlighted { }
/* 状态前缀 */
.is-loading { }
.is-disabled { }
.has-error { }
/* JS 钩子类名(不添加样式) */
.js-user-modal { }
| 组件类型 | 存放位置 | 职责 | 示例 |
|---|---|---|---|
| 原子组件 | components/ui/ |
不可拆分的基础 UI 元素 | Button, Input, Icon |
| 分子组件 | components/ui/ |
原子组件的组合 | SearchBar, FormField |
| 业务组件 | features/*/components/ |
特定业务功能的组件 | UserProfile, ProductCard |
| 布局组件 | components/layout/ |
页面结构和布局 | Header, Footer, Sidebar |
| 页面组件 | pages/ |
完整页面,路由级别 | HomePage, LoginPage |
| 模板组件 | components/templates/ |
页面布局模板 | DashboardLayout, AuthLayout |
import React, { FC, memo } from 'react';
import styles from './ComponentName.module.css';
/**
* 组件名称:ComponentName
* 功能描述:简要描述组件功能
* 使用示例:
* <ComponentName propName="value" />
*/
// Props 类型定义
export interface ComponentNameProps {
/** 属性 1 描述 */
prop1: string;
/** 属性 2 描述(可选) */
prop2?: number;
/** 子元素 */
children?: React.ReactNode;
/** 点击事件处理 */
onClick?: () => void;
}
// 组件定义
export const ComponentName: FC<ComponentNameProps> = memo(({
prop1,
prop2 = 0,
children,
onClick
}) => {
// ========== Hooks 区域 ==========
// 自定义 Hooks 调用
// ========== State 区域 ==========
// 本地状态定义
========== Effects 区域 ==========
// useEffect 逻辑
// ========== Handlers 区域 ==========
const handleClick = () => {
onClick?.();
};
// ========== Render 区域 ==========
return (
<div
className={styles.container}
data-testid="component-name"
role="button"
tabIndex={0}
onClick={handleClick}
>
<h1 className={styles.title}>{prop1}</h1>
{children}
</div>
);
});
// 设置显示名称
ComponentName.displayName = 'ComponentName';
// 导出默认(可选)
export default ComponentName;
memo() 包裹组件以优化性能displayName 便于调试handle 前缀keyimport { useState, useEffect, useCallback } from 'react';
/**
* Hook 名称:useHookName
* 功能描述:简要描述 Hook 功能
* 返回值:返回的状态和方法说明
*/
interface UseHookNameReturn {
/** 状态 1 */
data: DataType | null;
/** 加载状态 */
isLoading: boolean;
/** 错误信息 */
error: string | null;
/** 刷新方法 */
refresh: () => Promise<void>;
}
export const useHookName = (params: HookParams): UseHookNameReturn => {
// ========== State 区域 ==========
const [data, setData] = useState<DataType | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// ========== Logic 区域 ==========
const fetchData = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
// API 调用逻辑
const response = await api.getData(params);
setData(response.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, [params]);
// ========== Effect 区域 ==========
useEffect(() => {
fetchData();
}, [fetchData]);
// ========== Return 区域 ==========
return {
data,
isLoading,
error,
refresh: fetchData,
};
};
/* src/services/api.ts - Axios 实例配置 */
import axios, { AxiosInstance, AxiosError } from 'axios';
import type { ApiResponse } from '../types/api.types';
// 创建 Axios 实例
export const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 添加认证 Token
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求 ID 用于追踪
config.headers['X-Request-ID'] = crypto.randomUUID();
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => response.data,
(error: AxiosError<ApiResponse>) => {
// 统一错误处理
const status = error.response?.status;
switch (status) {
case 401:
// 未授权,跳转登录
window.location.href = '/login';
break;
case 403:
// 禁止访问
console.error('Access denied');
break;
case 404:
// 资源不存在
console.error('Resource not found');
break;
case 500:
// 服务器错误
console.error('Server error');
break;
default:
console.error('Unknown error');
}
return Promise.reject(error);
}
);
/* src/features/user/services/userService.ts */
import { apiClient } from '../../../services/api';
import type { User, UserListResponse, CreateUserRequest } from '../types/user.types';
/**
* 用户服务层
* 封装所有用户相关的 API 调用
*/
export const userService = {
/**
* 获取用户详情
* @param userId - 用户 ID
* @returns Promise<User>
*/
getUserById: async (userId: number): Promise<User> => {
return apiClient.get<User>(`/users/${userId}`);
},
/**
* 获取用户列表(分页)
* @param params - 查询参数
* @returns Promise<UserListResponse>
*/
getUserList: async (params: {
page: number;
pageSize: number;
keyword?: string;
}): Promise<UserListResponse> => {
return apiClient.get<UserListResponse>('/users', { params });
},
/**
* 创建用户
* @param data - 用户数据
* @returns Promise<User>
*/
createUser: async (data: CreateUserRequest): Promise<User> => {
return apiClient.post<User>('/users', data);
},
/**
* 更新用户
* @param userId - 用户 ID
* @param data - 更新数据
* @returns Promise<User>
*/
updateUser: async (
userId: number,
data: Partial<CreateUserRequest>
): Promise<User> => {
return apiClient.put<User>(`/users/${userId}`, data);
},
/**
* 删除用户
* @param userId - 用户 ID
* @returns Promise<void>
*/
deleteUser: async (userId: number): Promise<void> => {
return apiClient.delete(`/users/${userId}`);
},
};
/* src/types/api.types.ts */
/** 通用 API 响应结构 */
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp: number;
}
/** 分页参数 */
export interface PaginationParams {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
/** 分页响应 */
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/** 上传文件响应 */
export interface UploadResponse {
url: string;
filename: string;
size: number;
}
/* 使用 React Query 管理服务端状态 */
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '../services/userService';
// Query Keys 常量
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (params: any) => [...userKeys.lists(), params] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
};
// 获取用户列表 Hook
export const useUserList = (params: PaginationParams) => {
return useQuery({
queryKey: userKeys.list(params),
queryFn: () => userService.getUserList(params),
staleTime: 5 * 60 * 1000, // 5 分钟
});
};
// 获取用户详情 Hook
export const useUser = (userId: number) => {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => userService.getUserById(userId),
enabled: !!userId,
});
};
// 创建用户 Mutation
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.createUser,
onSuccess: () => {
// 失效用户列表缓存
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
};
/* src/features/user/store/userStore.ts */
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import type { User } from '../types/user.types';
import { userService } from '../services/userService';
// State 类型定义
interface UserState {
// 当前用户
currentUser: User | null;
// 用户列表
userList: User[];
// 加载状态
isLoading: boolean;
// 错误信息
error: string | null;
// Actions
setCurrentUser: (user: User | null) => void;
fetchUser: (userId: number) => Promise<void>;
fetchUserList: () => Promise<void>;
updateUser: (userId: number, data: Partial<User>) => Promise<void>;
clearError: () => void;
}
// 创建 Store
export const useUserStore = create<UserState>()(
devtools(
persist(
(set, get) => ({
// Initial State
currentUser: null,
userList: [],
isLoading: false,
error: null,
// Actions
setCurrentUser: (user) => set({ currentUser: user }),
fetchUser: async (userId) => {
set({ isLoading: true, error: null });
try {
const user = await userService.getUserById(userId);
set({ currentUser: user, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch user',
isLoading: false
});
}
},
fetchUserList: async () => {
set({ isLoading: true, error: null });
try {
const response = await userService.getUserList({ page: 1, pageSize: 20 });
set({ userList: response.data, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch users',
isLoading: false
});
}
},
updateUser: async (userId, data) => {
set({ isLoading: true, error: null });
try {
const updatedUser = await userService.updateUser(userId, data);
set((state) => ({
currentUser: state.currentUser?.id === userId ? updatedUser : state.currentUser,
userList: state.userList.map(u => u.id === userId ? updatedUser : u),
isLoading: false,
}));
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to update user',
isLoading: false
});
}
},
clearError: () => set({ error: null }),
}),
{ name: 'user-store' } // persist 配置
),
{ name: 'UserStore' } // devtools 配置
)
);
// Selectors(推荐模式)
export const selectCurrentUser = (state: UserState) => state.currentUser;
export const selectIsLoading = (state: UserState) => state.isLoading;
export const selectError = (state: UserState) => state.error;
import React from 'react';
import { useUserStore, selectCurrentUser, selectIsLoading } from '../store/userStore';
export const UserProfile: FC = () => {
// 使用 selector 模式(避免不必要的重渲染)
const currentUser = useUserStore(selectCurrentUser);
const isLoading = useUserStore(selectIsLoading);
// 直接使用整个 store(会订阅所有变化)
// const { currentUser, isLoading } = useUserStore();
if (isLoading) {
return <div>Loading...</div>;
}
if (!currentUser) {
return <div>No user found</div>;
}
return (
<div>
<h1>{currentUser.name}</h1>
<p>{currentUser.email}</p>
</div>
);
};
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| TailwindCSS | 快速原型、小型项目 | 开发速度快、无需写 CSS | HTML 冗长、学习成本 |
| CSS Modules | 中大型项目(推荐) | 作用域隔离、易于维护 | 需要额外文件 |
| Styled Components | 主题化需求强的项目 | 动态样式、组件化 | 运行时开销 |
| Sass/SCSS | 传统项目迁移 | 功能强大、生态成熟 | 需要编译 |
/* UserProfile.module.css */
/* ========== 变量定义 ========== */
:root {
/* 颜色变量 */
--primary-color: #00f5ff;
--secondary-color: #7b2cbf;
--text-color: #e0e7ff;
--bg-color: #0a0e27;
/* 间距变量 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* 字体大小 */
--font-sm: 0.875rem;
--font-md: 1rem;
--font-lg: 1.25rem;
--font-xl: 1.5rem;
}
/* ========== 容器样式 ========== */
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-lg);
background-color: var(--bg-color);
border-radius: 8px;
border: 1px solid rgba(0, 245, 255, 0.2);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: var(--spacing-md);
border-bottom: 2px solid var(--primary-color);
}
.title {
font-size: var(--font-xl);
color: var(--primary-color);
font-weight: 600;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--secondary-color);
}
.content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
.infoItem {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.label {
font-size: var(--font-sm);
color: rgba(224, 231, 255, 0.6);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
font-size: var(--font-md);
color: var(--text-color);
}
/* ========== 状态修饰符 ========== */
.container--loading {
opacity: 0.6;
pointer-events: none;
}
.value--highlighted {
color: var(--primary-color);
font-weight: 600;
}
/* ========== 响应式 ========== */
@media (max-width: 768px) {
.container {
padding: var(--spacing-sm);
}
.header {
flex-direction: column;
gap: var(--spacing-sm);
}
.content {
grid-template-columns: 1fr;
}
}
/* 使用 TailwindCSS 时的组件写法 */
import React from 'react';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export const Button: FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
onClick
}) => {
// 使用 clsx 或 classnames 合并类名
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const variantStyles = {
primary: 'bg-cyan-400 text-gray-900 hover:bg-cyan-500 focus:ring-cyan-400',
secondary: 'bg-purple-600 text-white hover:bg-purple-700 focus:ring-purple-600',
danger: 'bg-pink-600 text-white hover:bg-pink-700 focus:ring-pink-600',
};
const sizeStyles = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const classNames = `${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]}`;
return (
<button
className={classNames}
onClick={onClick}
type="button"
>
{children}
</button>
);
};
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
/* 品牌色 */
--color-primary: #00f5ff;
--color-secondary: #7b2cbf;
--color-accent: #f72585;
/* 中性色 */
--color-dark: #0a0e27;
--color-darker: #050818;
--color-light: #e0e7ff;
/* 功能色 */
--color-success: #00ff88;
--color-warning: #ffb700;
--color-error: #ff4757;
--color-info: #00f5ff;
/* 间距系统 */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* 字体大小 */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
/* 圆角 */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* 过渡 */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* 全局重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--color-darker);
color: var(--color-light);
line-height: 1.6;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-dark);
}
::-webkit-scrollbar-thumb {
background: var(--color-secondary);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-primary);
}
/* src/components/ui/Button/Button.test.tsx */
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button Component', () => {
// ========== 渲染测试 ==========
it('should render button with text', () => {
render(<Button>Click Me</Button>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
});
it('should apply correct variant styles', () => {
const { container } = render(<Button variant="primary">Primary</Button>);
expect(container.firstChild).toHaveClass('bg-cyan-400');
});
// ========== 交互测试 ==========
it('should call onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
// ========== 辅助功能测试 ==========
it('should support keyboard navigation', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByRole('button');
// Tab 键聚焦
button.focus();
expect(document.activeElement).toBe(button);
// Enter 键触发
fireEvent.keyDown(button, { key: 'Enter' });
expect(handleClick).toHaveBeenCalledTimes(1);
// Space 键触发
fireEvent.keyDown(button, { key: ' ' });
expect(handleClick).toHaveBeenCalledTimes(2);
});
// ========== 快照测试 ==========
it('should match snapshot', () => {
const { container } = render(
<Button variant="primary" size="md">
Snapshot Test
</Button>
);
expect(container).toMatchSnapshot();
});
});
/* src/hooks/useAuth.test.ts */
import { describe, it, expect, beforeEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useAuth } from './useAuth';
describe('useAuth Hook', () => {
beforeEach(() => {
localStorage.clear();
});
it('should return initial auth state', () => {
const { result } = renderHook(() => useAuth());
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeNull();
});
it('should login successfully', async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login({
email: 'test@example.com',
password: 'password123'
});
});
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toEqual({
email: 'test@example.com',
name: 'Test User',
});
});
});
it('should handle login error', async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
try {
await result.current.login({
email: 'invalid@example.com',
password: 'wrong'
});
} catch (error) {
// Expected error
}
});
expect(result.current.error).toBe('Invalid credentials');
});
it('should logout successfully', async () => {
const { result } = renderHook(() => useAuth());
// 先登录
await act(async () => {
await result.current.login({
email: 'test@example.com',
password: 'password123'
});
});
// 再登出
await act(async () => {
await result.current.logout();
});
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeNull();
});
});
/* tests/integration/userFlow.test.tsx */
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from '../../src/App';
import { server } from '../mocks/server';
import { rest } from 'msw';
// MSW Mock Handler
server.use(
rest.post('/api/login', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
token: 'mock-token',
user: { id: 1, name: 'Test User' },
})
);
})
);
describe('User Flow Integration Test', () => {
const renderApp = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
);
};
it('should complete full user flow: login -> view profile -> logout', async () => {
renderApp();
// Step 1: 登录
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const loginButton = screen.getByRole('button', { name: /login/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(loginButton);
// 等待登录成功并跳转
await waitFor(() => {
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
});
// Step 2: 查看个人资料
const profileLink = screen.getByRole('link', { name: /profile/i });
fireEvent.click(profileLink);
await waitFor(() => {
expect(screen.getByText('Test User')).toBeInTheDocument();
});
// Step 3: 登出
const logoutButton = screen.getByRole('button', { name: /logout/i });
fireEvent.click(logoutButton);
await waitFor(() => {
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
});
});
/* tests/e2e/user-journey.spec.ts */
import { test, expect } from '@playwright/test';
test.describe('User Journey', () => {
test('should complete registration and login flow', async ({ page }) => {
// 访问首页
await page.goto('http://localhost:3000');
// 点击注册按钮
await page.click('text=Register');
// 填写注册表单
await page.fill('input[name="name"]', 'Test User');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
// 提交注册
await page.click('button[type="submit"]');
// 验证注册成功
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('text=Welcome, Test User')).toBeVisible();
// 登出
await page.click('text=Logout');
// 验证登出成功
await expect(page).toHaveURL(/login/);
// 登录
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// 验证登录成功
await expect(page).toHaveURL(/dashboard/);
});
test('should handle responsive design', async ({ page }) => {
// 移动端视图
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('http://localhost:3000');
// 验证移动端菜单
const mobileMenu = page.locator('.mobile-menu');
await expect(mobileMenu).toBeVisible();
// 桌面端视图
await page.setViewportSize({ width: 1920, height: 1080 });
// 验证桌面端导航
const desktopNav = page.locator('.desktop-nav');
await expect(desktopNav).toBeVisible();
});
});
# .github/workflows/ci.yml
name: Frontend CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# ========== 代码质量检查 ==========
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint
- name: Run Type Check
run: pnpm type-check
# ========== 单元测试 ==========
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests with coverage
run: pnpm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
# ========== 构建验证 ==========
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build application
run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# ========== E2E 测试 ==========
e2e:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Install Playwright
run: pnpm exec playwright install --with-deps
- name: Start preview server
run: pnpm preview &
- name: Wait for server
run: sleep 5
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
# Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# 复制依赖文件
COPY package.json pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 构建应用
RUN pnpm build
# Stage 2: Production
FROM nginx:alpine AS production
# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-app
namespace: production
labels:
app: frontend-app
version: v1.0.0
spec:
replicas: 3
selector:
matchLabels:
app: frontend-app
template:
metadata:
labels:
app: frontend-app
spec:
containers:
- name: frontend
image: registry.example.com/frontend-app:latest
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: NODE_ENV
value: "production"
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: production
spec:
selector:
app: frontend-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- app.example.com
secretName: frontend-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
IMAGE_NAME = 'frontend-app'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh 'pnpm install --frozen-lockfile'
}
}
stage('Code Quality') {
parallel {
stage('Lint') {
steps {
sh 'pnpm lint'
}
}
stage('Type Check') {
steps {
sh 'pnpm type-check'
}
}
}
}
stage('Test') {
steps {
sh 'pnpm test -- --coverage'
}
post {
always {
junit 'reports/junit-*.xml'
publishCoverage adapters: [coberturaAdapter('coverage/cobertura-coverage.xml')]
}
}
}
stage('Build') {
steps {
sh 'pnpm build'
}
}
stage('Docker Build & Push') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
def customImage = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_ID}")
customImage.push()
customImage.push('latest')
}
}
}
}
stage('Deploy to K8s') {
when {
branch 'main'
}
steps {
sh '''
kubectl set image deployment/frontend-app \
frontend=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_ID} \
-n production
kubectl rollout status deployment/frontend-app -n production
'''
}
}
}
post {
success {
echo 'Pipeline completed successfully! 🎉'
}
failure {
echo 'Pipeline failed! ❌'
// 发送通知
// slackSend channel: '#deployments', message: "Build failed: ${env.BUILD_URL}"
}
}
}
# Role: 高级前端工程师 Agent
# Task: 根据规格说明生成 React + TypeScript 组件
## 上下文信息
- 项目技术栈:React 18.2 + TypeScript 5.3 + Vite 5.0
- 状态管理:Zustand 4.4
- UI 库:Ant Design 5.12
- 样式方案:CSS Modules
- 测试框架:Vitest + Testing Library
## 任务描述
请创建一个用户资料展示组件(UserProfile),需要满足以下规格:
### 功能需求
1. 展示用户基本信息(头像、姓名、邮箱、角色)
2. 支持编辑模式切换
3. 支持头像上传功能
4. 响应式设计(移动端适配)
### Props 接口
interface UserProfileProps {
userId: number;
editable?: boolean;
onUserUpdate?: (userId: number, data: UserData) => void;
}
### 设计要求
- 使用 CSS Modules 进行样式隔离
- 遵循 BEM 命名规范
- 支持暗色主题
- 加载状态和错误状态处理
### 代码规范
- 必须使用 TypeScript,禁止 any 类型
- 使用 memo() 优化性能
- 完整的 JSDoc 注释
- 包含单元测试
## 输出要求
1. 生成 UserProfile.tsx 组件文件
2. 生成 UserProfile.module.css 样式文件
3. 生成 UserProfile.test.tsx 测试文件
4. 生成 index.ts 统一导出文件
## 约束条件
- 遵循项目 CLAUDE.md 中的所有规范
- 代码必须符合 ESLint 规则
- 测试覆盖率要求:语句>80%, 分支>70%
# Role: 后端 API 集成专家 Agent
# Task: 根据 OpenAPI 规范生成前端 API 服务层
## 上下文信息
- API 规范:OpenAPI 3.0 (Swagger)
- HTTP 客户端:Axios 1.6
- 认证方式:JWT Bearer Token
- 错误处理:统一拦截器
## 任务描述
请根据以下 OpenAPI 规范生成用户模块的 API 服务层代码:
### API Endpoints
```yaml
/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
- name: pageSize
in: query
schema:
type: integer
responses:
200:
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
total:
type: integer
/users/{id}:
get:
summary: 获取用户详情
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
```
## 输出要求
1. 生成 user.types.ts 类型定义文件
2. 生成 userService.ts 服务层文件
3. 包含完整的 JSDoc 注释
4. 包含错误处理和重试逻辑
## 约束条件
- 所有 API 函数必须返回 Promise
- 使用 try-catch 处理错误
- 类型定义必须完整且准确
- 遵循项目 API 调用规范
# Role: 代码质量审查专家 Agent
# Task: 审查生成的前端代码是否符合规范
## 审查清单
### 1. TypeScript 类型安全
- [ ] 是否使用了 any 类型(禁止)
- [ ] Props 类型是否完整定义
- [ ] 返回值类型是否明确
- [ ] 泛型使用是否恰当
### 2. 代码规范
- [ ] 文件命名是否符合约定
- [ ] 变量命名是否语义化
- [ ] 函数长度是否合理(<50 行)
- [ ] 组件复杂度是否可控
### 3. 性能优化
- [ ] 是否使用 memo() 包裹组件
- [ ] 是否存在不必要的重渲染
- [ ] 列表渲染是否使用稳定 key
- [ ] 是否在 render 中创建对象/数组
### 4. 错误处理
- [ ] 异步操作是否有 try-catch
- [ ] 是否有友好的错误提示
- [ ] 边界情况是否处理
### 5. 测试覆盖
- [ ] 是否有单元测试
- [ ] 测试用例是否充分
- [ ] 是否测试了边界情况
- [ ] 测试代码是否遵循最佳实践
### 6. 可访问性
- [ ] 是否使用语义化 HTML
- [ ] 是否支持键盘导航
- [ ] 是否有适当的 ARIA 属性
- [ ] 颜色对比度是否达标
## 输出格式
请以 JSON 格式输出审查结果:
{
"passed": boolean,
"issues": [
{
"severity": "error" | "warning" | "info",
"rule": "规则名称",
"message": "问题描述",
"location": "文件:行号",
"suggestion": "修复建议"
}
],
"score": 0-100,
"summary": "总体评价"
}
# Role: 代码重构优化专家 Agent
# Task: 重构现有代码以提升质量和性能
## 原始代码
[在此粘贴需要重构的代码]
## 重构目标
1. 提升代码可读性和可维护性
2. 优化性能(减少重渲染、优化计算)
3. 增强类型安全性
4. 改进错误处理
5. 添加必要的注释和文档
## 重构原则
- 保持原有功能不变
- 遵循单一职责原则
- 提取可复用的逻辑到 Hooks
- 使用更精确的 TypeScript 类型
- 添加性能优化(memo、useMemo、useCallback)
## 输出要求
1. 重构后的完整代码
2. 重构说明文档(列出所有改动)
3. 性能对比分析(如果适用)
4. 潜在的风险分析
# 开发
pnpm dev # 启动开发服务器
pnpm build # 生产构建
pnpm preview # 预览生产构建
# 代码质量
pnpm lint # ESLint 检查
pnpm lint:fix # 自动修复 ESLint 问题
pnpm type-check # TypeScript 类型检查
pnpm format # Prettier 格式化
# 测试
pnpm test # 运行单元测试
pnpm test:watch # 监视模式运行测试
pnpm test:coverage # 生成测试覆盖率报告
pnpm test:e2e # 运行 E2E 测试
# Git
git commit -m "feat: add new feature" # 提交代码
git push origin main # 推送代码
// .eslintrc.cjs
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};