
React测试平台前端开发指南
大约 7 分钟
React测试平台前端开发指南
后端搭好了,现在该给我们的测试平台穿上漂亮的"外衣"了!今天我们用React + Ant Design打造一个颜值与实力并存的前端界面。别担心,我会用最通俗易懂的方式教你,保证比追剧还有趣!
🎨 为什么选择React + Ant Design?
技术选型:站在巨人的肩膀上
想象一下你要装修房子:
- React就像是房子的框架结构,稳固可靠,扩展性强
- Ant Design就像是精装修套餐,开箱即用,颜值在线
- TypeScript就像是质量检测员,帮你避免低级错误
为什么不选Vue?
- React生态更丰富,找工作机会更多
- 组件化思想更彻底,适合大型项目
- 社区活跃,遇到问题容易找到解决方案
为什么选择Ant Design?
- 阿里出品,专为企业级应用设计
- 组件丰富,基本不用自己造轮子
- 设计规范统一,UI一致性好
- 文档详细,上手简单
🏗️ 项目结构设计
目录结构:井井有条的代码家园
react_test_platform/
├── public/
│ ├── index.html
│ └── favicon.ico
├── src/
│ ├── components/ # 通用组件
│ │ ├── Layout/ # 布局组件
│ │ ├── Loading/ # 加载组件
│ │ └── ErrorBoundary/ # 错误边界
│ ├── pages/ # 页面组件
│ │ ├── Project/ # 项目管理页面
│ │ ├── TestCase/ # 用例管理页面
│ │ ├── Report/ # 报告页面
│ │ └── Dashboard/ # 仪表盘
│ ├── services/ # API服务
│ │ ├── api.ts # API配置
│ │ ├── project.ts # 项目相关API
│ │ └── testcase.ts # 用例相关API
│ ├── hooks/ # 自定义Hook
│ │ ├── useApi.ts # API调用Hook
│ │ └── useTable.ts # 表格Hook
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── storage.ts # 本地存储
│ │ └── constants.ts # 常量定义
│ ├── types/ # TypeScript类型定义
│ │ ├── api.ts # API类型
│ │ └── common.ts # 通用类型
│ ├── styles/ # 样式文件
│ │ ├── global.less # 全局样式
│ │ └── variables.less # 样式变量
│ ├── App.tsx # 根组件
│ └── index.tsx # 入口文件
├── package.json
├── tsconfig.json
└── craco.config.js # 构建配置项目初始化:从零到一的魔法
1. 创建React项目:
# 使用Create React App创建项目
npx create-react-app react_test_platform --template typescript
# 进入项目目录
cd react_test_platform
# 安装Ant Design
npm install antd
# 安装其他依赖
npm install axios dayjs @types/node2. 配置Ant Design主题:
# 安装craco用于自定义配置
npm install @craco/craco craco-lesscraco.config.js:
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
'@primary-color': '#1890ff',
'@border-radius-base': '6px',
},
javascriptEnabled: true,
},
},
},
},
],
};🔧 核心组件开发
布局组件:应用的"骨架"
src/components/Layout/MainLayout.tsx:
import React, { useState } from 'react';
import { Layout, Menu, Avatar, Dropdown, Space } from 'antd';
import {
DashboardOutlined,
ProjectOutlined,
FileTextOutlined,
BarChartOutlined,
UserOutlined,
LogoutOutlined,
SettingOutlined
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import './MainLayout.less';
const { Header, Sider, Content } = Layout;
interface MainLayoutProps {
children: React.ReactNode;
}
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const location = useLocation();
// 菜单配置
const menuItems = [
{
key: '/dashboard',
icon: <DashboardOutlined />,
label: '仪表盘',
},
{
key: '/projects',
icon: <ProjectOutlined />,
label: '项目管理',
},
{
key: '/testcases',
icon: <FileTextOutlined />,
label: '用例管理',
},
{
key: '/reports',
icon: <BarChartOutlined />,
label: '测试报告',
},
];
// 用户下拉菜单
const userMenuItems = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人信息',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置',
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
const handleMenuClick = ({ key }: { key: string }) => {
navigate(key);
};
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') {
// 处理退出登录逻辑
localStorage.removeItem('token');
navigate('/login');
}
};
return (
<Layout className="main-layout">
<Sider
trigger={null}
collapsible
collapsed={collapsed}
className="layout-sider"
>
<div className="logo">
<span>测试平台</span>
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems}
onClick={handleMenuClick}
/>
</Sider>
<Layout className="site-layout">
<Header className="layout-header">
<div className="header-left">
<button
className="trigger"
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? '展开' : '收起'}
</button>
</div>
<div className="header-right">
<Space>
<Dropdown
menu={{
items: userMenuItems,
onClick: handleUserMenuClick,
}}
placement="bottomRight"
>
<Space className="user-info">
<Avatar icon={<UserOutlined />} />
<span>测试工程师</span>
</Space>
</Dropdown>
</Space>
</div>
</Header>
<Content className="layout-content">
{children}
</Content>
</Layout>
</Layout>
);
};
export default MainLayout;API服务封装:与后端的"桥梁"
src/utils/request.ts:
import axios, { AxiosResponse, AxiosError } from 'axios';
import { message } from 'antd';
// API响应接口
export interface ApiResponse<T = any> {
success: boolean;
code: number;
message: string;
data: T;
}
// 创建axios实例
const request = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:5000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { data } = response;
// 如果是成功响应,直接返回数据
if (data.success) {
return response;
}
// 如果是业务错误,显示错误信息
message.error(data.message || '请求失败');
return Promise.reject(new Error(data.message || '请求失败'));
},
(error: AxiosError<ApiResponse>) => {
// 处理HTTP错误
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
message.error('登录已过期,请重新登录');
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
message.error('没有权限访问');
break;
case 404:
message.error('请求的资源不存在');
break;
case 500:
message.error('服务器内部错误');
break;
default:
message.error(data?.message || '请求失败');
}
} else if (error.request) {
message.error('网络连接失败,请检查网络');
} else {
message.error('请求配置错误');
}
return Promise.reject(error);
}
);
export default request;项目管理页面:CRUD操作的完美演示
src/pages/Project/ProjectList.tsx:
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Card,
Row,
Col,
Statistic
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined
} from '@ant-design/icons';
import { ColumnsType } from 'antd/es/table';
import { useNavigate } from 'react-router-dom';
import { projectApi } from '../../services/project';
import { Project } from '../../types/api';
import './ProjectList.less';
const { Option } = Select;
const { TextArea } = Input;
const ProjectList: React.FC = () => {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingProject, setEditingProject] = useState<Project | null>(null);
const [form] = Form.useForm();
const navigate = useNavigate();
// 表格列配置
const columns: ColumnsType<Project> = [
{
title: '项目名称',
dataIndex: 'name',
key: 'name',
render: (text, record) => (
<Button
type="link"
onClick={() => navigate(`/projects/${record.id}`)}
>
{text}
</Button>
),
},
{
title: '项目描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '负责人',
dataIndex: 'owner',
key: 'owner',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status) => (
<span className={`status-${status}`}>
{status === 'active' ? '活跃' : '非活跃'}
</span>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (text) => new Date(text).toLocaleDateString(),
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space size="middle">
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => navigate(`/projects/${record.id}`)}
>
查看
</Button>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
编辑
</Button>
<Popconfirm
title="确定要删除这个项目吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
// 获取项目列表
const fetchProjects = async () => {
setLoading(true);
try {
const response = await projectApi.getProjects();
setProjects(response.data.data.items);
} catch (error) {
message.error('获取项目列表失败');
} finally {
setLoading(false);
}
};
// 创建/更新项目
const handleSubmit = async (values: any) => {
try {
if (editingProject) {
await projectApi.updateProject(editingProject.id, values);
message.success('项目更新成功');
} else {
await projectApi.createProject(values);
message.success('项目创建成功');
}
setModalVisible(false);
setEditingProject(null);
form.resetFields();
fetchProjects();
} catch (error) {
message.error('操作失败');
}
};
// 编辑项目
const handleEdit = (project: Project) => {
setEditingProject(project);
form.setFieldsValue(project);
setModalVisible(true);
};
// 删除项目
const handleDelete = async (id: number) => {
try {
await projectApi.deleteProject(id);
message.success('项目删除成功');
fetchProjects();
} catch (error) {
message.error('删除失败');
}
};
// 新建项目
const handleCreate = () => {
setEditingProject(null);
form.resetFields();
setModalVisible(true);
};
useEffect(() => {
fetchProjects();
}, []);
return (
<div className="project-list">
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总项目数"
value={projects.length}
valueStyle={{ color: '#3f8600' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="活跃项目"
value={projects.filter(p => p.status === 'active').length}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
</Row>
{/* 操作栏 */}
<Card>
<div className="table-header">
<h3>项目列表</h3>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
新建项目
</Button>
</div>
{/* 项目表格 */}
<Table
columns={columns}
dataSource={projects}
loading={loading}
rowKey="id"
pagination={{
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条记录`,
}}
/>
</Card>
{/* 创建/编辑模态框 */}
<Modal
title={editingProject ? '编辑项目' : '新建项目'}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setEditingProject(null);
form.resetFields();
}}
footer={null}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="name"
label="项目名称"
rules={[{ required: true, message: '请输入项目名称' }]}
>
<Input placeholder="请输入项目名称" />
</Form.Item>
<Form.Item
name="description"
label="项目描述"
>
<TextArea
rows={4}
placeholder="请输入项目描述"
/>
</Form.Item>
<Form.Item
name="owner"
label="项目负责人"
rules={[{ required: true, message: '请输入负责人' }]}
>
<Input placeholder="请输入负责人" />
</Form.Item>
<Form.Item
name="status"
label="项目状态"
initialValue="active"
>
<Select>
<Option value="active">活跃</Option>
<Option value="inactive">非活跃</Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{editingProject ? '更新' : '创建'}
</Button>
<Button onClick={() => setModalVisible(false)}>
取消
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default ProjectList;🎯 下一步预告
今天我们用React + Ant Design搭建了一个现代化的前端界面,实现了:
- 清晰的项目结构
- 优雅的布局组件
- 完善的API封装
- 功能完整的项目管理页面
下一篇我们将深入测试用例管理系统的设计与实现,这是测试平台的核心功能,也是最有挑战性的部分。准备好迎接挑战了吗?
💡 前端开发小贴士:写React组件就像搭乐高积木,每个组件都应该职责单一、可复用。记住,用户体验是王道,功能再强大,界面难用也是白搭!
