基于 OpenClaw + Claude Code 的端到端研发自动化系统 · 前端技术架构规范
本模板为基于 OpenClaw + Claude Code 的端到端研发自动化系统中的前端技术方案标准规范,涵盖页面结构、组件设计、交互规范等核心内容,支持从需求到部署的全流程自动化研发。
src/ 目录结构
src/
├── assets/ # 静态资源
│ ├── images/ # 图片资源
│ ├── fonts/ # 字体文件
│ └── styles/ # 全局样式
├── components/ # 组件库
│ ├── atoms/ # 原子组件
│ ├── molecules/ # 分子组件
│ ├── organisms/ # 生物组件
│ └── templates/ # 模板组件
├── pages/ # 页面组件
├── hooks/ # 自定义 Hooks
├── stores/ # 状态管理
├── services/ # API 服务层
├── utils/ # 工具函数
├── types/ # TypeScript 类型定义
├── constants/ # 常量定义
└── config/ # 配置文件
| 布局类型 | 适用场景 | 组件示例 | 响应式断点 |
|---|---|---|---|
| 单栏布局 | 移动端、简单表单页 | LoginPage, FormPage | <768px |
| 双栏布局 | 后台管理、列表详情页 | Dashboard, AdminPanel | ≥768px |
| 三栏布局 | 复杂数据展示、对比分析 | DataAnalysis, ComparisonView | ≥1200px |
| 网格布局 | 卡片展示、图片墙 | Gallery, ProductGrid | 自适应 |
路由配置示例 (React Router v6)
// routes/index.tsx
import { createBrowserRouter } from 'react-router-dom';
export const router = createBrowserRouter([
{
path: '/',
element: <MainLayout />,
children: [
{
index: true,
element: <HomePage />,
loader: homeLoader,
},
{
path: 'dashboard',
element: <DashboardPage />,
loader: dashboardLoader,
errorElement: <ErrorBoundary />,
},
{
path: 'products/:id',
element: <ProductDetail />,
loader: productLoader,
},
],
},
{
path: '/auth',
element: <AuthLayout />,
children: [
{ path: 'login', element: <LoginPage /> },
{ path: 'register', element: <RegisterPage /> },
],
},
]);
Zustand Store 示例
// stores/userStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface UserState {
user: UserInfo | null;
token: string | null;
isAuthenticated: boolean;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
updateUser: (data: Partial<UserInfo>) => void;
}
export const useUserStore = create<UserState>()(
devtools(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (credentials) => {
const response = await authService.login(credentials);
set({
user: response.user,
token: response.token,
isAuthenticated: true
});
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
},
updateUser: (data) => {
set((state) => ({
user: state.user ? { ...state.user, ...data } : null
}));
},
}),
{ name: 'user-storage' }
)
)
);
基础 UI 元素,不可再分
原子的组合,具有简单功能
复杂的功能模块
页面布局结构
TypeScript Props 定义示例
// components/atoms/Button/Button.types.ts
import { ReactNode, MouseEventHandler } from 'react';
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';
export type ButtonHTMLType = 'button' | 'submit' | 'reset';
export interface ButtonProps {
// 基础属性
variant?: ButtonVariant;
size?: ButtonSize;
type?: ButtonHTMLType;
disabled?: boolean;
loading?: boolean;
// 内容
children: ReactNode;
icon?: ReactNode;
iconPosition?: 'left' | 'right';
// 事件
onClick?: MouseEventHandler<HTMLButtonElement>;
// 样式扩展
className?: string;
style?: React.CSSProperties;
// 无障碍
ariaLabel?: string;
// 数据测试
'data-testid'?: string;
}
// components/atoms/Button/Button.tsx
export const Button: FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
type = 'button',
disabled = false,
loading = false,
children,
icon,
iconPosition = 'left',
onClick,
className = '',
style,
ariaLabel,
'data-testid': testId,
}) => {
// 组件实现...
};
高阶组件 (HOC) 示例
// hoc/withLoading.tsx
import React from 'react';
interface WithLoadingProps {
isLoading: boolean;
loadingComponent?: React.ReactNode;
}
export function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & WithLoadingProps
) {
const { isLoading, loadingComponent, ...rest } = props;
if (isLoading) {
return loadingComponent || <Spinner />;
}
return <WrappedComponent {...(rest as P)} />;
};
}
// 使用示例
const EnhancedTable = withLoading(DataTable);
<EnhancedTable
data={tableData}
isLoading={isFetching}
/>
Storybook 故事示例
// components/atoms/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const WithIcon: Story = {
args: {
variant: 'primary',
icon: <SaveIcon />,
children: 'Save',
},
};
export const Loading: Story = {
args: {
variant: 'primary',
loading: true,
children: 'Loading...',
},
};
| 动画类型 | 持续时间 | 缓动函数 | 应用场景 |
|---|---|---|---|
| 微交互 | 150ms | ease-out | 按钮悬停、图标变化 |
| 过渡动画 | 300ms | ease-in-out | 页面切换、模态框显示 |
| 复杂动画 | 500ms | cubic-bezier | 数据可视化、图表加载 |
| 反馈动画 | 200ms | ease | 成功/错误提示、加载状态 |
CSS 动画配置
// styles/animations.css
:root {
--transition-fast: 150ms ease-out;
--transition-normal: 300ms ease-in-out;
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-in {
animation: fadeIn var(--transition-normal);
}
.slide-up {
animation: slideUp var(--transition-normal);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
无障碍最佳实践
// 1. 语义化 HTML
<nav aria-label="主导航">
<ul role="menubar">
<li role="none">
<a role="menuitem" href="/home">首页</a>
</li>
</ul>
</nav>
// 2. 键盘导航
<button
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
tabIndex={0}
>
可聚焦按钮
</button>
// 3. 屏幕阅读器支持
<div role="alert" aria-live="polite">
{notificationMessage}
</div>
// 4. 焦点管理
useEffect(() => {
if (isOpen) {
firstInputRef.current?.focus();
}
}, [isOpen]);
// 5. 颜色对比度
// 确保文本与背景对比度 ≥ 4.5:1 (WCAG AA)
API 服务封装示例
// services/api.ts
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
class ApiService {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
const token = useUserStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
useUserStore.getState().logout();
window.location.href = '/auth/login';
}
return Promise.reject(error);
}
);
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.get(url, config);
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.post(url, data, config);
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.put(url, data, config);
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.delete(url, config);
}
}
export const api = new ApiService(import.meta.env.VITE_API_BASE_URL);
自定义 Hooks 示例
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productService } from '@/services/productService';
import type { Product, ProductFilters } from '@/types/product';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => productService.getProducts(filters),
staleTime: 5 * 60 * 1000, // 5 分钟
});
}
export function useProduct(id: string) {
return useQuery({
queryKey: ['product', id],
queryFn: () => productService.getProduct(id),
enabled: !!id,
});
}
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: productService.createProduct,
onSuccess: () => {
queryClient.invalidateQueries(['products']);
},
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: productService.updateProduct,
onSuccess: (_, variables) => {
queryClient.invalidateQueries(['products']);
queryClient.invalidateQueries(['product', variables.id]);
},
});
}
组件测试示例
// components/atoms/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Loading</Button>);
expect(screen.getByRole('button')).toHaveAttribute('disabled');
});
it('respects disabled state', () => {
const handleClick = jest.fn();
render(
<Button disabled onClick={handleClick}>
Disabled
</Button>
);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});
E2E 测试示例
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/auth/login');
});
test('successful login', async ({ page }) => {
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="submit-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});
test('invalid credentials', async ({ page }) => {
await page.fill('[data-testid="email-input"]', 'wrong@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpass');
await page.click('[data-testid="submit-button"]');
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('Invalid credentials');
});
});
.github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Deploy to K8s
run: |
kubectl apply -f k8s/deployment.yaml
kubectl rollout restart deployment/frontend
Dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
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;"]
AI 代码生成 Prompt 模板
## 任务描述
请根据以下技术要求生成一个 React 组件:
## 组件名称
{ComponentName}
## 功能需求
- {Requirement1}
- {Requirement2}
- {Requirement3}
## 技术栈
- React 18 + TypeScript
- TailwindCSS for styling
- Zustand for state management
## Props 接口
{TypeScript Interface}
## 设计规范
- 遵循原子设计原则
- 支持暗色模式
- 包含完整的无障碍支持
- 添加 Storybook 故事
## 测试要求
- Jest + React Testing Library 单元测试
- 覆盖率要求:>90%
## 输出格式
请按以下结构输出:
1. 组件 TypeScript 文件
2. 样式文件 (如需要)
3. Storybook 故事文件
4. 测试文件
| 类别 | 推荐库 | 用途 |
|---|---|---|
| 状态管理 | Zustand, Redux Toolkit | 全局状态管理 |
| 数据获取 | React Query, SWR | 服务端状态管理 |
| 表单处理 | React Hook Form, Formik | 表单验证与管理 |
| UI 组件 | Radix UI, Headless UI | 无样式组件库 |
| 样式方案 | TailwindCSS, Styled Components | CSS 解决方案 |
| 测试框架 | Jest, Vitest, Playwright | 单元/E2E 测试 |