Files
fe_portal_frontend/src/pages/register/supplier/CommonFormSections.tsx

822 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 供应商注册表单通用部分
* 封装了国内企业和境外企业注册表单相同的部分
*/
import React, { useEffect, useState } from 'react';
import {
Form,
Input,
Button,
Select,
DatePicker,
Row,
Col,
Table,
Radio,
Cascader,
Empty,
} from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { message } from 'antd';
import { useIntl } from 'umi';
import FileUpload from '@/components/FileUpload';
import type { DictItem } from '@/servers/api/dict';
import { getDictList } from '@/servers/api/dict';
const { Option } = Select;
// 中国省市区级联数据
export const addressOptions = [
{
value: '330000',
label: '浙江省',
children: [
{
value: '330100',
label: '杭州市',
children: [
{ value: '330102', label: '上城区' },
{ value: '330103', label: '下城区' },
{ value: '330104', label: '江干区' },
{ value: '330105', label: '拱墅区' },
{ value: '330106', label: '西湖区' },
{ value: '330108', label: '滨江区' },
],
},
],
},
{
value: '310000',
label: '上海市',
children: [
{
value: '310100',
label: '上海市',
children: [
{ value: '310101', label: '黄浦区' },
{ value: '310104', label: '徐汇区' },
{ value: '310105', label: '长宁区' },
],
},
],
},
];
interface CommonFormSectionsProps {
form: any;
}
// 扩展问卷部分的属性接口
interface SurveySectionProps extends CommonFormSectionsProps {
surveyQuestions?: API.SurveyQuestionResponse;
}
/**
* 资质信息表单部分
* 包含资质证书类型、名称、编号、等级、发证机构、发证日期、有效期等
*/
export const QualificationSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
const intl = useIntl();
// 资质证书类型
const [certTypeList, setCertTypeList] = useState<DictItem[]>([]);
const getDict = async () => {
// qualification_type 资质证书类型
const certTypeResponse = await getDictList('certificate');
setCertTypeList(certTypeResponse.data || []);
};
useEffect(() => {
getDict();
}, []);
return (
<div className="border-box">
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.qualification' })}</div>
<Form.List name="coscoSupplierQualifications">
{(fields, { add, remove }) => (
<>
<Table
dataSource={fields.map((field) => ({
...field,
key: field.key,
fieldKey: field.name,
}))}
pagination={false}
bordered
size="middle"
rowKey="key"
columns={[
{
title: intl.formatMessage({ id: 'register.form.table.no' }),
dataIndex: 'name',
width: 60,
render: (_, __, index) => index + 1,
},
{
title: intl.formatMessage({ id: 'register.qualification.certType' }),
dataIndex: 'certType',
render: (_, record) => (
<Form.Item
name={[record.name, 'certificateType']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.certType.required' }) }]}
>
<Select placeholder={intl.formatMessage({ id: 'register.qualification.certType.placeholder' })} style={{ width: '100%' }}>
{certTypeList.map((item) => (
<Option key={item.code} value={item.code}>
{item.dicName}
</Option>
))}
</Select>
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.certName' }),
dataIndex: 'certName',
render: (_, record) => (
<Form.Item
name={[record.name, 'name']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.certName.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.qualification.certName.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.certNumber' }),
dataIndex: 'certNumber',
render: (_, record) => (
<Form.Item
name={[record.name, 'code']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.certNumber.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.qualification.certNumber.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.certLevel' }),
dataIndex: 'certLevel',
render: (_, record) => (
<Form.Item name={[record.name, 'typeLevel']} noStyle>
<Input placeholder={intl.formatMessage({ id: 'register.qualification.certLevel.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.issuingAuthority' }),
dataIndex: 'issuingAuthority',
render: (_, record) => (
<Form.Item
name={[record.name, 'authority']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.issuingAuthority.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.qualification.issuingAuthority.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.issueDate' }),
dataIndex: 'issueDate',
render: (_, record) => (
<Form.Item
name={[record.name, 'dateTime']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.issueDate.required' }) }]}
>
<DatePicker
placeholder={intl.formatMessage({ id: 'register.form.date.placeholder' })}
style={{ width: '100%' }}
format="YYYY-MM-DD"
/>
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.qualification.expiryDate' }),
dataIndex: 'expiryDate',
render: (_, record) => (
<Form.Item
name={[record.name, 'termOfValidity']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.expiryDate.required' }) }]}
>
<DatePicker
placeholder={intl.formatMessage({ id: 'register.form.date.placeholder' })}
style={{ width: '100%' }}
format="YYYY-MM-DD"
/>
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.form.attachment' }),
dataIndex: 'certFile',
render: (_, record) => (
<Form.Item
name={[record.name, 'accessory']}
noStyle
rules={[{ required: false, message: intl.formatMessage({ id: 'register.qualification.attachment.required' }) }]}
valuePropName="value"
>
<FileUpload
maxSize={10}
allowedTypes={['pdf', 'jpg', 'jpeg', 'png']}
maxCount={1}
buttonText={intl.formatMessage({ id: 'register.form.upload' })}
/>
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.form.operation' }),
width: 70,
render: (_, record) => (
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => remove(record.name)}
>
{intl.formatMessage({ id: 'register.form.delete' })}
</Button>
),
},
]}
/>
<Form.Item style={{ marginTop: 16 }} wrapperCol={{ span: 24 }}>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
{intl.formatMessage({ id: 'register.form.addRow' })}
</Button>
</Form.Item>
</>
)}
</Form.List>
</div>
);
};
/**
* 开票信息表单部分
* 包含纳税人类型、开票抬头、纳税人识别号、开票地址等
*/
export const InvoiceSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
const intl = useIntl();
// 纳税人类型
const [taxpayerTypeList, setTaxpayerTypeList] = useState<DictItem[]>([]);
const getDict = async () => {
// taxpayer_type 纳税人类型
const taxpayerTypeResponse = await getDictList('taxpayer_type');
setTaxpayerTypeList(taxpayerTypeResponse.data || []);
};
useEffect(() => {
getDict();
}, []);
return (
<div className="border-box">
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.invoice' })}</div>
<Row gutter={24}>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'taxpayerType']}
label={intl.formatMessage({ id: 'register.invoice.taxpayerType' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.invoice.taxpayerType.required' }) }]}
>
<Select placeholder={intl.formatMessage({ id: 'register.invoice.taxpayerType.placeholder' })}>
{taxpayerTypeList.map((item) => (
<Option key={item.code} value={item.code}>
{item.dicName}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'head']}
label={intl.formatMessage({ id: 'register.invoice.head' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.invoice.head.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.head.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'taxpayerCode']}
label={intl.formatMessage({ id: 'register.invoice.taxpayerCode' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.invoice.taxpayerCode.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.taxpayerCode.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'address']}
label={intl.formatMessage({ id: 'register.invoice.address' })}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.address.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'phone']}
label={intl.formatMessage({ id: 'register.invoice.phone' })}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.phone.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'bank']}
label={intl.formatMessage({ id: 'register.invoice.bank' })}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.bank.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'account']}
label={intl.formatMessage({ id: 'register.invoice.account' })}
>
<Input placeholder={intl.formatMessage({ id: 'register.invoice.account.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierInvoice', 'qualificationCertificate']}
label={intl.formatMessage({ id: 'register.invoice.qualificationCertificate' })}
valuePropName="value"
>
<FileUpload
maxSize={10}
allowedTypes={['pdf', 'jpg', 'jpeg', 'png']}
maxCount={1}
buttonText={intl.formatMessage({ id: 'register.form.upload' })}
/>
</Form.Item>
</Col>
</Row>
</div>
);
};
/**
* 银行账户表单部分
* 包含开户银行、账户名称、账号、所在地区等
*/
export const BankAccountSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
const intl = useIntl();
return (
<div className="border-box">
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.bankAccount' })}</div>
<Form.List name="coscoSupplierBank">
{(fields, { add, remove }) => (
<>
<Table
dataSource={fields.map((field) => ({
...field,
key: field.key,
fieldKey: field.name,
}))}
pagination={false}
bordered
size="middle"
rowKey="key"
columns={[
{
title: intl.formatMessage({ id: 'register.form.table.no' }),
dataIndex: 'name',
width: 60,
render: (_, __, index) => index + 1,
},
{
title: intl.formatMessage({ id: 'register.bank.bankName' }),
dataIndex: 'bankName',
render: (_, record) => (
<Form.Item
name={[record.name, 'bank']}
noStyle
rules={[{ required: true, message: intl.formatMessage({ id: 'register.bank.bankName.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.bank.bankName.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.bank.accountName' }),
dataIndex: 'accountName',
render: (_, record) => (
<Form.Item
name={[record.name, 'accountName']}
noStyle
rules={[{ required: true, message: intl.formatMessage({ id: 'register.bank.accountName.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.bank.accountName.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.bank.accountNumber' }),
dataIndex: 'accountNumber',
render: (_, record) => (
<Form.Item
name={[record.name, 'account']}
noStyle
rules={[{ required: true, message: intl.formatMessage({ id: 'register.bank.accountNumber.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.bank.accountNumber.placeholder' })} />
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.bank.location' }),
dataIndex: 'location',
render: (_, record) => (
<Form.Item
name={[record.name, 'address']}
noStyle
rules={[{ required: true, message: intl.formatMessage({ id: 'register.bank.location.required' }) }]}
>
<Cascader
options={addressOptions}
placeholder={intl.formatMessage({ id: 'register.bank.location.placeholder' })}
showSearch={{
filter: (inputValue, path) => {
return path.some((option) => {
if (typeof option.label === 'string') {
return (
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
);
}
return false;
});
},
}}
/>
</Form.Item>
),
},
{
title: intl.formatMessage({ id: 'register.form.operation' }),
width: 70,
render: (_, record) => (
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => remove(record.name)}
>
{intl.formatMessage({ id: 'register.form.delete' })}
</Button>
),
},
]}
/>
<Form.Item style={{ marginTop: 16 }} wrapperCol={{ span: 24 }}>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
{intl.formatMessage({ id: 'register.form.addRow' })}
</Button>
</Form.Item>
</>
)}
</Form.List>
</div>
);
};
/**
* 社会准则符合性自查问卷部分
* 包含填写人信息和问卷内容
*/
export const SurveySection: React.FC<SurveySectionProps> = ({ form, surveyQuestions }) => {
const intl = useIntl();
// 使用API获取的问卷数据如果没有则显示无数据状态
const hasQuestions =
surveyQuestions && Array.isArray(surveyQuestions) && surveyQuestions.length > 0;
// 调试日志
console.log('调查问卷数据:', surveyQuestions);
return (
<div className="border-box">
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.survey' })}</div>
<div className="questionnaire-header">{intl.formatMessage({ id: 'register.survey.fillerInfo' })}</div>
<Row gutter={24}>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'supplierName']}
label={intl.formatMessage({ id: 'register.survey.supplierName' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.supplierName.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.survey.supplierName.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'name']}
label={intl.formatMessage({ id: 'register.survey.name' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.name.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.survey.name.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'position']}
label={intl.formatMessage({ id: 'register.survey.position' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.position.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.survey.position.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'phone']}
label={intl.formatMessage({ id: 'register.survey.phone' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.phone.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'register.survey.phone.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'email']}
label={intl.formatMessage({ id: 'register.survey.email' })}
rules={[
{ type: 'email', message: intl.formatMessage({ id: 'register.email.invalid' }) },
{ required: true, message: intl.formatMessage({ id: 'register.survey.email.required' }) },
]}
>
<Input placeholder={intl.formatMessage({ id: 'register.survey.email.placeholder' })} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name={['coscoSupplierSurvey', 'dateTime']}
label={intl.formatMessage({ id: 'register.survey.date' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.date.required' }) }]}
>
<DatePicker placeholder={intl.formatMessage({ id: 'register.form.date.placeholder' })} style={{ width: '100%' }} format="YYYY-MM-DD" />
</Form.Item>
</Col>
</Row>
<div className="questionnaire-header" style={{ marginTop: '20px' }}>
{intl.formatMessage({ id: 'register.survey.questionnaire' })}
</div>
{hasQuestions ? (
<Form.List
name="coscoSupplierSurveyQuestionReply"
initialValue={surveyQuestions.map((q: any, index) => ({
surveyQuestionId: q.id,
replyValue: '',
}))}
>
{(fields, { add, remove }) => {
console.log('Form.List fields:', fields);
return (
<Table
pagination={false}
bordered
size="middle"
rowKey={(record, index) => `survey_question_${index}`}
dataSource={surveyQuestions}
columns={[
{
title: intl.formatMessage({ id: 'register.form.table.no' }),
dataIndex: 'id',
width: 60,
align: 'center',
render: (text, record, index) => index + 1,
},
{
title: intl.formatMessage({ id: 'register.survey.question' }),
dataIndex: 'questionName',
render: (text) => <div style={{ whiteSpace: 'pre-line' }}>{text}</div>,
},
{
title: intl.formatMessage({ id: 'register.survey.reply' }),
width: 650,
render: (_, record, index) => {
return (
<>
<Form.Item
name={[index, 'surveyQuestionId']}
initialValue={record.id}
hidden
>
<Input />
</Form.Item>
<Form.Item
name={[index, 'replyValue']}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.survey.answer.required' }, { index: index + 1 }) }]}
wrapperCol={{ span: 24 }}
>
{record.coscoSurveyQuestionOptionList &&
record.coscoSurveyQuestionOptionList.length > 0 ? (
<Radio.Group>
{record.coscoSurveyQuestionOptionList.map((option: any) => (
<Radio key={option.id} value={option.opentionValue}>
{option.optionName}
</Radio>
))}
</Radio.Group>
) : (
<Input placeholder={intl.formatMessage({ id: 'register.survey.answer.placeholder' })} />
)}
</Form.Item>
</>
);
},
},
]}
/>
);
}}
</Form.List>
) : (
<div style={{ padding: '30px 0' }}>
<Empty description={intl.formatMessage({ id: 'register.survey.noData' })} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</div>
)}
</div>
);
};
/**
* 供应商反商业贿赂承诺书和其他附件部分
*/
export const AttachmentSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
const intl = useIntl();
return (
<div className="border-box">
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.commitment' })}</div>
<Row gutter={24}>
<Col span={12}>
<div className="upload-label">
{intl.formatMessage({ id: 'register.attachment.stampUpload' })}
<Button
type="link"
href="/templates/anti-bribery-template.docx"
download="供应商反商业贿赂承诺书模板.docx"
>
{intl.formatMessage({ id: 'register.attachment.downloadTemplate' })}
</Button>
</div>
<Form.List name="coscoSupplierSurveyAttachments">
{(fields, { add, remove }) => {
// 确保至少有一项用于反商业贿赂承诺书
if (fields.length === 0) {
add({ attachmentsType: 'commitment' });
}
return (
<div>
{fields.map((field, index) => (
<div key={field.key} style={{ display: field.name === 0 ? 'block' : 'none' }}>
<Form.Item
name={[field.name, 'attachmentsType']}
initialValue="commitment"
hidden
>
<Input />
</Form.Item>
<Form.Item
name={[field.name, 'fileUrl']}
rules={[{ required: true, message: intl.formatMessage({ id: 'register.attachment.commitment.required' }) }]}
valuePropName="value"
>
<FileUpload
maxSize={10}
allowedTypes={['pdf', 'doc', 'docx']}
maxCount={1}
buttonText={intl.formatMessage({ id: 'register.form.upload' })}
onChange={(fileList) => {
if (fileList && fileList.length > 0) {
const file = fileList[0];
if (file.status === 'done' && file.response) {
const response = file.response;
if (response && response.success) {
// 填充文件信息
form.setFieldsValue({
coscoSupplierSurveyAttachments: [
{
...form.getFieldValue([
'coscoSupplierSurveyAttachments',
field.name,
]),
fileName: file.name,
fileType: file.type,
fileSize: file.size?.toString(),
filePath: response.filePath || response.url,
},
],
});
message.success(`${file.name} 上传成功`);
}
}
}
}}
/>
</Form.Item>
</div>
))}
</div>
);
}}
</Form.List>
</Col>
</Row>
<div className="form-section-title">{intl.formatMessage({ id: 'register.form.section.otherAttachments' })}</div>
<Row gutter={24}>
<Col span={24}>
<Form.List name="coscoSupplierSurveyAttachments">
{(fields, { add, remove }) => (
<>
<div className="upload-label">{intl.formatMessage({ id: 'register.attachment.otherAttachments.hint' })}</div>
{fields.map((field, index) => (
<div
key={field.key}
style={{
display: index > 0 || fields.length === 1 ? 'block' : 'none',
marginBottom: 8,
}}
>
{index > 0 && (
<Form.Item
name={[field.name, 'attachmentsType']}
initialValue="accessory"
hidden
>
<Input />
</Form.Item>
)}
{index > 0 && (
<Form.Item name={[field.name, 'fileUrl']} valuePropName="value">
<FileUpload
maxSize={20}
allowedTypes={['*']}
maxCount={1}
buttonText={intl.formatMessage({ id: 'register.form.upload' })}
onChange={(fileList) => {
if (fileList && fileList.length > 0) {
const file = fileList[0];
if (file.status === 'done' && file.response) {
const response = file.response;
if (response && response.success) {
// 填充文件信息
const fieldValue = form.getFieldValue([
'coscoSupplierSurveyAttachments',
field.name,
]);
form.setFieldsValue({
coscoSupplierSurveyAttachments: [
{
...fieldValue,
fileName: file.name,
fileType: file.type,
fileSize: file.size?.toString(),
filePath: response.filePath || response.url,
},
],
});
message.success(`${file.name} 上传成功`);
}
}
}
}}
/>
</Form.Item>
)}
</div>
))}
<Button
type="dashed"
onClick={() => add({ attachmentsType: 'accessory' })}
icon={<PlusOutlined />}
style={{ marginTop: 8 }}
>
{intl.formatMessage({ id: 'register.attachment.addMore' })}
</Button>
</>
)}
</Form.List>
</Col>
</Row>
</div>
);
};