
前端UI框架与组件库
大约 20 分钟
前端UI框架与组件库
如果说前端框架是"汽车引擎",那么UI组件库就是"豪华内饰"——让你的应用不仅跑得快,还要看起来美。UI组件库就像是前端开发的"宜家家具",提供现成的、设计精美的组件,让你快速搭建出专业级的界面。作为测试开发工程师,掌握UI组件库就像拥有了"装修大师"的技能,能让测试工具界面瞬间提升几个档次!
一、UI组件库概述:站在巨人的肩膀上
为什么要使用UI组件库?
想象一下,如果每次做饭都要从种菜开始,那得多累?UI组件库就是前端开发的"半成品菜":
- 开发效率:现成的组件,拿来即用
- 设计一致性:统一的设计语言和视觉风格
- 质量保证:经过大量项目验证的稳定组件
- 响应式支持:自动适配不同设备屏幕
- 无障碍访问:内置可访问性支持
主流UI组件库对比
| 组件库 | 适用框架 | 设计风格 | 组件数量 | 测试平台适用度 | 学习难度 |
|---|---|---|---|---|---|
| Ant Design | React | 企业级 | 60+ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Element Plus | Vue | 简洁现代 | 50+ | ⭐⭐⭐⭐ | ⭐⭐ |
| Arco Design | React/Vue | 现代化 | 40+ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Naive UI | Vue | 简约 | 80+ | ⭐⭐⭐ | ⭐⭐ |
| Chakra UI | React | 简单灵活 | 50+ | ⭐⭐⭐ | ⭐⭐ |
二、Ant Design:企业级设计语言
Ant Design简介
Ant Design(简称antd)是蚂蚁集团开源的企业级UI设计语言,特别适合构建测试平台这类B端应用。
快速开始
# 安装Ant Design
npm install antd
# 如果使用TypeScript
npm install @types/antd// App.jsx
import React from 'react';
import { ConfigProvider, App } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import TestPlatform from './components/TestPlatform';
import 'antd/dist/reset.css'; // 重置样式
function MyApp() {
return (
<ConfigProvider locale={zhCN}>
<App>
<TestPlatform />
</App>
</ConfigProvider>
);
}
export default MyApp;核心组件实战
1. 布局组件:构建页面骨架
import React, { useState } from 'react';
import {
Layout,
Menu,
Breadcrumb,
Avatar,
Dropdown,
Space,
Badge
} from 'antd';
import {
DashboardOutlined,
ExperimentOutlined,
PlayCircleOutlined,
BarChartOutlined,
SettingOutlined,
UserOutlined,
BellOutlined
} from '@ant-design/icons';
const { Header, Sider, Content } = Layout;
function TestPlatformLayout({ children }) {
const [collapsed, setCollapsed] = useState(false);
// 菜单配置
const menuItems = [
{
key: 'dashboard',
icon: <DashboardOutlined />,
label: '仪表盘',
path: '/'
},
{
key: 'testcases',
icon: <ExperimentOutlined />,
label: '测试用例',
children: [
{ key: 'testcase-list', label: '用例列表', path: '/testcases' },
{ key: 'testcase-create', label: '创建用例', path: '/testcases/create' }
]
},
{
key: 'execution',
icon: <PlayCircleOutlined />,
label: '测试执行',
path: '/execution'
},
{
key: 'reports',
icon: <BarChartOutlined />,
label: '测试报告',
path: '/reports'
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置',
path: '/settings'
}
];
// 用户菜单
const userMenuItems = [
{ key: 'profile', label: '个人资料' },
{ key: 'settings', label: '账户设置' },
{ type: 'divider' },
{ key: 'logout', label: '退出登录' }
];
return (
<Layout style={{ minHeight: '100vh' }}>
{/* 侧边栏 */}
<Sider
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
theme="light"
width={250}
>
<div className="logo" style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 18,
fontWeight: 'bold'
}}>
🧪 测试平台
</div>
<Menu
mode="inline"
defaultSelectedKeys={['dashboard']}
items={menuItems}
style={{ borderRight: 0 }}
/>
</Sider>
<Layout>
{/* 顶部导航 */}
<Header style={{
background: '#fff',
padding: '0 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
}}>
<Breadcrumb>
<Breadcrumb.Item>测试平台</Breadcrumb.Item>
<Breadcrumb.Item>测试用例</Breadcrumb.Item>
<Breadcrumb.Item>用例列表</Breadcrumb.Item>
</Breadcrumb>
<Space size="large">
<Badge count={5}>
<BellOutlined style={{ fontSize: 18 }} />
</Badge>
<Dropdown
menu={{ items: userMenuItems }}
placement="bottomRight"
>
<Space style={{ cursor: 'pointer' }}>
<Avatar icon={<UserOutlined />} />
<span>张三</span>
</Space>
</Dropdown>
</Space>
</Header>
{/* 主内容区 */}
<Content style={{
margin: '24px',
padding: '24px',
background: '#fff',
borderRadius: '8px'
}}>
{children}
</Content>
</Layout>
</Layout>
);
}
export default TestPlatformLayout;2. 表格组件:数据展示利器
import React, { useState } from 'react';
import {
Table,
Button,
Space,
Tag,
Popconfirm,
Input,
Select,
DatePicker,
message
} from 'antd';
import {
PlayCircleOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined
} from '@ant-design/icons';
const { Search } = Input;
const { RangePicker } = DatePicker;
function TestCaseTable() {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
// 模拟数据
const dataSource = [
{
key: '1',
id: 1,
name: '用户登录功能测试',
module: '用户管理',
priority: 'high',
status: 'passed',
author: '张三',
lastRun: '2024-01-15 10:30:00',
duration: '2.5s'
},
{
key: '2',
id: 2,
name: '商品搜索接口测试',
module: '商品管理',
priority: 'medium',
status: 'failed',
author: '李四',
lastRun: '2024-01-15 09:45:00',
duration: '1.8s'
},
{
key: '3',
id: 3,
name: '订单创建流程测试',
module: '订单管理',
priority: 'high',
status: 'pending',
author: '王五',
lastRun: null,
duration: null
}
];
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 80,
sorter: (a, b) => a.id - b.id
},
{
title: '用例名称',
dataIndex: 'name',
ellipsis: true,
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
placeholder="搜索用例名称"
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => confirm()}
style={{ marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => confirm()}
icon={<SearchOutlined />}
size="small"
>
搜索
</Button>
<Button onClick={() => clearFilters()} size="small">
重置
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) => record.name.toLowerCase().includes(value.toLowerCase())
},
{
title: '所属模块',
dataIndex: 'module',
filters: [
{ text: '用户管理', value: '用户管理' },
{ text: '商品管理', value: '商品管理' },
{ text: '订单管理', value: '订单管理' }
],
onFilter: (value, record) => record.module === value
},
{
title: '优先级',
dataIndex: 'priority',
render: (priority) => {
const config = {
high: { color: 'red', text: '高' },
medium: { color: 'orange', text: '中' },
low: { color: 'green', text: '低' }
};
return <Tag color={config[priority]?.color}>{config[priority]?.text}</Tag>;
},
filters: [
{ text: '高', value: 'high' },
{ text: '中', value: 'medium' },
{ text: '低', value: 'low' }
],
onFilter: (value, record) => record.priority === value
},
{
title: '执行状态',
dataIndex: 'status',
render: (status) => {
const config = {
passed: { color: 'success', text: '✅ 通过' },
failed: { color: 'error', text: '❌ 失败' },
pending: { color: 'warning', text: '⏳ 待执行' }
};
return <Tag color={config[status]?.color}>{config[status]?.text}</Tag>;
}
},
{
title: '创建人',
dataIndex: 'author'
},
{
title: '最后执行时间',
dataIndex: 'lastRun',
sorter: (a, b) => new Date(a.lastRun || 0) - new Date(b.lastRun || 0),
render: (time) => time || '-'
},
{
title: '执行耗时',
dataIndex: 'duration',
render: (duration) => duration || '-'
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
render: (_, record) => (
<Space size="small">
<Button
type="primary"
icon={<PlayCircleOutlined />}
size="small"
onClick={() => handleExecute(record)}
>
执行
</Button>
<Button
icon={<EditOutlined />}
size="small"
onClick={() => handleEdit(record)}
>
编辑
</Button>
<Popconfirm
title="确定要删除这个测试用例吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button
danger
icon={<DeleteOutlined />}
size="small"
>
删除
</Button>
</Popconfirm>
</Space>
)
}
];
// 行选择配置
const rowSelection = {
selectedRowKeys,
onChange: setSelectedRowKeys,
onSelectAll: (selected, selectedRows, changeRows) => {
console.log('选择所有:', selected, selectedRows, changeRows);
}
};
// 事件处理
const handleExecute = (record) => {
message.info(`执行测试用例: ${record.name}`);
};
const handleEdit = (record) => {
message.info(`编辑测试用例: ${record.name}`);
};
const handleDelete = (record) => {
message.success(`删除测试用例: ${record.name}`);
};
const handleBatchExecute = () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择要执行的测试用例');
return;
}
message.info(`批量执行 ${selectedRowKeys.length} 个测试用例`);
};
return (
<div className="test-case-table">
{/* 工具栏 */}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Space>
<Button type="primary">新建用例</Button>
<Button
onClick={handleBatchExecute}
disabled={selectedRowKeys.length === 0}
>
批量执行
</Button>
<Button>导入用例</Button>
<Button>导出报告</Button>
</Space>
<Space>
<Search
placeholder="搜索测试用例"
allowClear
style={{ width: 250 }}
onSearch={(value) => console.log('搜索:', value)}
/>
<Select
placeholder="选择模块"
style={{ width: 120 }}
allowClear
>
<Select.Option value="user">用户管理</Select.Option>
<Select.Option value="product">商品管理</Select.Option>
<Select.Option value="order">订单管理</Select.Option>
</Select>
<RangePicker placeholder={['开始时间', '结束时间']} />
</Space>
</div>
{/* 表格 */}
<Table
columns={columns}
dataSource={dataSource}
rowSelection={rowSelection}
loading={loading}
pagination={{
total: 100,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
}}
scroll={{ x: 1200 }}
size="middle"
/>
</div>
);
}
export default TestCaseTable;3. 表单组件:数据录入专家
import React, { useState } from 'react';
import {
Form,
Input,
Select,
Radio,
Checkbox,
Switch,
Slider,
DatePicker,
Upload,
Button,
Card,
Divider,
Space,
message
} from 'antd';
import {
UploadOutlined,
PlusOutlined,
MinusCircleOutlined
} from '@ant-design/icons';
const { TextArea } = Input;
const { Option } = Select;
function TestCaseForm({ initialValues, onSubmit }) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// 表单提交
const handleSubmit = async (values) => {
setLoading(true);
try {
console.log('表单数据:', values);
await onSubmit?.(values);
message.success('保存成功');
form.resetFields();
} catch (error) {
message.error('保存失败');
} finally {
setLoading(false);
}
};
// 文件上传配置
const uploadProps = {
name: 'file',
action: '/api/upload',
headers: {
authorization: 'Bearer ' + localStorage.getItem('token'),
},
onChange(info) {
if (info.file.status === 'done') {
message.success(`${info.file.name} 上传成功`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 上传失败`);
}
},
};
return (
<Card title="测试用例信息" style={{ maxWidth: 800, margin: '0 auto' }}>
<Form
form={form}
layout="vertical"
initialValues={initialValues}
onFinish={handleSubmit}
autoComplete="off"
>
{/* 基本信息 */}
<Divider orientation="left">基本信息</Divider>
<Form.Item
label="用例名称"
name="name"
rules={[
{ required: true, message: '请输入用例名称' },
{ min: 5, message: '用例名称至少5个字符' },
{ max: 100, message: '用例名称不能超过100个字符' }
]}
>
<Input placeholder="请输入测试用例名称" />
</Form.Item>
<Form.Item
label="用例描述"
name="description"
rules={[{ required: true, message: '请输入用例描述' }]}
>
<TextArea
rows={4}
placeholder="请详细描述测试用例的目的和预期结果"
showCount
maxLength={500}
/>
</Form.Item>
<Form.Item
label="所属模块"
name="module"
rules={[{ required: true, message: '请选择所属模块' }]}
>
<Select placeholder="请选择模块">
<Option value="user">用户管理</Option>
<Option value="product">商品管理</Option>
<Option value="order">订单管理</Option>
<Option value="payment">支付管理</Option>
</Select>
</Form.Item>
<Form.Item
label="优先级"
name="priority"
rules={[{ required: true, message: '请选择优先级' }]}
>
<Radio.Group>
<Radio value="high">🔴 高</Radio>
<Radio value="medium">🟡 中</Radio>
<Radio value="low">🟢 低</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label="测试类型"
name="types"
>
<Checkbox.Group>
<Checkbox value="functional">功能测试</Checkbox>
<Checkbox value="performance">性能测试</Checkbox>
<Checkbox value="security">安全测试</Checkbox>
<Checkbox value="compatibility">兼容性测试</Checkbox>
</Checkbox.Group>
</Form.Item>
{/* 执行配置 */}
<Divider orientation="left">执行配置</Divider>
<Form.Item
label="自动执行"
name="autoRun"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</Form.Item>
<Form.Item
label="超时时间(秒)"
name="timeout"
>
<Slider
min={1}
max={300}
marks={{
1: '1s',
60: '1min',
180: '3min',
300: '5min'
}}
/>
</Form.Item>
<Form.Item
label="预期执行时间"
name="expectedDate"
>
<DatePicker
showTime
placeholder="选择预期执行时间"
style={{ width: '100%' }}
/>
</Form.Item>
{/* 测试步骤 */}
<Divider orientation="left">测试步骤</Divider>
<Form.List name="steps">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space
key={key}
style={{ display: 'flex', marginBottom: 8 }}
align="baseline"
>
<Form.Item
{...restField}
name={[name, 'step']}
rules={[{ required: true, message: '请输入测试步骤' }]}
style={{ flex: 1 }}
>
<Input placeholder="测试步骤描述" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'expected']}
rules={[{ required: true, message: '请输入预期结果' }]}
style={{ flex: 1 }}
>
<Input placeholder="预期结果" />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
添加测试步骤
</Button>
</Form.Item>
</>
)}
</Form.List>
{/* 附件上传 */}
<Divider orientation="left">附件</Divider>
<Form.Item
label="相关文档"
name="attachments"
>
<Upload {...uploadProps} multiple>
<Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>
</Form.Item>
{/* 提交按钮 */}
<Form.Item style={{ marginTop: 32 }}>
<Space>
<Button
type="primary"
htmlType="submit"
loading={loading}
size="large"
>
保存用例
</Button>
<Button size="large" onClick={() => form.resetFields()}>
重置
</Button>
<Button size="large">
预览
</Button>
</Space>
</Form.Item>
</Form>
</Card>
);
}
export default TestCaseForm;三、Element Plus:Vue生态的明星
Element Plus简介
Element Plus是饿了么团队开源的Vue 3组件库,设计简洁现代,特别适合快速开发。
快速开始
# 安装Element Plus
npm install element-plus
# 安装图标库
npm install @element-plus/icons-vue// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')Element Plus核心组件
1. 数据展示:表格和卡片
<template>
<div class="test-case-management">
<!-- 搜索栏 -->
<el-card class="search-card" shadow="never">
<el-form :model="searchForm" inline>
<el-form-item label="用例名称">
<el-input
v-model="searchForm.name"
placeholder="请输入用例名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="执行状态">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="通过" value="passed" />
<el-option label="失败" value="failed" />
<el-option label="待执行" value="pending" />
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select
v-model="searchForm.priority"
placeholder="请选择优先级"
clearable
style="width: 120px"
>
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作栏 -->
<el-card class="toolbar-card" shadow="never">
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建用例
</el-button>
<el-button
type="success"
:disabled="!hasSelection"
@click="handleBatchExecute"
>
<el-icon><VideoPlay /></el-icon>
批量执行
</el-button>
<el-button @click="handleImport">
<el-icon><Upload /></el-icon>
导入用例
</el-button>
</div>
<div class="toolbar-right">
<el-button @click="handleExport">
<el-icon><Download /></el-icon>
导出报告
</el-button>
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never">
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" sortable />
<el-table-column prop="name" label="用例名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="module" label="所属模块" width="120">
<template #default="{ row }">
<el-tag type="info">{{ row.module }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag
:type="getPriorityType(row.priority)"
effect="dark"
>
{{ getPriorityText(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="执行状态" width="120">
<template #default="{ row }">
<el-tag
:type="getStatusType(row.status)"
:icon="getStatusIcon(row.status)"
>
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="author" label="创建人" width="100" />
<el-table-column prop="lastRun" label="最后执行时间" width="180">
<template #default="{ row }">
{{ row.lastRun || '-' }}
</template>
</el-table-column>
<el-table-column prop="duration" label="执行耗时" width="100">
<template #default="{ row }">
{{ row.duration || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleExecute(row)"
>
<el-icon><VideoPlay /></el-icon>
执行
</el-button>
<el-button
size="small"
@click="handleEdit(row)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-popconfirm
title="确定要删除这个测试用例吗?"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button
type="danger"
size="small"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import {
Search,
Plus,
VideoPlay,
Upload,
Download,
Refresh,
Edit,
Delete,
SuccessFilled,
CircleCloseFilled,
WarningFilled
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 响应式数据
const loading = ref(false)
const tableData = ref([])
const selectedRows = ref([])
const searchForm = reactive({
name: '',
status: '',
priority: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 计算属性
const hasSelection = computed(() => selectedRows.value.length > 0)
// 状态映射函数
const getPriorityType = (priority) => {
const map = { high: 'danger', medium: 'warning', low: 'success' }
return map[priority] || ''
}
const getPriorityText = (priority) => {
const map = { high: '高', medium: '中', low: '低' }
return map[priority] || priority
}
const getStatusType = (status) => {
const map = { passed: 'success', failed: 'danger', pending: 'warning' }
return map[status] || ''
}
const getStatusText = (status) => {
const map = { passed: '通过', failed: '失败', pending: '待执行' }
return map[status] || status
}
const getStatusIcon = (status) => {
const map = {
passed: SuccessFilled,
failed: CircleCloseFilled,
pending: WarningFilled
}
return map[status]
}
// 事件处理
const handleSearch = () => {
pagination.currentPage = 1
loadData()
}
const handleReset = () => {
Object.assign(searchForm, { name: '', status: '', priority: '' })
handleSearch()
}
const handleCreate = () => {
ElMessage.info('打开创建用例对话框')
}
const handleBatchExecute = () => {
ElMessageBox.confirm(
`确定要执行选中的 ${selectedRows.value.length} 个测试用例吗?`,
'批量执行确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
ElMessage.success('开始批量执行测试用例')
})
}
const handleImport = () => {
ElMessage.info('打开导入对话框')
}
const handleExport = () => {
ElMessage.info('开始导出报告')
}
const handleRefresh = () => {
loadData()
}
const handleExecute = (row) => {
ElMessage.info(`执行测试用例: ${row.name}`)
}
const handleEdit = (row) => {
ElMessage.info(`编辑测试用例: ${row.name}`)
}
const handleDelete = (row) => {
ElMessage.success(`删除测试用例: ${row.name}`)
loadData()
}
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
const handleSizeChange = (size) => {
pagination.pageSize = size
loadData()
}
const handleCurrentChange = (page) => {
pagination.currentPage = page
loadData()
}
// 数据加载
const loadData = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟数据
tableData.value = [
{
id: 1,
name: '用户登录功能测试',
module: '用户管理',
priority: 'high',
status: 'passed',
author: '张三',
lastRun: '2024-01-15 10:30:00',
duration: '2.5s'
},
{
id: 2,
name: '商品搜索接口测试',
module: '商品管理',
priority: 'medium',
status: 'failed',
author: '李四',
lastRun: '2024-01-15 09:45:00',
duration: '1.8s'
},
{
id: 3,
name: '订单创建流程测试',
module: '订单管理',
priority: 'high',
status: 'pending',
author: '王五',
lastRun: null,
duration: null
}
]
pagination.total = 100
} catch (error) {
ElMessage.error('数据加载失败')
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
loadData()
})
</script>
<style scoped>
.test-case-management {
padding: 20px;
}
.search-card,
.toolbar-card {
margin-bottom: 20px;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-left,
.toolbar-right {
display: flex;
gap: 10px;
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
</style>2. 表单组件:数据录入
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑测试用例' : '新建测试用例'"
width="800px"
:before-close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
label-position="left"
>
<el-tabs v-model="activeTab">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<el-form-item label="用例名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入测试用例名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="用例描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="4"
placeholder="请详细描述测试用例的目的和预期结果"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属模块" prop="module">
<el-select
v-model="form.module"
placeholder="请选择模块"
style="width: 100%"
>
<el-option label="用户管理" value="user" />
<el-option label="商品管理" value="product" />
<el-option label="订单管理" value="order" />
<el-option label="支付管理" value="payment" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级" prop="priority">
<el-radio-group v-model="form.priority">
<el-radio label="high">🔴 高</el-radio>
<el-radio label="medium">🟡 中</el-radio>
<el-radio label="low">🟢 低</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="测试类型">
<el-checkbox-group v-model="form.types">
<el-checkbox label="functional">功能测试</el-checkbox>
<el-checkbox label="performance">性能测试</el-checkbox>
<el-checkbox label="security">安全测试</el-checkbox>
<el-checkbox label="compatibility">兼容性测试</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-tab-pane>
<!-- 测试步骤 -->
<el-tab-pane label="测试步骤" name="steps">
<div class="steps-container">
<div
v-for="(step, index) in form.steps"
:key="index"
class="step-item"
>
<div class="step-header">
<span class="step-number">步骤 {{ index + 1 }}</span>
<el-button
type="danger"
size="small"
text
@click="removeStep(index)"
>
删除
</el-button>
</div>
<el-form-item
:prop="`steps.${index}.description`"
:rules="{ required: true, message: '请输入步骤描述' }"
>
<el-input
v-model="step.description"
placeholder="请输入测试步骤描述"
/>
</el-form-item>
<el-form-item
:prop="`steps.${index}.expected`"
:rules="{ required: true, message: '请输入预期结果' }"
>
<el-input
v-model="step.expected"
placeholder="请输入预期结果"
/>
</el-form-item>
</div>
<el-button
type="primary"
dashed
style="width: 100%"
@click="addStep"
>
<el-icon><Plus /></el-icon>
添加测试步骤
</el-button>
</div>
</el-tab-pane>
<!-- 执行配置 -->
<el-tab-pane label="执行配置" name="config">
<el-form-item label="自动执行">
<el-switch
v-model="form.autoRun"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<el-form-item label="超时时间">
<el-slider
v-model="form.timeout"
:min="1"
:max="300"
:marks="{ 1: '1s', 60: '1min', 180: '3min', 300: '5min' }"
style="width: 300px"
/>
<span style="margin-left: 20px">{{ form.timeout }}秒</span>
</el-form-item>
<el-form-item label="预期执行时间">
<el-date-picker
v-model="form.expectedDate"
type="datetime"
placeholder="选择预期执行时间"
style="width: 300px"
/>
</el-form-item>
<el-form-item label="相关文档">
<el-upload
v-model:file-list="form.attachments"
action="/api/upload"
multiple
:limit="5"
:on-exceed="handleExceed"
>
<el-button>
<el-icon><Upload /></el-icon>
上传文件
</el-button>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png/pdf文件,且不超过5MB
</div>
</template>
</el-upload>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button @click="handlePreview">预览</el-button>
<el-button
type="primary"
:loading="submitting"
@click="handleSubmit"
>
{{ isEdit ? '更新' : '创建' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// Props
const props = defineProps({
modelValue: Boolean,
testCase: Object
})
// Emits
const emit = defineEmits(['update:modelValue', 'submit'])
// 响应式数据
const formRef = ref()
const activeTab = ref('basic')
const submitting = ref(false)
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const isEdit = computed(() => !!props.testCase?.id)
const form = reactive({
name: '',
description: '',
module: '',
priority: 'medium',
types: [],
steps: [{ description: '', expected: '' }],
autoRun: false,
timeout: 60,
expectedDate: null,
attachments: []
})
// 表单验证规则
const rules = {
name: [
{ required: true, message: '请输入用例名称', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入用例描述', trigger: 'blur' }
],
module: [
{ required: true, message: '请选择所属模块', trigger: 'change' }
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' }
]
}
// 监听testCase变化,初始化表单
watch(() => props.testCase, (newVal) => {
if (newVal) {
Object.assign(form, newVal)
} else {
resetForm()
}
}, { immediate: true })
// 方法
const addStep = () => {
form.steps.push({ description: '', expected: '' })
}
const removeStep = (index) => {
if (form.steps.length > 1) {
form.steps.splice(index, 1)
} else {
ElMessage.warning('至少保留一个测试步骤')
}
}
const handleExceed = () => {
ElMessage.warning('最多只能上传5个文件')
}
const handlePreview = () => {
ElMessage.info('打开预览窗口')
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitting.value = true
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
emit('submit', { ...form })
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
handleClose()
} catch (error) {
console.error('表单验证失败:', error)
} finally {
submitting.value = false
}
}
const handleClose = () => {
visible.value = false
resetForm()
}
const resetForm = () => {
Object.assign(form, {
name: '',
description: '',
module: '',
priority: 'medium',
types: [],
steps: [{ description: '', expected: '' }],
autoRun: false,
timeout: 60,
expectedDate: null,
attachments: []
})
formRef.value?.resetFields()
}
</script>
<style scoped>
.steps-container {
max-height: 400px;
overflow-y: auto;
}
.step-item {
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.step-number {
font-weight: bold;
color: #409eff;
}
.dialog-footer {
text-align: right;
}
</style>四、组件库选择指南
技术栈匹配
| 前端框架 | 推荐UI库 | 理由 |
|---|---|---|
| React | Ant Design | 生态最完善,企业级组件丰富 |
| Vue 3 | Element Plus | 官方推荐,文档完善,社区活跃 |
| Vue 2 | Element UI | 成熟稳定,组件齐全 |
| 通用 | TailwindCSS + Headless UI | 高度定制化,现代化设计 |
项目类型匹配
企业级管理系统(如测试平台)
- 首选:Ant Design (React) / Element Plus (Vue)
- 特点:组件丰富、设计专业、文档完善
- 适用场景:后台管理、数据展示、表单密集型应用
移动端应用
- 首选:Vant (Vue) / Ant Design Mobile (React)
- 特点:移动端优化、触摸友好、性能优秀
营销页面/官网
- 首选:TailwindCSS + 自定义组件
- 特点:设计灵活、性能优秀、SEO友好
团队技能匹配
| 团队水平 | 推荐方案 | 说明 |
|---|---|---|
| 初级团队 | Element Plus / Ant Design | 开箱即用,学习成本低 |
| 中级团队 | Ant Design Pro / Vue Admin | 基于组件库的解决方案 |
| 高级团队 | 自研组件库 | 完全定制化,符合业务需求 |
五、实战技巧与最佳实践
1. 主题定制
// Ant Design 主题定制
// config-overrides.js
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions: {
modifyVars: {
'@primary-color': '#1DA57A', // 主色调
'@link-color': '#1DA57A',
'@border-radius-base': '6px', // 圆角
'@font-size-base': '14px', // 字体大小
},
javascriptEnabled: true,
},
}),
);// Element Plus 主题定制
// styles/element-variables.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #1DA57A,
),
'success': (
'base': #52c41a,
),
'warning': (
'base': #faad14,
),
'danger': (
'base': #ff4d4f,
),
),
$border-radius: (
'base': 6px,
),
$font-size: (
'base': 14px,
)
);2. 按需加载优化
// Ant Design 按需加载
// babel.config.js
module.exports = {
plugins: [
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
},
],
],
};
// 使用示例
import { Button, Table, Form } from 'antd';// Element Plus 自动导入
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})3. 响应式设计
// Ant Design 响应式布局
import { Row, Col } from 'antd';
function ResponsiveLayout() {
return (
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8} lg={6} xl={4}>
<div>响应式内容</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6} xl={4}>
<div>响应式内容</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6} xl={4}>
<div>响应式内容</div>
</Col>
</Row>
);
}<!-- Element Plus 响应式布局 -->
<template>
<el-row :gutter="16">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
<div>响应式内容</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
<div>响应式内容</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
<div>响应式内容</div>
</el-col>
</el-row>
</template>4. 国际化支持
// Ant Design 国际化
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
function App() {
const [locale, setLocale] = useState(zhCN);
return (
<ConfigProvider locale={locale}>
<YourApp />
</ConfigProvider>
);
}// Element Plus 国际化
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import en from 'element-plus/dist/locale/en.mjs'
const app = createApp(App)
app.use(ElementPlus, { locale: zhCn })六、性能优化技巧
1. 组件懒加载
// React 组件懒加载
import { lazy, Suspense } from 'react';
const TestCaseTable = lazy(() => import('./components/TestCaseTable'));
const TestCaseForm = lazy(() => import('./components/TestCaseForm'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<TestCaseTable />
<TestCaseForm />
</Suspense>
);
}// Vue 组件懒加载
const TestCaseTable = defineAsyncComponent(() =>
import('./components/TestCaseTable.vue')
);
const TestCaseForm = defineAsyncComponent(() =>
import('./components/TestCaseForm.vue')
);2. 虚拟滚动
// Ant Design 虚拟滚动表格
import { Table } from 'antd';
function VirtualTable() {
return (
<Table
columns={columns}
dataSource={largeDataSource}
scroll={{ y: 400, x: 1200 }}
virtual
pagination={false}
/>
);
}3. 表单性能优化
// 使用 Form.useWatch 优化表单性能
import { Form } from 'antd';
function OptimizedForm() {
const [form] = Form.useForm();
// 只监听特定字段变化
const moduleValue = Form.useWatch('module', form);
return (
<Form form={form}>
<Form.Item name="module">
<Select>
<Option value="user">用户管理</Option>
<Option value="product">商品管理</Option>
</Select>
</Form.Item>
{/* 根据module值动态显示字段 */}
{moduleValue === 'user' && (
<Form.Item name="userConfig">
<Input placeholder="用户配置" />
</Form.Item>
)}
</Form>
);
}七、总结与建议
通过这篇文章,我们深入学习了前端UI框架与组件库的使用:
✅ UI组件库选择:根据技术栈和项目需求选择合适的组件库 ✅ Ant Design实战:企业级React组件库的核心用法 ✅ Element Plus实战:Vue生态的优秀组件库应用 ✅ 最佳实践:主题定制、按需加载、响应式设计 ✅ 性能优化:懒加载、虚拟滚动、表单优化
选择建议总结
测试平台开发推荐:
React技术栈:Ant Design + Ant Design Pro
- 组件丰富,企业级设计
- 生态完善,文档详细
- 适合复杂的后台管理系统
Vue技术栈:Element Plus + Vue Admin
- 学习成本低,上手快
- 设计简洁,性能优秀
- 适合快速开发和原型验证
高度定制需求:TailwindCSS + Headless UI
- 完全控制样式
- 现代化设计系统
- 适合有设计师支持的团队
学习建议
- 先掌握一个:深入学习一个组件库,理解设计理念
- 关注文档:组件库的官方文档是最好的学习资源
- 实践项目:通过实际项目加深理解
- 关注更新:组件库更新频繁,要跟上版本变化
- 自定义能力:学会主题定制和组件扩展
🎯 下一步预告:掌握了UI组件库,我们已经能够快速构建美观的界面了!下一篇将学习前端工程化与构建工具,了解如何搭建高效的开发环境和构建流程!
UI组件库让我们站在巨人的肩膀上,快速构建专业级的前端应用。让我们继续探索前端工程化的精彩世界!🚀
