基于 OpenClaw + Claude Code 的端到端研发自动化系统 · 前端研发岗位 Agent 专用
| 层级 | 约束类型 | 执行方式 | 违规处理 |
|---|---|---|---|
| L1 - 基础规范 | 强制 | ESLint + Prettier | 构建失败 |
| L2 - 架构规范 | 强制 | 架构检查工具 | Code Review 拦截 |
| L3 - 最佳实践 | 推荐 | AI 提示词约束 | 警告提示 |
| L4 - 性能规范 | 推荐 | Lighthouse CI | 性能报告 |
| 文件类型 | 命名规则 | 示例 | 优先级 |
|---|---|---|---|
| React 组件 | PascalCase.tsx | UserProfile.tsx, OrderList.tsx | 强制 |
| Vue 组件 | PascalCase.vue | UserProfile.vue, OrderList.vue | 强制 |
| 工具函数 | camelCase.ts | formatDate.ts, validateEmail.ts | 强制 |
| 常量文件 | UPPER_CASE.ts | API_ENDPOINTS.ts, ROUTES.ts | 强制 |
| 类型定义 | PascalCase.types.ts | User.types.ts, Api.types.ts | 强制 |
| 测试文件 | *.test.ts(x) | UserProfile.test.tsx | 强制 |
| 样式文件 | *.module.css/scss | UserProfile.module.css | 推荐 |
// ✅ 正确示例
const userData = { // camelCase 用于变量
userName: 'John',
userAge: 25
};
function calculateTotalPrice() { // camelCase 用于函数
return price * quantity;
}
const MAX_RETRY_COUNT = 3; // UPPER_CASE 用于常量
interface UserProfile { // PascalCase 用于类型
id: string;
name: string;
}
// ❌ 错误示例 - AI 生成时必须避免
const user_data = {}; // 不使用 snake_case
function CalculateTotal() {} // 函数不使用 PascalCase
const maxRetryCount = 3; // 常量必须 UPPER_CASE
// ✅ 组件命名规则
// 1. 组件名必须与文件名一致
// 2. 使用业务语义命名,避免通用名
// 3. 列表组件使用复数形式
// 好示例
export function UserProfile() { // 具体业务语义
return <div>...</div>;
}
export function OrderList() { // 列表使用复数
return <ul>...</ul>;
}
// 坏示例 - AI 不应生成
export function Component1() {} // 无意义命名
export function Page() {} // 过于通用
export function Order() {} // 列表应该用复数
# React + TypeScript 项目结构模板
project-root/
├── src/
│ ├── components/ # 可复用组件
│ │ ├── ui/ # 基础 UI 组件
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.module.css
│ │ │ │ ├── Button.types.ts
│ │ │ │ └── Button.test.tsx
│ │ │ └── Input/
│ │ ├── business/ # 业务组件
│ │ │ ├── UserProfile/
│ │ │ └── OrderList/
│ │ └── layouts/ # 布局组件
│ ├── pages/ # 页面组件 (Next.js: app/)
│ │ ├── Home/
│ │ ├── User/
│ │ └── Order/
│ ├── hooks/ # 自定义 Hooks
│ │ ├── useAuth.ts
│ │ ├── useFetch.ts
│ │ └── useLocalStorage.ts
│ ├── services/ # API 服务层
│ │ ├── api.ts
│ │ ├── userService.ts
│ │ ├── orderService.ts
│ │ └── interceptors.ts
│ ├── store/ # 状态管理
│ │ ├── slices/
│ │ ├── store.ts
│ │ └── selectors.ts
│ ├── types/ # 全局类型定义
│ │ ├── api.types.ts
│ │ ├── user.types.ts
│ │ └── common.types.ts
│ ├── utils/ # 工具函数
│ │ ├── format.ts
│ │ ├── validate.ts
│ │ └── helpers.ts
│ ├── constants/ # 常量定义
│ │ ├── routes.ts
│ │ ├── apiEndpoints.ts
│ │ └── config.ts
│ ├── styles/ # 全局样式
│ │ ├── globals.css
│ │ ├── variables.css
│ │ └── mixins.scss
│ ├── assets/ # 静态资源
│ │ ├── images/
│ │ ├── icons/
│ │ └── fonts/
│ ├── App.tsx
│ └── main.tsx
├── tests/ # 集成测试
│ ├── e2e/
│ └── fixtures/
├── public/
├── .eslintrc.js
├── .prettierrc
├── tsconfig.json
├── vite.config.ts
├── package.json
└── README.md
src 根目录直接创建组件文件/**
* @file UserProfile.tsx
* @description 用户个人资料展示组件
* @author AI Code Generator
* @created 2026-03-13
*/
import React, { FC, memo } from 'react';
import PropTypes from 'prop-types';
import styles from './UserProfile.module.css';
import { UserProfileProps, UserData } from './UserProfile.types';
import { Avatar, Card, Skeleton } from '@/components/ui';
import { formatDate } from '@/utils/format';
/**
* UserProfile 组件 Props 默认值
*/
const defaultProps: Partial<UserProfileProps> = {
showJoinDate: true,
size: 'medium',
onEditClick: undefined
};
/**
* UserProfile - 用户个人资料展示组件
*
* @param {UserProfileProps} props - 组件属性
* @returns {JSX.Element} 渲染的用户资料组件
*
* @example
* <UserProfile
* userId="123"
* showJoinDate={true}
* onEditClick={() => handleEdit()}
* />
*/
export const UserProfile: FC<UserProfileProps> = memo((props) => {
// 合并默认 props
const mergedProps = { ...defaultProps, ...props };
const { userId, showJoinDate, size, onEditClick } = mergedProps;
// 自定义 Hooks 调用
const { data, loading, error } = useUser(userId);
// 加载状态
if (loading) {
return <Skeleton height={200} />;
}
// 错误状态
if (error) {
return <div className={styles.error}>{error}</div>;
}
// 主渲染逻辑
return (
<Card className={styles[container]}>
<Avatar
src={data?.avatar}
size={size}
alt={data?.name}
/>
<div className={styles.info}>
<h3 className={styles.name}>
{data?.name}
</h3>
{showJoinDate && (
<p className={styles.joinDate}>
加入于 {formatDate(data?.createdAt)}
</p>
)}
</div>
{onEditClick && (
<button
onClick={onEditClick}
className={styles.editBtn}
>
编辑资料
</button>
)}
</Card>
);
});
// PropTypes 运行时验证(可选,TypeScript 项目可省略)
UserProfile.propTypes = {
userId: PropTypes.string.isRequired,
showJoinDate: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium', 'large']),
onEditClick: PropTypes.func
};
// 显示名称
UserProfile.displayName = 'UserProfile';
export default UserProfile;
/**
* @file UserProfile.types.ts
* @description UserProfile 组件类型定义
*/
import { CSSProperties } from 'react';
/**
* 用户数据接口
*/
export interface UserData {
/** 用户 ID */
id: string;
/** 用户名 */
name: string;
/** 头像 URL */
avatar?: string;
/** 邮箱 */
email: string;
/** 创建时间 */
createdAt: string;
/** 更新时间 */
updatedAt: string;
}
/**
* 组件尺寸枚举
*/
export type ComponentSize = 'small' | 'medium' | 'large';
/**
* UserProfile 组件 Props 接口
*/
export interface UserProfileProps {
/** 用户 ID(必填) */
userId: string;
/** 是否显示加入日期(可选,默认 true) */
showJoinDate?: boolean;
/** 组件尺寸(可选,默认 'medium') */
size?: ComponentSize;
/** 编辑按钮点击回调(可选) */
onEditClick?: () => void;
/** 自定义样式类名(可选) */
className?: string;
/** 自定义内联样式(可选) */
style?: CSSProperties;
}
/**
* 内部状态接口(如需要)
*/
export interface UserProfileState {
isEditing: boolean;
isLoading: boolean;
}
<!--
* @file UserProfile.vue
* @description 用户个人资料展示组件
* @author AI Code Generator
* @created 2026-03-13
-->
<script setup lang="ts">
import { defineProps, defineEmits, computed, ref } from 'vue';
import { UserProfileProps, UserData } from './UserProfile.types';
import { useUser } from '@/hooks/useUser';
import { formatDate } from '@/utils/format';
/**
* 定义组件 Props
*/
const props = defineProps<UserProfileProps>({
userId: {
type: String,
required: true
},
showJoinDate: {
type: Boolean,
default: true
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
}
});
/**
* 定义组件事件
*/
const emit = defineEmits<{
edit: []
update: [data: UserData]
}>();
/**
* 使用组合式 API
*/
const { data, loading, error } = useUser(toRef(props, 'userId'));
const formattedJoinDate = computed(() => {
return data.value ? formatDate(data.value.createdAt) : '';
});
const handleEdit = () => {
emit('edit');
};
</script>
<template>
<div v-if="loading" class="skeleton">
<Skeleton height="200" />
</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<Card v-else class="user-profile">
<Avatar
:src="data?.avatar"
:size="size"
:alt="data?.name"
/>
<div class="info">
<h3 class="name">{{ data?.name }}</h3>
<p v-if="showJoinDate" class="join-date">
加入于 {{ formattedJoinDate }}
</p>
</div>
<button
@click="handleEdit"
class="edit-btn"
>
编辑资料
</button>
</Card>
</template>
<style module scoped>
/* CSS 样式见第 8 节 */
</style>
// ✅ 必须遵守的类型规则
// 1. 禁止使用 any 类型
let data: any; // ❌ 严禁使用
let data: unknown; // ✅ 使用 unknown 代替
// 2. 明确返回值类型
function getUser(id: string) { // ❌ 缺少返回类型
return { id, name: 'John' };
}
function getUser(id: string): Promise<UserData> { // ✅ 明确返回类型
return api.get(`/users/${id}`);
}
// 3. 使用接口定义对象结构
type User = { // ❌ 优先使用 interface
id: string;
name: string;
};
interface User { // ✅ 使用 interface
id: string;
name: string;
}
// 4. 联合类型优于枚举(简单场景)
type Status = 'pending' | 'success' | 'error'; // ✅ 推荐
// 5. 使用 Pick/Omit 进行类型转换
interface UserFull {
id: string;
name: string;
email: string;
password: string;
}
type UserPublic = Omit<UserFull, 'password'>; // ✅ 排除敏感字段
type UserNameOnly = Pick<UserFull, 'id' | 'name'>; // ✅ 选取部分字段
/**
* @file api.types.ts
* @description 通用 API 类型定义
*/
/**
* 通用 API 响应结构
*/
export interface ApiResponse<T = any> {
/** 状态码 */
code: number;
/** 消息 */
message: string;
/** 数据 */
data: T;
/** 时间戳 */
timestamp: number;
}
/**
* 分页参数
*/
export interface PaginationParams {
/** 页码,从 1 开始 */
page: number;
/** 每页数量 */
pageSize: number;
/** 排序字段 */
sortBy?: string;
/** 排序方向 */
order?: 'asc' | 'desc';
}
/**
* 分页响应数据
*/
export interface PaginatedResponse<T> {
/** 数据列表 */
items: T[];
/** 总数 */
total: number;
/** 总页数 */
totalPages: number;
/** 当前页 */
currentPage: number;
}
/**
* 异步请求状态
*/
export type RequestStatus = 'idle' | 'loading' | 'success' | 'error';
/**
* 异步请求结果
*/
export interface AsyncResult<T> {
data: T | null;
error: Error | null;
status: RequestStatus;
isLoading: boolean;
}
/**
* @file interceptors.ts
* @description Axios 请求/响应拦截器配置
*/
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosError,
AxiosResponse
} from 'axios';
import { ApiResponse } from '@/types/api.types';
import { getToken, clearToken } from '@/utils/auth';
import { refreshToken } from '@/services/authService';
/**
* 创建 Axios 实例
*/
export const apiClient: AxiosInstance = axios.create({
baseURL: process.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json'
}
});
/**
* 请求拦截器
*/
apiClient.interceptors.request.use(
async (config: AxiosRequestConfig) => {
// 添加认证 Token
const token = getToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求 ID 用于追踪
config.headers['X-Request-ID'] = generateRequestId();
return config;
},
(error: AxiosError) => Promise.reject(error)
);
/**
* 响应拦截器
*/
apiClient.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { data } = response;
// 统一处理业务错误
if (data.code !== 200) {
return Promise.reject(new BusinessError(data.message, data.code));
}
return data.data;
},
async (error: AxiosError<ApiResponse>) => {
const originalRequest = error.config;
// Token 过期处理
if (error.response?.status === 401 && !originalRequest?._retry) {
originalRequest._retry = true;
try {
const newToken = await refreshToken();
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
clearToken();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// 统一错误处理
const errorMessage = error.response?.data?.message || '网络请求失败';
showErrorToast(errorMessage);
return Promise.reject(error);
}
);
/**
* @file userService.ts
* @description 用户相关 API 服务
*/
import { apiClient } from './api';
import { UserData, UserCreateParams, UserUpdateParams } from '@/types/user.types';
import { ApiResponse, PaginatedResponse, PaginationParams } from '@/types/api.types';
/**
* API 端点常量
*/
const ENDPOINTS = {
USERS: '/users',
USER_PROFILE: (id: string) => `/users/${id}`,
USER_AVATAR: (id: string) => `/users/${id}/avatar`
} as const;
/**
* 获取用户列表
* @param params - 分页和筛选参数
* @returns 分页用户数据
*/
export async function getUserList(
params: PaginationParams & { keyword?: string }
): Promise<PaginatedResponse<UserData>> {
return apiClient.get(ENDPOINTS.USERS, { params });
}
/**
* 获取用户详情
* @param userId - 用户 ID
* @returns 用户数据
*/
export async function getUserById(userId: string): Promise<UserData> {
return apiClient.get(ENDPOINTS.USER_PROFILE(userId));
}
/**
* 创建用户
* @param data - 用户创建参数
* @returns 创建的用户数据
*/
export async function createUser(data: UserCreateParams): Promise<UserData> {
return apiClient.post(ENDPOINTS.USERS, data);
}
/**
* 更新用户
* @param userId - 用户 ID
* @param data - 用户更新参数
* @returns 更新后的用户数据
*/
export async function updateUser(
userId: string,
data: UserUpdateParams
): Promise<UserData> {
return apiClient.put(ENDPOINTS.USER_PROFILE(userId), data);
}
/**
* 删除用户
* @param userId - 用户 ID
*/
export async function deleteUser(userId: string): Promise<void> {
return apiClient.delete(ENDPOINTS.USER_PROFILE(userId));
}
/**
* 上传用户头像
* @param userId - 用户 ID
* @param file - 头像文件
* @returns 头像 URL
*/
export async function uploadUserAvatar(
userId: string,
file: File
): Promise<string> {
const formData = new FormData();
formData.append('avatar', file);
return apiClient.post(ENDPOINTS.USER_AVATAR(userId), formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
/**
* @file userSlice.ts
* @description 用户状态管理 Slice
*/
import {
createSlice,
createAsyncThunk,
PayloadAction,
createSelector
} from '@reduxjs/toolkit';
import { UserData } from '@/types/user.types';
import { getUserById, updateUser } from '@/services/userService';
/**
* 状态接口
*/
interface UserState {
current: UserData | null;
loading: boolean;
error: string | null;
lastUpdated: number | null;
}
/**
* 初始状态
*/
const initialState: UserState = {
current: null,
loading: false,
error: null,
lastUpdated: null
};
/**
* 异步 Thunk: 获取用户详情
*/
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId: string) => {
return await getUserById(userId);
}
);
/**
* 异步 Thunk: 更新用户
*/
export const patchUser = createAsyncThunk(
'user/updateUser',
async ({ userId, data }: { userId: string; data: Partial<UserData> }) => {
return await updateUser(userId, data);
}
);
/**
* 创建 Slice
*/
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
clearUser: (state) => {
state.current = null;
state.error = null;
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
}
},
extraReducers: (builder) => {
builder
// 获取用户
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.current = action.payload;
state.loading = false;
state.lastUpdated = Date.now();
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch user';
})
// 更新用户
.addCase(patchUser.fulfilled, (state, action) => {
state.current = action.payload;
state.lastUpdated = Date.now();
});
}
});
/**
* 导出 Actions
*/
export const { clearUser, setError } = userSlice.actions;
/**
* 选择器
*/
export const selectCurrentUser = (state: { user: UserState }) =>
state.user.current;
export const selectIsLoading = (state: { user: UserState }) =>
state.user.loading;
export const selectUserError = (state: { user: UserState }) =>
state.user.error;
// 记忆化选择器
export const selectUserName = createSelector(
[selectCurrentUser],
(user) => user?.name || 'Guest'
);
export default userSlice.reducer;
/* ✅ CSS Modules 命名约定 */
/* 容器使用 container 或 wrapper */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* 使用 BEM 风格命名 */
.userProfile { /* Block */
display: flex;
flexDirection: column;
}
.userProfile__header { /* Element */
padding: 20px;
borderBottom: 1px solid var(--border-color);
}
.userProfile__avatar--large { /* Modifier */
width: 120px;
height: 120px;
}
/* 状态前缀 */
.isLoading {
opacity: 0.6;
pointerEvents: none;
}
.isDisabled {
cursor: not-allowed;
opacity: 0.5;
}
.hasError {
borderColor: var(--error-color);
}
/* 布局相关 */
.flexCenter {
display: flex;
alignItems: center;
justifyContent: center;
}
.gridLayout {
display: grid;
gap: 20px;
}
/* 间距使用 scale */
.mt1 { marginTop: 8px; }
.mt2 { marginTop: 16px; }
.mt3 { marginTop: 24px; }
.mt4 { marginTop: 32px; }
<!-- ✅ TailwindCSS 最佳实践 -->
<!-- 1. 使用组件抽象复杂样式 -->
<!-- ❌ 避免 -->
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<!-- ✅ 推荐 -->
<Card variant="hoverable">
<!-- 2. 响应式设计从移动优先 -->
<div className="w-full md:w-1/2 lg:w-1/3 px-4">
<!-- 3. 使用 CSS 变量保持主题一致性 -->
<div className="bg-primary text-primary-foreground">
<!-- 4. 条件样式使用 clsx 或 classnames -->
import clsx from 'clsx';
<button
className={clsx(
'px-4 py-2 rounded font-medium',
isActive && 'bg-blue-500 text-white',
isDisabled && 'opacity-50 cursor-not-allowed'
)}
>
/**
* @file UserProfile.test.tsx
* @description UserProfile 组件单元测试
*/
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserProfile } from './UserProfile';
import { useUser } from '@/hooks/useUser';
// Mock Hooks
vi.mock('@/hooks/useUser', () => ({
useUser: vi.fn()
}));
describe('UserProfile Component', () => {
const mockUser = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg',
createdAt: '2024-01-01T00:00:00Z'
};
beforeEach(() => {
vi.clearAllMocks();
});
it('renders loading state initially', () => {
// Arrange
(useUser as any).mockReturnValue({
data: null,
loading: true,
error: null
});
// Act
render(<UserProfile userId="123" />);
// Assert
expect(screen.queryByRole('progressbar')).toBeInTheDocument();
});
it('displays user data when loaded successfully', async () => {
// Arrange
(useUser as any).mockReturnValue({
data: mockUser,
loading: false,
error: null
});
// Act
render(<UserProfile userId="123" />);
// Assert
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
it('shows error message when fetch fails', () => {
// Arrange
(useUser as any).mockReturnValue({
data: null,
loading: false,
error: 'Failed to load user'
});
// Act
render(<UserProfile userId="123" />);
// Assert
expect(screen.getByText('Failed to load user')).toBeInTheDocument();
});
it('calls onEditClick when edit button is clicked', async () => {
// Arrange
const handleEdit = vi.fn();
(useUser as any).mockReturnValue({
data: mockUser,
loading: false,
error: null
});
// Act
render(<UserProfile userId="123" onEditClick={handleEdit} />);
await waitFor(() => {
expect(screen.getByText('编辑资料')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('编辑资料'));
// Assert
expect(handleEdit).toHaveBeenCalledTimes(1);
});
it('does not show join date when showJoinDate is false', async () => {
// Arrange
(useUser as any).mockReturnValue({
data: mockUser,
loading: false,
error: null
});
// Act
render(<UserProfile userId="123" showJoinDate={false} />);
// Assert
await waitFor(() => {
expect(screen.queryByText(/加入于/)).not.toBeInTheDocument();
});
});
});
/**
* @file formatDate.test.ts
* @description formatDate 工具函数测试
*/
import { describe, it, expect } from 'vitest';
import { formatDate } from './formatDate';
describe('formatDate utility', () => {
it('formats ISO date string correctly', () => {
const input = '2024-01-15T10:30:00Z';
const expected = '2024-01-15';
expect(formatDate(input)).toBe(expected);
});
it('returns empty string for null input', () => {
expect(formatDate(null)).toBe('');
});
it('returns empty string for undefined input', () => {
expect(formatDate(undefined)).toBe('');
});
it('handles invalid date gracefully', () => {
expect(formatDate('invalid-date')).toBe('');
});
});
// .eslintrc.js - AI 生成代码必须遵守的规则
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react', 'import'],
rules: {
// 强制规则 - 违反将导致构建失败
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_'
}],
'no-explicit-any': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc' }
}],
// 复杂度限制
'max-depth': ['warn', 4],
'max-nested-callbacks': ['warn', 3],
'max-params': ['warn', 4],
'complexity': ['warn', 20],
// 代码长度限制
'max-lines-per-function': ['warn', {
max: 100,
skipBlankLines: true,
skipComments: true
}],
'max-lines': ['warn', {
max: 500,
skipBlankLines: true,
skipComments: true
}]
}
};
| 检查项 | 检查方式 | 通过标准 | 优先级 |
|---|---|---|---|
| 类型安全 | TypeScript 编译 | 零类型错误 | P0 |
| ESLint 规则 | eslint --max-warnings=0 | 零错误零警告 | P0 |
| 格式化 | Prettier 检查 | 格式完全一致 | P0 |
| 单元测试覆盖率 | Vitest/Jest | 行覆盖率 ≥80% | P0 |
| 组件可访问性 | axe-core | WCAG AA 合规 | P1 |
| 性能指标 | Lighthouse | Performance ≥90 | P1 |
| Bundle 大小 | webpack-bundle-analyzer | 单 chunk ≤500KB | P1 |
# Git 分支命名约定
# 主分支
main # 生产环境
develop # 开发环境
# 功能分支 (feature)
feature/user-authentication
feature/order-management
feature/payment-integration
# 修复分支 (fix)
fix/login-bug
fix/memory-leak
fix/api-timeout
# 热修复分支 (hotfix)
hotfix/critical-security-patch
hotfix/production-crash
# 发布分支 (release)
release/v1.2.0
release/v2.0.0-beta
# 实验分支 (experiment)
exp/new-routing-strategy
exp/performance-optimization
# Conventional Commits 规范
# <type>(<scope>): <subject>
# Type 类型:
feat: # 新功能
fix: # Bug 修复
docs: # 文档变更
style: # 代码格式 (不影响代码运行)
refactor: # 重构 (既不是新增功能也不是修改 bug)
perf: # 性能优化
test: # 测试相关
chore: # 构建过程或辅助工具变动
ci: # CI 配置文件
build: # 影响构建系统或外部依赖
revert: # 回滚提交
# 示例:
feat(user): add user profile component
fix(api): resolve timeout issue in getUserById
docs(readme): update installation instructions
refactor(auth): simplify token refresh logic
perf(render): optimize list rendering with virtualization
test(user): add unit tests for UserProfile component
chore(deps): upgrade react to v18.2.0
ci(github): add automated testing workflow
# 完整的 Commit Message 格式:
feat(user): add user profile component
Add UserProfile component with avatar display and edit functionality.
- Create UserProfile component with TypeScript types
- Add CSS Modules styling
- Implement loading and error states
- Add comprehensive unit tests
Closes #123
Related to #456
"""
你是一位资深前端工程师,请根据以下需求生成符合企业规范的 React 组件代码。
【需求描述】
{用户需求描述}
【技术要求】
1. 使用 React 18 + TypeScript 5
2. 遵循以下规范:
- 文件命名:PascalCase.tsx
- 组件结构:函数组件 + memo 优化
- 样式方案:CSS Modules
- 类型定义:独立的 .types.ts 文件
- 必须包含完整的 JSDoc 注释
【代码约束】
1. 禁止使用 any 类型
2. 所有 Props 必须有明确的类型定义
3. 必须处理 loading 和 error 状态
4. 必须包含 PropTypes 验证(可选)
5. 函数最大行数不超过 100 行
6. 组件嵌套深度不超过 4 层
【输出要求】
请按以下顺序输出完整代码:
1. 组件类型定义文件 (*.types.ts)
2. 组件实现文件 (*.tsx)
3. 组件样式文件 (*.module.css)
4. 组件测试文件 (*.test.tsx)
【质量标准】
- ESLint 零错误零警告
- 单元测试覆盖率 ≥80%
- 符合 WCAG 2.1 AA 可访问性标准
- 响应式设计支持移动端
请开始生成代码:
"""
"""
你是一位后端 API 集成专家,请根据以下 API 文档生成 TypeScript 服务层代码。
【API 端点信息】
Base URL: {BASE_URL}
Endpoints:
{API 端点列表}
【数据类型】
{API 请求/响应数据结构}
【技术要求】
1. 使用 Axios 作为 HTTP 客户端
2. 遵循以下规范:
- 服务文件命名:*Service.ts
- 所有方法必须是 async 函数
- 明确的泛型类型定义
- 统一的错误处理机制
- 请求/响应拦截器已配置
【代码约束】
1. 端点路径必须定义为常量
2. 每个方法必须有完整的 JSDoc
3. 必须处理常见错误场景(401, 403, 404, 500)
4. 支持请求取消(AbortController)
5. 添加适当的超时配置
【输出要求】
1. 类型定义文件 (types/*.types.ts)
2. API 服务实现文件 (services/*Service.ts)
3. 使用示例代码
请开始生成代码:
"""
# .github/workflows/frontend-ci.yml
name: Frontend CI/CD Pipeline
on:
push:
branches: ['main', 'develop']
pull_request:
branches: ['main', 'develop']
jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint -- --max-warnings=0
- name: Type Check
run: npm run typecheck
test:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: './coverage/lcov.info'
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Analyze bundle size
run: npm run analyze
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-dist
path: dist/
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to K8s
run: |
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/frontend
# Dockerfile - 多阶段构建优化镜像大小
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]