表单样式

main
wangyunfei888 4 weeks ago
parent b1fb028e6e
commit 284e8fbcab

@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
import styles from './BusinessPlanManagement.less';
import { Select, Space, Button, Input, DatePicker } from 'antd';
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import BusinessPlanDrawer from './second_oil_components/BusinessPlanDrawer';
import StandardTable from '@/components/StandardTable';
import topIcon from '@/assets/business_planmanage/jyjhgl1.svg';
import topIcon2 from '@/assets/business_planmanage/jyjhgl2.svg';
@ -10,6 +11,7 @@ import topIcon4 from '@/assets/business_planmanage/jyjhgl4.svg';
const BusinessPlanManagement = () => {
const [searchKeyword, setSearchKeyword] = useState('');
const [selectedRows, setSelectedRows] = useState([]);
const [drawerVisible, setDrawerVisible] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// 列表筛选与数据(演示数据,可替换为接口)
@ -227,6 +229,7 @@ const BusinessPlanManagement = () => {
type="primary"
icon={<PlusOutlined />}
className={styles.addBtn}
onClick={() => setDrawerVisible(true)}
>
新增计划
</Button>
@ -257,6 +260,15 @@ const BusinessPlanManagement = () => {
onChangePageSize={(size) => { setPageSize(size); setCurrentPage(1); }}
/>
</div>
<BusinessPlanDrawer
visible={drawerVisible}
onClose={() => setDrawerVisible(false)}
onSubmit={(values) => {
// TODO: 调用新增接口后刷新列表
console.log('submit', values);
setDrawerVisible(false);
}}
/>
</div>
</div>
</div>

@ -0,0 +1,318 @@
import React, { useMemo, useState } from 'react';
import { Drawer, Steps, Form, Input, Select, DatePicker, Upload, Button, Space, message, Progress } from 'antd';
import { InboxOutlined, DeleteOutlined, DownloadOutlined, CloseOutlined } from '@ant-design/icons';
import styles from './BusinessPlanDrawer.less';
const { Step } = Steps;
const { TextArea } = Input;
/**
* 多步抽屉表单父组件控制 visible/onClose/onSubmit/initialValues
*/
const BusinessPlanDrawer = ({
visible,
onClose,
onSubmit,
initialValues = {},
title = '新增经营计划',
}) => {
const [current, setCurrent] = useState(0); // 当前步骤索引
const [form] = Form.useForm(); // 表单实例
const [uploadingFiles, setUploadingFiles] = useState([]); // 正在上传的文件
const [uploadedFiles, setUploadedFiles] = useState([]); // 已完成上传的文件
// 步骤配置:决定进度条和内容块
const steps = useMemo(
() => [
{ key: 'basic', title: '基础信息' },
{ key: 'cargo', title: '货品信息' },
{ key: 'vessel', title: '舰具信息' },
{ key: 'upload', title: '附件上传' },
],
[],
);
// 下一步:无需必填校验,直接切换
const handleNext = () => {
setCurrent((c) => Math.min(c + 1, steps.length - 1));
};
// 上一步:简单回退
const handlePrev = () => setCurrent((c) => Math.max(c - 1, 0));
// 提交:不做必填校验,直接取表单值
const handleFinish = async () => {
const values = await form.getFieldsValue();
values.files = uploadedFiles;
onSubmit?.(values);
message.success('已提交');
};
const formatSize = (size) => {
if (!size && size !== 0) return '';
const mb = size / (1024 * 1024);
return `${mb.toFixed(1)} MB`;
};
// 模拟上传推进进度到100%后移入已上传列表
const startMockUpload = (file) => {
const item = { uid: file.uid, name: file.name, size: file.size, percent: 0 };
setUploadingFiles((prev) => [...prev, item]);
let percent = 0;
const timer = setInterval(() => {
percent = Math.min(percent + 20, 100);
setUploadingFiles((prev) => prev.map((f) => (f.uid === file.uid ? { ...f, percent } : f)));
if (percent === 100) {
clearInterval(timer);
setUploadingFiles((prev) => prev.filter((f) => f.uid !== file.uid));
setUploadedFiles((prev) => [{ ...item, percent: 100 }, ...prev]);
}
}, 180);
};
const handleRemoveUploaded = (uid) => {
setUploadedFiles((prev) => prev.filter((f) => f.uid !== uid));
};
// 按步骤渲染表单内容
const renderStepContent = () => {
const stepKey = steps[current].key;
switch (stepKey) {
case 'basic':
return (
<div className={styles.formGrid}>
<Form.Item label="计划编号" name="planNo">
<Input placeholder="92号汽油" />
</Form.Item>
<Form.Item label="计划名称" name="planName">
<Input placeholder="请输入计划名称" />
</Form.Item>
<Form.Item label="计划日期" name="planDate">
<DatePicker style={{ width: '100%' }} placeholder="年/月/日" />
</Form.Item>
<Form.Item label="负责人" name="owner">
<Input placeholder="张经理" />
</Form.Item>
<Form.Item label="运输方式" name="transportMethod">
<Select
placeholder="请选择"
options={[
{ label: '管道运输', value: '管道' },
{ label: '公路运输', value: '公路' },
{ label: '铁路运输', value: '铁路' },
{ label: '船舶运输', value: '船舶' },
]}
/>
</Form.Item>
<Form.Item label="供应商/客户" name="supplier">
<Select
placeholder="请选择"
options={[
{ label: '中石化华东分公司', value: 'sinopec_east' },
{ label: '山东恒燃实业集团', value: 'hengran' },
]}
/>
</Form.Item>
<Form.Item label="关联合同号" name="contractNo">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="请求时间窗口" name="timeWindow">
<Input placeholder="例如:六月份" />
</Form.Item>
<Form.Item label="备注" name="remark" className={styles.fullRow}>
<TextArea rows={4} placeholder="请输入备注信息" />
</Form.Item>
</div>
);
case 'cargo':
return (
<div className={styles.formGrid}>
<Form.Item label="货品编号" name="cargoCode">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="货品名称" name="cargoName">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="规格型号" name="cargoSpec">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="货品数量" name="cargoQuantity">
<Input placeholder="例如0.735" />
</Form.Item>
{/* 单位选择框移除,根据需求不再展示 */}
<Form.Item label="来源地" name="origin">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="目的地" name="destination">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="货品状态" name="cargoStatus" initialValue="normal">
<Select
placeholder="请选择"
options={[
{ label: '正常', value: 'normal' },
{ label: '异常', value: 'abnormal' },
]}
/>
</Form.Item>
</div>
);
case 'vessel':
return (
<div className={styles.formGrid}>
<Form.Item label="载具编号" name="vesselCode">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="载具类型" name="vesselType">
<Select
placeholder="请选择"
options={[
{ label: '油罐车', value: 'truck' },
{ label: '油轮', value: 'ship' },
{ label: '火车罐车', value: 'train' },
{ label: '储罐', value: 'tank' },
]}
/>
</Form.Item>
<Form.Item label="载具容量" name="vesselCapacity">
<Input placeholder="例如0.735" />
</Form.Item>
<Form.Item label="载具状态" name="vesselStatus" initialValue="available">
<Select
placeholder="请选择"
options={[
{ label: '可用', value: 'available' },
{ label: '不可用', value: 'unavailable' },
{ label: '维护中', value: 'maintaining' },
]}
/>
</Form.Item>
<Form.Item label="司机/操作员姓名" name="vesselOperator">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="联系方式" name="vesselContact">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="载具备注" name="vesselRemark" className={styles.fullRow}>
<TextArea rows={3} placeholder="请输入载具相关备注信息" />
</Form.Item>
</div>
);
case 'upload':
return (
<div className={styles.uploadSection}>
<Upload.Dragger
multiple
name="files"
showUploadList={false}
beforeUpload={(file) => {
startMockUpload(file);
return false; // 阻止自动上传
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">拖放文件到此处或点击上传</p>
<p className="ant-upload-hint">支持合同调度令等文件格式单个文件不超过10MB</p>
</Upload.Dragger>
{uploadingFiles.length > 0 && (
<div className={styles.uploadingList}>
<div className={styles.listTitle}>上传中文件</div>
{uploadingFiles.map((f) => (
<div key={f.uid} className={styles.uploadingItem}>
<div className={styles.fileMeta}>
<span className={styles.fileName}>{f.name}</span>
<span className={styles.fileSize}>{formatSize(f.size)}</span>
</div>
<Progress percent={f.percent} showInfo={false} strokeColor="#6abfbb" />
</div>
))}
</div>
)}
{uploadedFiles.length > 0 && (
<div className={styles.uploadedList}>
<div className={styles.listTitle}>已上传文件</div>
{uploadedFiles.map((f) => (
<div key={f.uid} className={styles.uploadedItem}>
<div className={styles.fileMeta}>
<span className={styles.fileName}>{f.name}</span>
<span className={styles.fileSize}>{formatSize(f.size)}</span>
</div>
<Space size={8}>
<Button size="small" type="text" icon={<DownloadOutlined />} />
<Button size="small" type="text" icon={<DeleteOutlined />} onClick={() => handleRemoveUploaded(f.uid)} />
</Space>
</div>
))}
</div>
)}
</div>
);
default:
return null;
}
};
return (
<div className={styles.drawerWrapper}>
<button className={styles.outsideClose} onClick={onClose} aria-label="关闭抽屉">
<CloseOutlined />
</button>
<Drawer
title={title}
width="41%"
height="100vh"
open={visible}
onClose={onClose}
className={styles.customDrawer}
closable={false}
destroyOnClose
bodyStyle={{ padding: 24 }}
maskClosable={false}
>
<div className={styles.drawerHeader}>
<Steps current={current} responsive>
{steps.map((s) => (
<Step key={s.key} title={s.title} />
))}
</Steps>
</div>
<Form
form={form}
layout="vertical"
initialValues={initialValues}
className={styles.formWrapper}
>
{renderStepContent()}
</Form>
<div className={styles.footer}>
<Space>
<Button className={styles.cancelButton} onClick={onClose}>取消</Button>
<Button className={styles.saveDraftButton} onClick={() => message.info('已保存草稿(示例)')}>保存草稿</Button>
</Space>
<Space>
<Button className={styles.navButton} onClick={handlePrev} disabled={current === 0}>
上一步
</Button>
{current < steps.length - 1 ? (
<Button className={styles.navButton} type="primary" onClick={handleNext}>
下一步
</Button>
) : (
<Button className={styles.navButton} type="primary" onClick={handleFinish}>
提交
</Button>
)}
</Space>
</div>
</Drawer>
</div>
);
};
export default BusinessPlanDrawer;

@ -0,0 +1,163 @@
.drawerHeader {
margin-bottom: 16px;
}
.customDrawer {
:global {
// /* 允许抽屉内容区域溢出,便于关闭按钮外移仍可见 */
// .ant-drawer {
// overflow: visible;
// z-index: 100;
// }
// .ant-drawer-content-wrapper,
// .ant-drawer-content {
// overflow: visible;
// z-index: 100;
// }
.ant-drawer-header {
background: rgba(184, 224, 216, 0.2);
border: 1px solid;
border-image-slice: 1;
border-image-source: linear-gradient(96.54deg, #ffffff -0.94%, rgba(255, 255, 255, 0) 25.28%, rgba(167, 229, 228, 0) 59.69%, #a7e5e4 79.76%);
backdrop-filter: blur(3.4px);
box-shadow: 1px 2px 5px 0 rgba(0, 102, 101, 0.25);
}
.ant-drawer-title {
font-size: 18px;
font-weight: 500;
color: rgba(78, 88, 86, 1);
z-index: 100;
}
.ant-drawer-close {
// position: absolute;
// left: -20px;
// /* 向左平移 200px 到抽屉外侧 */
// right: auto;
// top: 50%;
// transform: translateY(-50%);
// color: rgba(78, 88, 86, 1);
// z-index: 200;
}
/* 全局表单控件统一样式 */
.ant-form-item-label > label,
.ant-form-item-required,
.ant-form-item-label::after,
.ant-form-item-label::before {
color: rgba(51, 51, 51, 1);
}
.ant-input,
.ant-input-affix-wrapper,
.ant-input-number,
.ant-input-number .ant-input-number-input,
.ant-picker,
.ant-select-selector,
.ant-picker-input > input,
.ant-select-selection-search-input {
border: 1px solid var(--, rgba(44, 158, 157, 1));
color: rgba(51, 51, 51, 1);
}
}
}
.formWrapper {
margin-top: 16px;
.formGrid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 16px 24px;
.fullRow {
grid-column: 1 / -1;
}
}
}
.footer {
margin-top: 24px;
display: flex;
justify-content: space-between;
.cancelButton {
background: rgba(183, 229, 213, 0.2);
border: 1px solid;
border-image-slice: 1;
border-image-source: conic-gradient(from 102.75deg at 50% 52.91%, rgba(249, 249, 249, 0.5) -32.95deg, rgba(140, 160, 156, 0.5) 10.52deg, rgba(140, 160, 156, 0.35) 32.12deg, rgba(255, 255, 255, 0.5) 60.28deg, rgba(255, 255, 255, 0.5) 107.79deg, rgba(140, 160, 156, 0.35) 187.59deg, #F9F9F9 207.58deg, rgba(255, 255, 255, 0.5) 287.31deg, rgba(249, 249, 249, 0.5) 327.05deg, rgba(140, 160, 156, 0.5) 370.52deg);
backdrop-filter: blur(3.4px);
}
.saveDraftButton {
background: rgba(4, 95, 94, 0.5);
border: 1px solid;
color: #fff;
border-image-slice: 1;
border-image-source: linear-gradient(96.54deg, #FFFFFF -0.94%, rgba(255, 255, 255, 0) 25.28%, rgba(0, 143, 142, 0) 59.69%, #008F8E 79.76%);
box-shadow: 1px 2px 5px 0px rgba(0, 102, 101, 0.25),
-2px 4px 10px 0px rgba(145, 145, 145, 0.05),
-7px 17px 18px 0px rgba(145, 145, 145, 0.04),
-15px 37px 24px 0px rgba(145, 145, 145, 0.03),
-27px 66px 29px 0px rgba(145, 145, 145, 0.01),
-42px 103px 31px 0px rgba(145, 145, 145, 0);
backdrop-filter: blur(3.4px);
}
.navButton {
background: rgba(4, 95, 94, 0.5);
border: 0.5px solid;
color: #fff;
border-image-slice: 1;
border-image-source: linear-gradient(96.54deg, #B0DEC5 9.43%, rgba(255, 255, 255, 0) 25.28%, rgba(0, 143, 142, 0) 59.69%, #146A59 75.4%);
box-shadow: 1px 2px 5px 0px rgba(0, 102, 101, 0.25),
-2px 4px 10px 0px rgba(145, 145, 145, 0.05),
-7px 17px 18px 0px rgba(145, 145, 145, 0.04),
-15px 37px 24px 0px rgba(145, 145, 145, 0.03),
-27px 66px 29px 0px rgba(145, 145, 145, 0.01),
-42px 103px 31px 0px rgba(145, 145, 145, 0);
backdrop-filter: blur(3.4px);
}
}
.uploadSection {
margin-top: 12px;
.uploadingList,
.uploadedList {
margin-top: 16px;
.listTitle {
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.uploadingItem,
.uploadedItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f0f2f5;
.fileMeta {
display: flex;
flex-direction: column;
.fileName {
color: #333;
}
.fileSize {
color: #888;
font-size: 12px;
}
}
}
}
}
Loading…
Cancel
Save