从零开始构建新零售供应链采购审批流程界面——React与原生HTML表单的完美融合
【摘要】 引言采购审批流程作为供应链的第一道关口,不仅需要保证合规性和安全性,还要兼顾操作便捷性和用户体验。本文将带领大家从零开始,采用现代前端技术栈,结合HTML表单的原生特性和React组件化思想,打造一个高效、美观且易于维护的采购审批界面。一、项目架构设计与技术选型1.1 整体架构规划在开始编码之前,我们需要明确项目的整体架构。采购审批系统主要包括以下几个核心模块:采购审批系统登录认证模块采购申...
引言
采购审批流程作为供应链的第一道关口,不仅需要保证合规性和安全性,还要兼顾操作便捷性和用户体验。
本文将带领大家从零开始,采用现代前端技术栈,结合HTML表单的原生特性和React组件化思想,打造一个高效、美观且易于维护的采购审批界面。
一、项目架构设计与技术选型
1.1 整体架构规划
在开始编码之前,我们需要明确项目的整体架构。采购审批系统主要包括以下几个核心模块:
- 采购审批系统
- 登录认证模块
- 采购申请模块
- 基本信息填写
- 商品明细录入
- 附件上传
- 审批流程模块
- 一级审批
- 二级审批
- 最终确认
- 数据展示模块
- 通知提醒模块
1.2 技术栈选择理由
我们选择React作为主要框架,原因如下:
- 组件化开发模式提高代码复用性
- 虚拟DOM提升页面渲染性能
- 丰富的生态系统支持快速开发
- 原生HTML表单功能保证数据验证可靠性
二、基础环境搭建与项目初始化
首先创建React项目并安装必要依赖:
npx create-react-app procurement-approval-system
cd procurement-approval-system
npm start
清理默认模板文件后,建立基本目录结构:
src/
├── components/ # 公共组件
├── pages/ # 页面组件
├── styles/ # 样式文件
├── utils/ # 工具函数
└── App.js
三、核心组件设计与实现
3.1 采购申请表单组件
让我们从最核心的采购申请表单开始。这个组件将充分利用HTML5表单验证特性,并结合React的状态管理。
import React, { useState } from 'react';
import '../styles/ProcurementForm.css';
const ProcurementForm = () => {
// 表单状态管理
const [formData, setFormData] = useState({
applicant: '',
department: '',
applyDate: '',
expectedDeliveryDate: '',
supplier: '',
totalAmount: '',
description: ''
});
// 商品明细状态
const [items, setItems] = useState([
{ id: 1, name: '', quantity: '', unitPrice: '', totalPrice: '' }
]);
// 处理输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// 处理商品明细变化
const handleItemChange = (id, field, value) => {
setItems(prev => prev.map(item => {
if (item.id === id) {
const updatedItem = { ...item, [field]: value };
// 自动计算总价
if (field === 'quantity' || field === 'unitPrice') {
const quantity = field === 'quantity' ? value : item.quantity;
const unitPrice = field === 'unitPrice' ? value : item.unitPrice;
updatedItem.totalPrice = (quantity * unitPrice).toFixed(2);
}
return updatedItem;
}
return item;
}));
};
// 添加新商品项
const addItem = () => {
setItems(prev => [
...prev,
{
id: prev.length > 0 ? Math.max(...prev.map(i => i.id)) + 1 : 1,
name: '',
quantity: '',
unitPrice: '',
totalPrice: ''
}
]);
};
// 删除商品项
const removeItem = (id) => {
if (items.length > 1) {
setItems(prev => prev.filter(item => item.id !== id));
}
};
// 提交表单
const handleSubmit = (e) => {
e.preventDefault();
// 这里可以添加表单提交逻辑
console.log('提交数据:', { ...formData, items });
};
return (
<form className="procurement-form" onSubmit={handleSubmit}>
<div className="form-section">
<h2>基础信息</h2>
<div className="form-row">
<div className="form-group">
<label htmlFor="applicant">申请人 *</label>
<input
type="text"
id="applicant"
name="applicant"
value={formData.applicant}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="department">所属部门 *</label>
<select
id="department"
name="department"
value={formData.department}
onChange={handleInputChange}
required
>
<option value="">请选择部门</option>
<option value="采购部">采购部</option>
<option value="运营部">运营部</option>
<option value="市场部">市场部</option>
<option value="财务部">财务部</option>
</select>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="applyDate">申请日期 *</label>
<input
type="date"
id="applyDate"
name="applyDate"
value={formData.applyDate}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="expectedDeliveryDate">期望到货日期 *</label>
<input
type="date"
id="expectedDeliveryDate"
name="expectedDeliveryDate"
value={formData.expectedDeliveryDate}
onChange={handleInputChange}
required
/>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="supplier">供应商 *</label>
<input
type="text"
id="supplier"
name="supplier"
value={formData.supplier}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="totalAmount">总金额</label>
<input
type="number"
id="totalAmount"
name="totalAmount"
value={formData.totalAmount}
onChange={handleInputChange}
readOnly
/>
</div>
</div>
<div className="form-group full-width">
<label htmlFor="description">采购说明</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
rows="3"
/>
</div>
</div>
<div className="form-section">
<div className="section-header">
<h2>商品明细</h2>
<button type="button" onClick={addItem} className="add-item-btn">
+ 添加商品
</button>
</div>
<div className="items-table">
<div className="table-header">
<div className="table-cell">商品名称</div>
<div className="table-cell">数量</div>
<div className="table-cell">单价</div>
<div className="table-cell">总价</div>
<div className="table-cell">操作</div>
</div>
{items.map((item) => (
<div key={item.id} className="table-row">
<div className="table-cell">
<input
type="text"
value={item.name}
onChange={(e) => handleItemChange(item.id, 'name', e.target.value)}
required
/>
</div>
<div className="table-cell">
<input
type="number"
value={item.quantity}
onChange={(e) => handleItemChange(item.id, 'quantity', e.target.value)}
required
/>
</div>
<div className="table-cell">
<input
type="number"
step="0.01"
value={item.unitPrice}
onChange={(e) => handleItemChange(item.id, 'unitPrice', e.target.value)}
required
/>
</div>
<div className="table-cell">
<input
type="text"
value={item.totalPrice}
readOnly
/>
</div>
<div className="table-cell">
<button
type="button"
onClick={() => removeItem(item.id)}
disabled={items.length <= 1}
className="remove-btn"
>
删除
</button>
</div>
</div>
))}
</div>
</div>
<div className="form-actions">
<button type="button" className="cancel-btn">取消</button>
<button type="submit" className="submit-btn">提交申请</button>
</div>
</form>
);
};
export default ProcurementForm;
这段代码实现了完整的采购申请表单,具有以下特点:
- 状态管理:使用useState钩子管理表单数据和商品明细
- 动态表单:支持动态添加和删除商品项
- 自动计算:商品数量和单价变化时自动计算总价
- 表单验证:利用HTML5原生验证属性确保必填项
3.2 审批流程组件
接下来实现审批流程组件,展示审批状态和操作按钮:
import React, { useState } from 'react';
import '../styles/ApprovalFlow.css';
const ApprovalFlow = () => {
const [currentStep, setCurrentStep] = useState(1);
const [approvals, setApprovals] = useState([
{ id: 1, name: '部门主管', status: 'pending', approved: false },
{ id: 2, name: '财务经理', status: 'pending', approved: false },
{ id: 3, name: '总经理', status: 'pending', approved: false }
]);
const handleApprove = (id) => {
setApprovals(prev => prev.map(approval =>
approval.id === id
? { ...approval, status: 'approved', approved: true }
: approval
));
// 如果不是最后一个审批人,则进入下一步
if (id < approvals.length) {
setCurrentStep(id + 1);
}
};
const handleReject = (id) => {
setApprovals(prev => prev.map(approval =>
approval.id === id
? { ...approval, status: 'rejected' }
: approval
));
};
return (
<div className="approval-flow">
<h2>审批流程</h2>
<div className="progress-tracker">
{approvals.map((approval, index) => (
<div key={approval.id} className="step">
<div className={`step-icon ${approval.status}`}>
{approval.status === 'approved' ? '✓' :
approval.status === 'rejected' ? '✗' :
index + 1}
</div>
<div className="step-label">{approval.name}</div>
<div className="step-status">{approval.status === 'approved' ? '已批准' :
approval.status === 'rejected' ? '已拒绝' :
'待审批'}</div>
</div>
))}
</div>
<div className="current-approval">
<h3>当前审批</h3>
{approvals
.filter(approval => approval.id === currentStep)
.map(approval => (
<div key={approval.id} className="approval-card">
<div className="approval-info">
<h4>{approval.name}</h4>
<p>请审核采购申请并做出决定</p>
</div>
<div className="approval-actions">
<button
className="approve-btn"
onClick={() => handleApprove(approval.id)}
disabled={approval.status !== 'pending'}
>
批准
</button>
<button
className="reject-btn"
onClick={() => handleReject(approval.id)}
disabled={approval.status !== 'pending'}
>
拒绝
</button>
</div>
</div>
))
}
</div>
</div>
);
};
export default ApprovalFlow;
四、样式设计与美化
4.1 采购表单样式
.procurement-form {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.form-section h2 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.form-group {
flex: 1;
}
.form-group.full-width {
flex: 1 0 100%;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-group input:invalid {
border-color: #dc3545;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.add-item-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.add-item-btn:hover {
background: #218838;
}
.items-table {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.table-header {
display: flex;
background: #f8f9fa;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.table-row {
display: flex;
border-bottom: 1px solid #eee;
}
.table-row:last-child {
border-bottom: none;
}
.table-cell {
flex: 1;
padding: 12px;
display: flex;
align-items: center;
}
.table-cell input {
width: 100%;
padding: 6px;
border: 1px solid #ddd;
border-radius: 3px;
}
.remove-btn {
background: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.remove-btn:hover:not(:disabled) {
background: #c82333;
}
.remove-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
}
.cancel-btn, .submit-btn {
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.cancel-btn {
background: #6c757d;
color: white;
}
.cancel-btn:hover {
background: #5a6268;
}
.submit-btn {
background: #007bff;
color: white;
}
.submit-btn:hover {
background: #0069d9;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 10px;
}
.table-header,
.table-row {
flex-wrap: wrap;
}
.table-cell {
flex: 0 0 50%;
}
}
4.2 审批流程样式
.approval-flow {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.progress-tracker {
display: flex;
justify-content: space-between;
margin: 30px 0;
position: relative;
}
.progress-tracker::before {
content: '';
position: absolute;
top: 20px;
left: 0;
right: 0;
height: 2px;
background: #e0e0e0;
z-index: 1;
}
.step {
text-align: center;
position: relative;
z-index: 2;
}
.step-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
font-weight: bold;
color: #fff;
}
.step-icon.pending {
background: #6c757d;
}
.step-icon.approved {
background: #28a745;
}
.step-icon.rejected {
background: #dc3545;
}
.step-label {
font-weight: 500;
margin-bottom: 5px;
}
.step-status {
font-size: 14px;
color: #666;
}
.current-approval {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
}
.approval-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.approval-info h4 {
margin: 0 0 5px 0;
color: #333;
}
.approval-info p {
margin: 0;
color: #666;
}
.approval-actions {
display: flex;
gap: 10px;
}
.approve-btn, .reject-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.approve-btn {
background: #28a745;
color: white;
}
.approve-btn:hover:not(:disabled) {
background: #218838;
}
.reject-btn {
background: #dc3545;
color: white;
}
.reject-btn:hover:not(:disabled) {
background: #c82333;
}
.approve-btn:disabled,
.reject-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
五、主应用组件整合
现在让我们创建主应用组件,整合所有功能模块:
import React from 'react';
import ProcurementForm from './components/ProcurementForm';
import ApprovalFlow from './components/ApprovalFlow';
import './App.css';
function App() {
return (
<div className="App">
<header className="app-header">
<h1>新零售供应链采购审批系统</h1>
</header>
<main className="app-main">
<div className="container">
<ProcurementForm />
<ApprovalFlow />
</div>
</main>
</div>
);
}
export default App;
css
/* src/App.css */
.App {
min-height: 100vh;
background: #f5f5f5;
}
.app-header {
background: #007bff;
color: white;
padding: 20px 0;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
margin: 0;
font-size: 24px;
}
.app-main {
padding: 30px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
@media (max-width: 768px) {
.app-header h1 {
font-size: 20px;
}
.app-main {
padding: 20px 0;
}
}
总结
本文从零开始构建了一个完整的新零售供应链采购审批界面,涵盖了以下关键技术要点:
- 表单设计:充分利用HTML5原生表单验证功能,确保数据的有效性和完整性
- 状态管理:合理运用React的useState钩子管理复杂表单状态
- 组件化开发:将功能拆分为独立组件,提高代码可维护性
- 响应式设计:通过CSS媒体查询适配不同屏幕尺寸
- 用户体验:注重交互细节,提供良好的操作反馈
通过本项目的实践,我们不仅掌握了React组件开发的核心技能,还深入理解了如何结合原生HTML表单功能构建企业级应用。这套解决方案可以轻松扩展到其他类似的审批流程场景中,为企业的数字化转型提供坚实的技术支撑。
希望这篇文章能帮助读者深入理解现代前端开发的技术要点,并能在实际项目中灵活应用这些知识。在未来的开发工作中,建议继续关注Web标准的发展和React生态的变化,持续提升技术水平。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)