621 lines
20 KiB
JavaScript
621 lines
20 KiB
JavaScript
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||
import {
|
||
Button,
|
||
DatePicker,
|
||
Descriptions,
|
||
Form,
|
||
Input,
|
||
message,
|
||
Modal,
|
||
Select,
|
||
Table,
|
||
Upload,
|
||
} from "antd";
|
||
import { useEffect, useState } from "react";
|
||
import dayjs from "dayjs";
|
||
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
||
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
|
||
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
|
||
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
|
||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||
import { NS_STAFF_CERTIFICATE } from "~/enumerate/namespace";
|
||
import CertPreviewImg from "~/components/CertPreviewImg";
|
||
import AttachmentUpload from "~/components/AttachmentUpload";
|
||
import { dateRangeRule } from "~/utils/validators";
|
||
import { CERT_TYPE_OPTIONS, CERT_TYPE_MAP } from "~/enumerate/constant";
|
||
import { QUALIFICATION_INDUSTRY_OPTIONS, QUALIFICATION_INDUSTRY_OPTIONS_MAP } from "~/enumerate/enterpriseOptions";
|
||
|
||
const { router } = tools;
|
||
|
||
function StaffCertificatePage(props) {
|
||
const urlParams = Object.fromEntries(
|
||
new URLSearchParams(window.location.search),
|
||
);
|
||
const staffId = urlParams.staffId;
|
||
const staffName = urlParams.staffName
|
||
? decodeURIComponent(urlParams.staffName)
|
||
: "";
|
||
const [addModalOpen, setAddModalOpen] = useState(false);
|
||
const [viewModalOpen, setViewModalOpen] = useState(false);
|
||
const [currentId, setCurrentId] = useState("");
|
||
const [searchForm] = Form.useForm();
|
||
const { staffCertificate } = props;
|
||
const {
|
||
staffCertificateList: dataSource,
|
||
staffCertificateTotal: total,
|
||
staffCertificateLoading: loading,
|
||
} = staffCertificate || {};
|
||
|
||
useEffect(() => {
|
||
searchForm.setFieldsValue(router.query);
|
||
handleSearch();
|
||
}, []);
|
||
|
||
const handleSearch = () => {
|
||
props.staffCertificateList({ ...router.query, personnelId: staffId });
|
||
};
|
||
|
||
const onDelete = (id) => {
|
||
Modal.confirm({
|
||
title: "提示",
|
||
content: "数据将会删除,您是否确认删除?",
|
||
okText: "是",
|
||
cancelText: "否",
|
||
onOk: async () => {
|
||
const res = await props.staffCertificateRemove({ data: id });
|
||
if (res?.success !== false) {
|
||
message.success("删除成功");
|
||
handleSearch();
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<PageLayout
|
||
history={props.history}
|
||
previous
|
||
title={`人员证书 - ${staffName || ""}`}
|
||
extra={
|
||
<Button
|
||
type="primary"
|
||
onClick={() => {
|
||
setCurrentId("");
|
||
setAddModalOpen(true);
|
||
}}
|
||
>
|
||
新增证书
|
||
</Button>
|
||
}
|
||
>
|
||
<SearchForm
|
||
style={{ marginBottom: 24 }}
|
||
form={searchForm}
|
||
loading={loading}
|
||
formLine={[
|
||
<Form.Item
|
||
key="certName"
|
||
name="certName"
|
||
rules={[{ max: 200, message: "证照名称不能超过200个字符" }]}
|
||
>
|
||
<ControlWrapper.Input
|
||
label="证照名称"
|
||
placeholder="请输入证照名称"
|
||
allowClear
|
||
maxLength={200}
|
||
/>
|
||
</Form.Item>,
|
||
<Form.Item
|
||
key="certNo"
|
||
name="certNo"
|
||
rules={[{ max: 100, message: "证书编号不能超过100个字符" }]}
|
||
>
|
||
<ControlWrapper.Input
|
||
label="证书编号"
|
||
placeholder="请输入证书编号"
|
||
allowClear
|
||
maxLength={100}
|
||
/>
|
||
</Form.Item>,
|
||
<Form.Item
|
||
key="certCategoryName"
|
||
name="certCategoryName"
|
||
rules={[{ max: 50, message: "证书类别名称不能超过50个字符" }]}
|
||
>
|
||
<ControlWrapper.Input
|
||
label="证书类别名称"
|
||
placeholder="请输入证书类别名称"
|
||
allowClear
|
||
maxLength={50}
|
||
/>
|
||
</Form.Item>,
|
||
<Form.Item
|
||
key="operationCategoryName"
|
||
name="operationCategoryName"
|
||
rules={[{ max: 50, message: "证书作业类别名称不能超过50个字符" }]}
|
||
>
|
||
<ControlWrapper.Input
|
||
label="证书作业类别名称"
|
||
placeholder="请输入证书作业类别名称"
|
||
allowClear
|
||
maxLength={50}
|
||
/>
|
||
</Form.Item>,
|
||
]}
|
||
onReset={(values) => {
|
||
router.query = { ...values, current: 1, size: 10 };
|
||
handleSearch();
|
||
}}
|
||
onFinish={(values) => {
|
||
router.query = { ...values, current: 1, size: 10 };
|
||
handleSearch();
|
||
}}
|
||
/>
|
||
<Table
|
||
rowKey="id"
|
||
columns={[
|
||
{ title: "证照名称", dataIndex: "certName", ellipsis: true },
|
||
{
|
||
title: "证书类型",
|
||
dataIndex: "certTypeCode",
|
||
render: (v) => CERT_TYPE_MAP[v] || v,
|
||
},
|
||
{ title: "证书类别", dataIndex: "certCategoryName" },
|
||
{ title: "证书作业类别", dataIndex: "operationCategoryName" },
|
||
{ title: "证书编号", dataIndex: "certNo" },
|
||
{
|
||
title: "操作",
|
||
width: 200,
|
||
render: (_, record) => (
|
||
<TableAction>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => {
|
||
setCurrentId(record.id);
|
||
setViewModalOpen(true);
|
||
}}
|
||
>
|
||
查看
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => {
|
||
setCurrentId(record.id);
|
||
setAddModalOpen(true);
|
||
}}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
danger
|
||
type="link"
|
||
size="small"
|
||
onClick={() => onDelete(record.id)}
|
||
>
|
||
删除
|
||
</Button>
|
||
</TableAction>
|
||
),
|
||
},
|
||
]}
|
||
dataSource={dataSource}
|
||
scroll={{ y: props.scrollY }}
|
||
loading={loading}
|
||
pagination={{
|
||
total,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
current: Number(router.query.current) || 1,
|
||
pageSize: Number(router.query.size) || 10,
|
||
onChange: (page, pageSize) => {
|
||
router.query = { ...router.query, current: page, size: pageSize };
|
||
handleSearch();
|
||
},
|
||
}}
|
||
/>
|
||
|
||
{addModalOpen && (
|
||
<CertModal
|
||
open={addModalOpen}
|
||
staffId={staffId}
|
||
currentId={currentId}
|
||
staffCertificateAdd={props.staffCertificateAdd}
|
||
staffCertificateEdit={props.staffCertificateEdit}
|
||
staffCertificateInfo={props.staffCertificateInfo}
|
||
queryIndustryProfessionalStandards={
|
||
props.queryIndustryProfessionalStandards
|
||
}
|
||
onCancel={() => {
|
||
setAddModalOpen(false);
|
||
setCurrentId("");
|
||
}}
|
||
onSuccess={handleSearch}
|
||
/>
|
||
)}
|
||
{viewModalOpen && (
|
||
<ViewModal
|
||
open={viewModalOpen}
|
||
currentId={currentId}
|
||
staffCertificateInfo={props.staffCertificateInfo}
|
||
onCancel={() => {
|
||
setViewModalOpen(false);
|
||
setCurrentId("");
|
||
}}
|
||
/>
|
||
)}
|
||
</PageLayout>
|
||
);
|
||
}
|
||
|
||
function CertModal({
|
||
open,
|
||
staffId,
|
||
currentId,
|
||
staffCertificateAdd,
|
||
staffCertificateEdit,
|
||
staffCertificateInfo,
|
||
queryIndustryProfessionalStandards,
|
||
onCancel,
|
||
onSuccess,
|
||
}) {
|
||
const [form] = Form.useForm();
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
const [uploadFileList, setUploadFileList] = useState([]);
|
||
const [professionalCapabilityOptions, setProfessionalCapabilityOptions] =
|
||
useState([]);
|
||
|
||
const loadProfessionalCapabilities = async (industryCode) => {
|
||
if (!industryCode) {
|
||
setProfessionalCapabilityOptions([]);
|
||
form.setFieldsValue({ professionalCapabilityId: undefined });
|
||
return;
|
||
}
|
||
const res = await queryIndustryProfessionalStandards({
|
||
industryCode,
|
||
pageSize: 999,
|
||
});
|
||
const list = res?.data || [];
|
||
setProfessionalCapabilityOptions(
|
||
list.map((item) => ({
|
||
value: item.id,
|
||
label: item.professionalCapabilityName,
|
||
})),
|
||
);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
if (!currentId) {
|
||
form.resetFields();
|
||
setUploadFileList([]);
|
||
setProfessionalCapabilityOptions([]);
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
(async () => {
|
||
setDetailLoading(true);
|
||
try {
|
||
const res = await staffCertificateInfo({ id: currentId });
|
||
if (cancelled || !res?.data) return;
|
||
const data = { ...res.data };
|
||
// 将 validStartDate/validEndDate 合并为 validDate(RangePicker 需要 dayjs 对象)
|
||
if (data.validStartDate || data.validEndDate) {
|
||
data.validDate = [
|
||
data.validStartDate ? dayjs(data.validStartDate) : undefined,
|
||
data.validEndDate ? dayjs(data.validEndDate) : undefined,
|
||
];
|
||
}
|
||
// 复核日期需要转为 dayjs 对象
|
||
if (data.reviewDate) {
|
||
data.reviewDate = dayjs(data.reviewDate);
|
||
}
|
||
// 处理证书附件 fileList
|
||
let fileList = [];
|
||
if (Array.isArray(data.certImgFiles)) {
|
||
fileList = data.certImgFiles.map((f, i) => ({
|
||
uid: f.uid || `-${i}`,
|
||
name: f.fileName || f.name || `证书附件${i + 1}`,
|
||
status: "done",
|
||
url: f.url,
|
||
}));
|
||
} else if (data.certAttachmentUrl) {
|
||
fileList = data.certAttachmentUrl
|
||
.split(",")
|
||
.filter(Boolean)
|
||
.map((url, i) => ({
|
||
uid: `-${i}`,
|
||
name: url.split("/").pop() || `证书附件${i + 1}`,
|
||
status: "done",
|
||
url,
|
||
}));
|
||
}
|
||
data.certAttachmentUrl = fileList;
|
||
setUploadFileList(fileList);
|
||
form.setFieldsValue(data);
|
||
if (data.businessScope) {
|
||
loadProfessionalCapabilities(data.businessScope);
|
||
}
|
||
} catch (err) {
|
||
console.warn("[StaffCertificate] load detail failed:", err);
|
||
} finally {
|
||
if (!cancelled) setDetailLoading(false);
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [open, currentId]);
|
||
|
||
const handleCancel = () => {
|
||
form.resetFields();
|
||
setUploadFileList([]);
|
||
onCancel();
|
||
};
|
||
|
||
const handleSubmit = async (values) => {
|
||
try {
|
||
setSubmitting(true);
|
||
const payload = {
|
||
...values,
|
||
personnelId: staffId,
|
||
validStartDate: values.validDate?.[0]
|
||
? dayjs(values.validDate[0]).format("YYYY-MM-DD")
|
||
: undefined,
|
||
validEndDate: values.validDate?.[1]
|
||
? dayjs(values.validDate[1]).format("YYYY-MM-DD")
|
||
: undefined,
|
||
reviewDate: values.reviewDate
|
||
? dayjs(values.reviewDate).format("YYYY-MM-DD")
|
||
: undefined,
|
||
certAttachmentUrl: Array.isArray(values.certAttachmentUrl)
|
||
? values.certAttachmentUrl
|
||
.map((f) => f.url || f.response?.url)
|
||
.filter(Boolean)
|
||
.join(",")
|
||
: "",
|
||
};
|
||
delete payload.validDate;
|
||
delete payload.certImgFiles;
|
||
if (currentId) payload.id = currentId;
|
||
const request = currentId ? staffCertificateEdit : staffCertificateAdd;
|
||
const res = await request(payload);
|
||
if (res?.success !== false) {
|
||
message.success(currentId ? "编辑成功" : "添加成功");
|
||
handleCancel();
|
||
onSuccess();
|
||
} else {
|
||
}
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
destroyOnHidden
|
||
title={currentId ? "编辑证书" : "添加证书"}
|
||
width={800}
|
||
loading={detailLoading}
|
||
confirmLoading={submitting}
|
||
onCancel={handleCancel}
|
||
onOk={form.submit}
|
||
>
|
||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||
<Form.Item name="businessScope" label="专业范围" rules={[{ required: true, message: "请选择专业范围" }]}>
|
||
<Select
|
||
placeholder="请选择专业范围"
|
||
allowClear
|
||
options={QUALIFICATION_INDUSTRY_OPTIONS}
|
||
onChange={loadProfessionalCapabilities}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="professionalCapabilityId" label="专业能力" rules={[{ required: true, message: "请选择专业能力" }]}>
|
||
<Select
|
||
placeholder="请先选择专业范围"
|
||
allowClear
|
||
options={professionalCapabilityOptions}
|
||
disabled={professionalCapabilityOptions.length === 0}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="certName"
|
||
label="证照名称"
|
||
rules={[
|
||
{ required: true, message: "请输入证照名称" },
|
||
{ max: 200, message: "证照名称不能超过200个字符" },
|
||
]}
|
||
>
|
||
<Input placeholder="请输入证照名称" maxLength={200} showCount />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="certNo"
|
||
label="证书编号"
|
||
rules={[
|
||
{ required: true, message: "请输入证书编号" },
|
||
{ max: 100, message: "证书编号不能超过100个字符" },
|
||
]}
|
||
>
|
||
<Input placeholder="请输入证书编号" maxLength={100} showCount />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="issueOrg"
|
||
label="发证机关"
|
||
rules={[
|
||
{ required: true, message: "请输入发证机关" },
|
||
{ max: 200, message: "发证机关不能超过200个字符" },
|
||
]}
|
||
>
|
||
<Input placeholder="请输入发证机关" maxLength={200} showCount />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="validDate"
|
||
label="证书有效开始/结束日期"
|
||
rules={[
|
||
{ required: true, message: "请选择证书有效期" },
|
||
dateRangeRule(),
|
||
]}
|
||
>
|
||
<DatePicker.RangePicker style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="certTypeCode"
|
||
label="证书类型"
|
||
rules={[{ required: true, message: "请选择证书类型" }]}
|
||
>
|
||
<Select placeholder="请选择证书类型" options={CERT_TYPE_OPTIONS} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="certCategoryCode"
|
||
label="证书类别编码"
|
||
rules={[{ max: 32, message: "证书类别编码不能超过32个字符" }]}
|
||
>
|
||
<Input placeholder="请输入证书类别编码" maxLength={32} showCount />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="certCategoryName"
|
||
label="证书类别名称"
|
||
rules={[{ max: 50, message: "证书类别名称不能超过50个字符" }]}
|
||
>
|
||
<Input placeholder="请输入证书类别名称" maxLength={50} showCount />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="operationCategoryCode"
|
||
label="证书作业类别编码"
|
||
rules={[{ max: 32, message: "证书作业类别编码不能超过32个字符" }]}
|
||
>
|
||
<Input
|
||
placeholder="请输入证书作业类别编码"
|
||
maxLength={32}
|
||
showCount
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="operationCategoryName"
|
||
label="证书作业类别名称"
|
||
rules={[{ max: 50, message: "证书作业类别名称不能超过50个字符" }]}
|
||
>
|
||
<Input
|
||
placeholder="请输入证书作业类别名称"
|
||
maxLength={50}
|
||
showCount
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="reviewDate" label="复核日期">
|
||
<DatePicker style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
<AttachmentUpload
|
||
name="certAttachmentUrl"
|
||
label="证书附件"
|
||
maxCount={10}
|
||
accept=".png,.jpg,.jpeg,.bmp,.webp,.pdf"
|
||
extra={"请上传证书附件,支持10个以内的图片和PDF格式"}
|
||
/>
|
||
</Form>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
function ViewModal({ open, currentId, staffCertificateInfo, onCancel }) {
|
||
const [info, setInfo] = useState({});
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!open || !currentId) {
|
||
setInfo({});
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
(async () => {
|
||
setDetailLoading(true);
|
||
try {
|
||
const res = await staffCertificateInfo({ id: currentId });
|
||
if (cancelled || !res?.data) return;
|
||
const data = { ...res.data };
|
||
// 处理 certImgFiles
|
||
if (Array.isArray(data.certImgFiles)) {
|
||
data.certDisplayFiles = data.certImgFiles;
|
||
} else if (data.certAttachmentUrl) {
|
||
data.certDisplayFiles = data.certAttachmentUrl
|
||
.split(",")
|
||
.filter(Boolean)
|
||
.map((url, i) => ({
|
||
uid: `-${i}`,
|
||
fileName: url.split("/").pop() || `证书附件${i + 1}`,
|
||
url,
|
||
}));
|
||
} else {
|
||
data.certDisplayFiles = [];
|
||
}
|
||
setInfo(data);
|
||
} catch (err) {
|
||
console.warn("[StaffCertificate] load view failed:", err);
|
||
} finally {
|
||
if (!cancelled) setDetailLoading(false);
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [open, currentId]);
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
destroyOnHidden
|
||
title="证书详情"
|
||
width={800}
|
||
loading={detailLoading}
|
||
cancelText="返回"
|
||
okButtonProps={{ style: { display: "none" } }}
|
||
onCancel={onCancel}
|
||
>
|
||
<Descriptions bordered column={1} labelStyle={{ width: 160 }}>
|
||
<Descriptions.Item label="专业范围">{QUALIFICATION_INDUSTRY_OPTIONS_MAP[info.businessScope] }</Descriptions.Item>
|
||
<Descriptions.Item label="专业能力">{info.professionalCapabilityName }</Descriptions.Item>
|
||
<Descriptions.Item label="证照名称">{info.certName}</Descriptions.Item>
|
||
<Descriptions.Item label="证书类型">
|
||
{CERT_TYPE_MAP[info.certTypeCode] || info.certTypeCode}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书类别编码">
|
||
{info.certCategoryCode}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书类别名称">
|
||
{info.certCategoryName}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书作业类别编码">
|
||
{info.operationCategoryCode}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书作业类别名称">
|
||
{info.operationCategoryName}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书编号">{info.certNo}</Descriptions.Item>
|
||
<Descriptions.Item label="发证机关">{info.issueOrg}</Descriptions.Item>
|
||
<Descriptions.Item label="证书有效开始日期">
|
||
{info.validStartDate}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书有效结束日期">
|
||
{info.validEndDate}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="复核日期">
|
||
{info.reviewDate}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="证书图片">
|
||
<CertPreviewImg files={info.certDisplayFiles} />
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
export default Connect(
|
||
[NS_STAFF_CERTIFICATE],
|
||
true,
|
||
)(AntdTableFuncControl(StaffCertificatePage));
|