2026-07-08 14:44:41 +08:00
|
|
|
import { Button, Descriptions, Image, Modal, Table, Upload } from "antd";
|
2026-06-29 11:59:34 +08:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
2026-07-08 14:44:41 +08:00
|
|
|
import FilePreviewModal, {
|
|
|
|
|
FilePreviewContent,
|
|
|
|
|
} from "~/components/FilePreviewModal";
|
2026-06-29 11:59:34 +08:00
|
|
|
import { resolvePreviewSrc } from "~/components/CertPreviewImg";
|
2026-07-08 14:44:41 +08:00
|
|
|
import {
|
|
|
|
|
fetchStaffCertDetail,
|
|
|
|
|
fetchStaffCertListByPersonnelId,
|
|
|
|
|
} from "~/api/staffCertificate";
|
2026-07-07 17:13:33 +08:00
|
|
|
import { fetchOrgPersonnelDetail } from "~/utils/qualFiling/personnelHelper";
|
2026-07-15 16:13:07 +08:00
|
|
|
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP, TITLE_LEVEL_MAP } from "~/enumerate/enterpriseOptions";
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
const GENDER_MAP = { 1: "男", 2: "女" };
|
|
|
|
|
const REGISTER_ENGINEER_MAP = { 1: "是", 2: "否" };
|
|
|
|
|
|
|
|
|
|
export default function StaffViewModal({
|
|
|
|
|
open,
|
|
|
|
|
currentId,
|
2026-07-02 16:58:07 +08:00
|
|
|
requestDetails = fetchOrgPersonnelDetail,
|
2026-06-26 17:28:06 +08:00
|
|
|
requestCertList = fetchStaffCertListByPersonnelId,
|
|
|
|
|
requestCertDetails = fetchStaffCertDetail,
|
|
|
|
|
onCancel,
|
|
|
|
|
}) {
|
|
|
|
|
const [info, setInfo] = useState({});
|
|
|
|
|
const [certificates, setCertificates] = useState([]);
|
|
|
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
|
|
|
const [attachmentPreview, setAttachmentPreview] = useState(null);
|
|
|
|
|
const [certPreview, setCertPreview] = useState(null);
|
|
|
|
|
const [certPreviewLoading, setCertPreviewLoading] = useState(false);
|
|
|
|
|
const [certPreviewInfo, setCertPreviewInfo] = useState(null);
|
2026-06-29 11:59:34 +08:00
|
|
|
const requestDetailsRef = useRef(requestDetails);
|
|
|
|
|
const requestCertListRef = useRef(requestCertList);
|
|
|
|
|
const requestCertDetailsRef = useRef(requestCertDetails);
|
|
|
|
|
requestDetailsRef.current = requestDetails;
|
|
|
|
|
requestCertListRef.current = requestCertList;
|
|
|
|
|
requestCertDetailsRef.current = requestCertDetails;
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open || !currentId) {
|
|
|
|
|
setInfo({});
|
|
|
|
|
setCertificates([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
setDetailLoading(true);
|
2026-07-07 09:07:19 +08:00
|
|
|
|
2026-06-29 11:59:34 +08:00
|
|
|
const loadCertList = requestCertListRef.current;
|
2026-06-26 17:28:06 +08:00
|
|
|
Promise.all([
|
2026-07-08 14:44:41 +08:00
|
|
|
Promise.resolve(requestDetailsRef.current({ id: currentId })).catch(
|
|
|
|
|
(err) => {
|
|
|
|
|
console.warn("[StaffViewModal] load detail failed:", err);
|
|
|
|
|
return { data: {} };
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-06-29 11:59:34 +08:00
|
|
|
typeof loadCertList === "function"
|
|
|
|
|
? Promise.resolve(loadCertList(currentId)).catch((err) => {
|
2026-07-08 14:44:41 +08:00
|
|
|
console.warn("[StaffViewModal] load certificates failed:", err);
|
|
|
|
|
return [];
|
|
|
|
|
})
|
2026-06-26 17:28:06 +08:00
|
|
|
: Promise.resolve([]),
|
2026-07-08 14:44:41 +08:00
|
|
|
])
|
|
|
|
|
.then(([detailRes, certList]) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setInfo(detailRes?.data || {});
|
|
|
|
|
setCertificates(Array.isArray(certList) ? certList : []);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setDetailLoading(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-26 17:28:06 +08:00
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
2026-06-29 11:59:34 +08:00
|
|
|
}, [open, currentId]);
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
const openCertPreview = async (record) => {
|
|
|
|
|
setCertPreview(record);
|
|
|
|
|
setCertPreviewInfo(null);
|
2026-06-29 11:59:34 +08:00
|
|
|
if (typeof requestCertDetailsRef.current !== "function") {
|
2026-06-26 17:28:06 +08:00
|
|
|
setCertPreviewInfo(record);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setCertPreviewLoading(true);
|
|
|
|
|
try {
|
2026-07-07 09:07:19 +08:00
|
|
|
const res = await requestCertDetailsRef.current({ id: record.id });
|
2026-06-26 17:28:06 +08:00
|
|
|
setCertPreviewInfo(res?.data || record);
|
2026-07-07 09:07:19 +08:00
|
|
|
} catch (err) {
|
|
|
|
|
console.warn("[StaffViewModal] load cert detail failed:", err);
|
|
|
|
|
setCertPreviewInfo(record);
|
|
|
|
|
} finally {
|
2026-06-26 17:28:06 +08:00
|
|
|
setCertPreviewLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-08 14:44:41 +08:00
|
|
|
const proofMaterialUrl = info.proofMaterialUrl
|
|
|
|
|
? JSON.parse(info.proofMaterialUrl)
|
|
|
|
|
: [];
|
2026-07-07 15:27:25 +08:00
|
|
|
|
2026-07-31 16:59:40 +08:00
|
|
|
const titleCodeArr = Array.isArray(info.titleCodeArr)
|
|
|
|
|
? info.titleCodeArr.map((item) => {
|
|
|
|
|
let proveUrlList = [];
|
|
|
|
|
if (item.proveUrl) {
|
|
|
|
|
try {
|
|
|
|
|
proveUrlList =
|
|
|
|
|
typeof item.proveUrl === "string"
|
|
|
|
|
? JSON.parse(item.proveUrl)
|
|
|
|
|
: item.proveUrl;
|
|
|
|
|
} catch {
|
|
|
|
|
proveUrlList = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
industryName:
|
|
|
|
|
QUALIFICATION_INDUSTRY_OPTIONS_MAP[item.industryCode] ||
|
|
|
|
|
item.industryCode ||
|
|
|
|
|
"-",
|
|
|
|
|
titleName: TITLE_LEVEL_MAP[item.titleCode] || item.titleCode || "-",
|
|
|
|
|
proveUrlList,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const renderFileList = (files) => {
|
|
|
|
|
if (!files?.length) return "-";
|
|
|
|
|
return files.map((item) => {
|
|
|
|
|
const isImage = /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(
|
|
|
|
|
item.url,
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<div key={item.uid || item.url}>
|
|
|
|
|
{isImage ? (
|
|
|
|
|
<Image
|
|
|
|
|
src={item.url}
|
|
|
|
|
style={{ maxWidth: 150, cursor: "pointer" }}
|
|
|
|
|
preview={{ mask: item.fileName || item.name || "预览" }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Button type="link" onClick={() => window.open(item.url)}>
|
|
|
|
|
{item.fileName || item.name || item.url}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-26 17:28:06 +08:00
|
|
|
const certPreviewFiles = certPreviewInfo?.certImgs?.length
|
|
|
|
|
? certPreviewInfo.certImgs
|
|
|
|
|
: certPreviewInfo?.certImgFiles || [];
|
2026-06-29 11:59:34 +08:00
|
|
|
const certPreviewUrl = resolvePreviewSrc(certPreviewFiles[0]);
|
2026-07-08 14:44:41 +08:00
|
|
|
const certPreviewName =
|
|
|
|
|
certPreviewFiles[0]?.fileName ||
|
|
|
|
|
certPreviewFiles[0]?.name ||
|
|
|
|
|
certPreviewInfo?.certName;
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
2026-07-06 18:06:49 +08:00
|
|
|
destroyOnHidden
|
2026-06-26 17:28:06 +08:00
|
|
|
title="查看"
|
|
|
|
|
width={900}
|
|
|
|
|
loading={detailLoading}
|
|
|
|
|
cancelText="关闭"
|
|
|
|
|
okButtonProps={{ style: { display: "none" } }}
|
|
|
|
|
onCancel={onCancel}
|
|
|
|
|
>
|
|
|
|
|
<Descriptions bordered column={2} labelStyle={{ width: 120 }}>
|
2026-07-14 18:31:33 +08:00
|
|
|
<Descriptions.Item label="姓名">{info.userName}</Descriptions.Item>
|
2026-07-08 14:44:41 +08:00
|
|
|
<Descriptions.Item label="性别">
|
|
|
|
|
{GENDER_MAP[info.genderCode]}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="出生日期">
|
|
|
|
|
{info.birthDate}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-10 16:37:58 +08:00
|
|
|
<Descriptions.Item label="参加工作时间">
|
|
|
|
|
{info.joinWorkDate}
|
|
|
|
|
</Descriptions.Item>
|
2026-06-26 17:28:06 +08:00
|
|
|
<Descriptions.Item label="账号">{info.account}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="部门">{info.deptName}</Descriptions.Item>
|
2026-07-07 15:27:25 +08:00
|
|
|
<Descriptions.Item label="岗位">{info.postName}</Descriptions.Item>
|
2026-07-08 14:44:41 +08:00
|
|
|
<Descriptions.Item label="资质范围">
|
2026-07-15 16:13:07 +08:00
|
|
|
{QUALIFICATION_INDUSTRY_OPTIONS_MAP[info.qualScope] || info.qualScope || "-"}
|
2026-07-08 14:44:41 +08:00
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="学历类型">
|
|
|
|
|
{info.educationTypeName || "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="学历层次">
|
|
|
|
|
{info.educationLevelName || "-"}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-31 16:59:40 +08:00
|
|
|
<Descriptions.Item label="职称信息" span={2}>
|
|
|
|
|
{titleCodeArr.length ? (
|
|
|
|
|
<Table
|
|
|
|
|
size="small"
|
|
|
|
|
bordered
|
|
|
|
|
pagination={false}
|
|
|
|
|
rowKey={(_, idx) => idx}
|
|
|
|
|
dataSource={titleCodeArr}
|
|
|
|
|
columns={[
|
|
|
|
|
{ title: "行业", dataIndex: "industryName", width: 150 },
|
|
|
|
|
{ title: "职称", dataIndex: "titleName", width: 100 },
|
|
|
|
|
{
|
|
|
|
|
title: "证明材料",
|
|
|
|
|
dataIndex: "proveUrlList",
|
|
|
|
|
render: renderFileList,
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
"-"
|
|
|
|
|
)}
|
2026-07-08 14:44:41 +08:00
|
|
|
</Descriptions.Item>
|
2026-06-26 17:28:06 +08:00
|
|
|
<Descriptions.Item label="是否注册安全工程师">
|
|
|
|
|
{REGISTER_ENGINEER_MAP[info.registerEngineerFlag] ?? "-"}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-15 14:26:05 +08:00
|
|
|
<Descriptions.Item label="是否技术负责人">
|
|
|
|
|
{info.technicalDirectorFlag === true ? "是" : info.technicalDirectorFlag === false ? "否" : "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="是否过程控制负责人">
|
|
|
|
|
{info.processControlLeaderFlag === true ? "是" : info.processControlLeaderFlag === false ? "否" : "-"}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-08 14:44:41 +08:00
|
|
|
<Descriptions.Item label="身份证号">
|
|
|
|
|
{info.idCardNo}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
|
|
|
|
<Descriptions.Item label="现住地址" span={2}>
|
|
|
|
|
{info.currentAddress}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="办公地址" span={2}>
|
|
|
|
|
{info.officeAddress}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="毕业院校">
|
|
|
|
|
{info.graduateSchool}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-31 16:59:40 +08:00
|
|
|
|
2026-07-08 14:44:41 +08:00
|
|
|
<Descriptions.Item
|
|
|
|
|
label="出版学术专著、专利、获奖、发表学术论文等"
|
|
|
|
|
span={2}
|
|
|
|
|
>
|
2026-06-26 17:28:06 +08:00
|
|
|
{info.publications || "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="自我申报的专业能力及认定方式" span={2}>
|
|
|
|
|
{info.abilityDeclaration || "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="主要学习工作经历" span={2}>
|
|
|
|
|
{info.workExperience || "-"}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-31 16:59:40 +08:00
|
|
|
<Descriptions.Item label="学历证明材料" span={2}>
|
2026-07-08 14:44:41 +08:00
|
|
|
{proofMaterialUrl.length
|
|
|
|
|
? proofMaterialUrl.map((item) => {
|
2026-07-09 09:23:22 +08:00
|
|
|
const isImage =
|
|
|
|
|
/\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(item.url);
|
2026-07-08 14:44:41 +08:00
|
|
|
return (
|
|
|
|
|
<div key={item.id}>
|
|
|
|
|
{isImage ? (
|
|
|
|
|
<Image
|
|
|
|
|
src={item.url}
|
|
|
|
|
style={{ maxWidth: 200, cursor: "pointer" }}
|
2026-07-09 09:23:22 +08:00
|
|
|
preview={{
|
|
|
|
|
mask: item.fileName || item.name || "预览",
|
|
|
|
|
}}
|
2026-07-08 14:44:41 +08:00
|
|
|
/>
|
|
|
|
|
) : (
|
2026-07-09 09:23:22 +08:00
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
onClick={() => window.open(item.url)}
|
|
|
|
|
>
|
2026-07-08 14:44:41 +08:00
|
|
|
{item.fileName || item.name || item.url}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
: "-"}
|
2026-06-26 17:28:06 +08:00
|
|
|
</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
2026-07-08 14:44:41 +08:00
|
|
|
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
|
|
|
|
|
证照信息
|
|
|
|
|
</div>
|
2026-06-26 17:28:06 +08:00
|
|
|
<Table
|
|
|
|
|
size="small"
|
|
|
|
|
bordered
|
|
|
|
|
rowKey="id"
|
|
|
|
|
pagination={false}
|
|
|
|
|
dataSource={certificates}
|
|
|
|
|
locale={{ emptyText: "暂无证照信息" }}
|
|
|
|
|
columns={[
|
2026-07-16 18:14:16 +08:00
|
|
|
{ title: "证照名称", dataIndex: "certName",ellipsis: true },
|
|
|
|
|
{ title: "证书编号", dataIndex: "certNo",ellipsis: true },
|
|
|
|
|
{ title: "证书类别", dataIndex: "certTypeName",ellipsis: true },
|
|
|
|
|
{ title: "发证机关", dataIndex: "issueOrg",ellipsis: true },
|
2026-06-26 17:28:06 +08:00
|
|
|
{ title: "有效开始日期", dataIndex: "validStartDate", width: 120 },
|
|
|
|
|
{ title: "有效结束日期", dataIndex: "validEndDate", width: 120 },
|
|
|
|
|
{
|
|
|
|
|
title: "操作",
|
|
|
|
|
width: 90,
|
|
|
|
|
render: (_, record) => (
|
2026-07-08 14:44:41 +08:00
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => openCertPreview(record)}
|
|
|
|
|
>
|
|
|
|
|
查看证书
|
|
|
|
|
</Button>
|
2026-06-26 17:28:06 +08:00
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
<FilePreviewModal
|
|
|
|
|
open={!!attachmentPreview}
|
|
|
|
|
title="附件预览"
|
|
|
|
|
fileName={attachmentPreview?.fileName}
|
|
|
|
|
url={attachmentPreview?.url}
|
|
|
|
|
onCancel={() => setAttachmentPreview(null)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
open={!!certPreview}
|
2026-07-06 18:06:49 +08:00
|
|
|
destroyOnHidden
|
2026-06-26 17:28:06 +08:00
|
|
|
title="证书预览"
|
|
|
|
|
width={720}
|
|
|
|
|
loading={certPreviewLoading}
|
|
|
|
|
cancelText="关闭"
|
|
|
|
|
okText={certPreviewUrl ? "新窗口打开" : "关闭"}
|
2026-07-08 14:44:41 +08:00
|
|
|
okButtonProps={{
|
|
|
|
|
style: certPreviewUrl ? undefined : { display: "none" },
|
|
|
|
|
}}
|
2026-06-26 17:28:06 +08:00
|
|
|
onCancel={() => {
|
|
|
|
|
setCertPreview(null);
|
|
|
|
|
setCertPreviewInfo(null);
|
|
|
|
|
}}
|
|
|
|
|
onOk={() => {
|
|
|
|
|
if (certPreviewUrl) {
|
|
|
|
|
window.open(certPreviewUrl, "_blank");
|
|
|
|
|
}
|
|
|
|
|
setCertPreview(null);
|
|
|
|
|
setCertPreviewInfo(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{certPreviewInfo && (
|
|
|
|
|
<div>
|
2026-07-08 14:44:41 +08:00
|
|
|
<Descriptions
|
|
|
|
|
bordered
|
|
|
|
|
column={1}
|
|
|
|
|
size="small"
|
|
|
|
|
style={{ marginBottom: 12 }}
|
|
|
|
|
>
|
|
|
|
|
<Descriptions.Item label="证照名称">
|
|
|
|
|
{certPreviewInfo.certName}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="证书编号">
|
|
|
|
|
{certPreviewInfo.certNo}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="证书类别">
|
2026-07-09 09:23:22 +08:00
|
|
|
{certPreviewInfo.certTypeName || "-"}
|
2026-07-08 14:44:41 +08:00
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="发证机关">
|
|
|
|
|
{certPreviewInfo.issueOrg || "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="有效开始日期">
|
|
|
|
|
{certPreviewInfo.validStartDate || "-"}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="有效结束日期">
|
|
|
|
|
{certPreviewInfo.validEndDate || "-"}
|
|
|
|
|
</Descriptions.Item>
|
2026-07-09 09:23:22 +08:00
|
|
|
<Descriptions.Item label="查看证书">
|
|
|
|
|
{certPreviewInfo.certAttachmentUrl &&
|
2026-07-15 16:13:07 +08:00
|
|
|
certPreviewInfo.certAttachmentUrl.split(",").map((item) => {
|
|
|
|
|
const isImage = /\.(png|jpe?g|gif|bmp|webp|svg)(\?|#|$)/i.test(item);
|
|
|
|
|
return (
|
|
|
|
|
<div key={item} style={{ marginBottom: 4 }}>
|
|
|
|
|
{isImage ? (
|
|
|
|
|
<Image src={item} width={100} style={{ cursor: "pointer" }} />
|
|
|
|
|
) : (
|
|
|
|
|
<a onClick={() => window.open(item)}>{item}</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-07-09 09:23:22 +08:00
|
|
|
</Descriptions.Item>
|
2026-06-26 17:28:06 +08:00
|
|
|
</Descriptions>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Modal>
|
|
|
|
|
</>
|
|
|
|
|
);
|
2026-07-08 14:44:41 +08:00
|
|
|
}
|