2026-08-10 09:17:49 +08:00
|
|
|
|
import { Button, Modal, message } from "antd";
|
|
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
|
import { fetchOrgPersonnelDetail } from "~/utils/qualFiling/personnelHelper";
|
2026-08-20 17:26:37 +08:00
|
|
|
|
import { fillDocxTemplate } from "~/utils/fillDocxTemplate";
|
2026-08-10 09:17:49 +08:00
|
|
|
|
import { ABILITY_SOURCE_MAP } from "~/enumerate/enterpriseOptions";
|
|
|
|
|
|
import {
|
|
|
|
|
|
CERT_LEVEL_LABEL_BY_TYPE,
|
|
|
|
|
|
CAPABILITY_MAP,
|
|
|
|
|
|
} from "~/enumerate/constant";
|
|
|
|
|
|
import "./index.less";
|
|
|
|
|
|
|
2026-08-20 17:26:37 +08:00
|
|
|
|
const RESUME_TEMPLATE_URL = "/安全评价师简历表模板.docx";
|
|
|
|
|
|
|
2026-08-10 09:17:49 +08:00
|
|
|
|
const GENDER_MAP = { 1: "男", 2: "女" };
|
|
|
|
|
|
|
|
|
|
|
|
const parseFileList = (raw) => {
|
|
|
|
|
|
if (!raw) return [];
|
|
|
|
|
|
if (Array.isArray(raw)) return raw;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const maskIdCard = (val) => {
|
|
|
|
|
|
if (!val || String(val).length < 8) return val || "-";
|
|
|
|
|
|
const s = String(val);
|
|
|
|
|
|
return `${s.slice(0, 14)}****`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const maskPhone = (val) => {
|
|
|
|
|
|
if (!val || String(val).length < 7) return val || "-";
|
|
|
|
|
|
const s = String(val);
|
|
|
|
|
|
return `${s.slice(0, 3)}****${s.slice(-4)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const display = (val) => {
|
|
|
|
|
|
if (val === 0) return "0";
|
|
|
|
|
|
if (val == null || val === "") return "-";
|
|
|
|
|
|
return val;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function StaffResumeModal({
|
|
|
|
|
|
open,
|
|
|
|
|
|
currentId,
|
|
|
|
|
|
requestDetails = fetchOrgPersonnelDetail,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [info, setInfo] = useState({});
|
|
|
|
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
|
|
|
|
const [downloading, setDownloading] = useState(false);
|
|
|
|
|
|
const requestDetailsRef = useRef(requestDetails);
|
|
|
|
|
|
requestDetailsRef.current = requestDetails;
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!open || !currentId) {
|
|
|
|
|
|
setInfo({});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
setDetailLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
Promise.resolve(requestDetailsRef.current({ id: currentId }))
|
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
|
console.warn("[StaffResumeModal] load detail failed:", err);
|
|
|
|
|
|
return { data: {} };
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((detailRes) => {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setInfo(detailRes?.data || {});
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setDetailLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [open, currentId]);
|
|
|
|
|
|
|
|
|
|
|
|
const evalCert = info.evaluatorCert;
|
|
|
|
|
|
const evalLevelMap = CERT_LEVEL_LABEL_BY_TYPE.evaluator;
|
|
|
|
|
|
const evalLevelName = evalLevelMap[evalCert?.certLevel] || "";
|
|
|
|
|
|
const qualificationText =
|
|
|
|
|
|
evalLevelName || evalCert?.certNo
|
|
|
|
|
|
? [evalLevelName, evalCert?.certNo].filter(Boolean).join(" / ")
|
|
|
|
|
|
: "-";
|
|
|
|
|
|
|
|
|
|
|
|
const regCert = info.registeredSafetyEngineerCert;
|
|
|
|
|
|
const regCertFiles = parseFileList(regCert?.certAttachmentUrl);
|
|
|
|
|
|
|
|
|
|
|
|
const educationText = [info.educationTypeName, info.educationLevelName]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join("") || "-";
|
|
|
|
|
|
|
|
|
|
|
|
const schoolMajorText = [info.graduateSchool, info.basicDisciplineMajorName]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(" / ") || "-";
|
|
|
|
|
|
|
|
|
|
|
|
const capabilityText = Array.isArray(info.capabilityAssessment)
|
|
|
|
|
|
? info.capabilityAssessment
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
const name =
|
|
|
|
|
|
CAPABILITY_MAP[item.professionalCapabilityCode] ||
|
|
|
|
|
|
item.professionalCapabilityCode ||
|
|
|
|
|
|
"";
|
|
|
|
|
|
const source =
|
|
|
|
|
|
ABILITY_SOURCE_MAP[item.professionalCapabilitySourceCode] ||
|
|
|
|
|
|
item.professionalCapabilitySourceCode ||
|
|
|
|
|
|
"";
|
|
|
|
|
|
if (!name && !source) return "";
|
|
|
|
|
|
return [name, source].filter(Boolean).join(" / ");
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(";") || "-"
|
|
|
|
|
|
: "-";
|
|
|
|
|
|
|
|
|
|
|
|
const handleDownload = async () => {
|
|
|
|
|
|
setDownloading(true);
|
|
|
|
|
|
try {
|
2026-08-20 17:26:37 +08:00
|
|
|
|
const data = {
|
|
|
|
|
|
userName: display(info.userName),
|
|
|
|
|
|
gender: GENDER_MAP[info.genderCode] || "-",
|
|
|
|
|
|
birthDate: display(info.birthDate),
|
|
|
|
|
|
currentAddress: display(info.currentAddress),
|
|
|
|
|
|
officeAddress: display(info.officeAddress),
|
|
|
|
|
|
qualificationText,
|
|
|
|
|
|
certNo: display(regCert?.certNo),
|
|
|
|
|
|
idCardNo: maskIdCard(info.idCardNo),
|
|
|
|
|
|
phone: maskPhone(info.account),
|
|
|
|
|
|
education: educationText,
|
|
|
|
|
|
schoolMajor: schoolMajorText,
|
|
|
|
|
|
publications: info.publications || "无",
|
|
|
|
|
|
capability: capabilityText,
|
|
|
|
|
|
certFiles:
|
|
|
|
|
|
regCertFiles
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(item) =>
|
|
|
|
|
|
item.fileName || item.name || "查看注册安全工程师资格证",
|
|
|
|
|
|
)
|
|
|
|
|
|
.join("\n") || "-",
|
|
|
|
|
|
workExperience: display(info.workExperience),
|
|
|
|
|
|
};
|
|
|
|
|
|
await fillDocxTemplate(
|
|
|
|
|
|
RESUME_TEMPLATE_URL,
|
|
|
|
|
|
data,
|
|
|
|
|
|
`安全评价师简历表_${info.userName || currentId || ""}.docx`,
|
|
|
|
|
|
{ download: true },
|
|
|
|
|
|
);
|
2026-08-10 09:17:49 +08:00
|
|
|
|
message.success("简历下载成功");
|
|
|
|
|
|
} catch (err) {
|
2026-08-20 17:26:37 +08:00
|
|
|
|
console.warn("[StaffResumeModal] download word failed:", err);
|
2026-08-10 09:17:49 +08:00
|
|
|
|
message.error("简历下载失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setDownloading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
destroyOnHidden
|
2026-08-11 11:20:20 +08:00
|
|
|
|
title={'安全评价师简历表'}
|
2026-08-10 09:17:49 +08:00
|
|
|
|
width={860}
|
|
|
|
|
|
loading={detailLoading}
|
|
|
|
|
|
className="staff-resume-modal"
|
|
|
|
|
|
onCancel={onCancel}
|
|
|
|
|
|
footer={[
|
|
|
|
|
|
<Button key="download" type="primary" loading={downloading} onClick={handleDownload}>
|
|
|
|
|
|
下载简历
|
|
|
|
|
|
</Button>,
|
|
|
|
|
|
<Button key="close" onClick={onCancel}>
|
|
|
|
|
|
关闭
|
|
|
|
|
|
</Button>,
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-08-20 17:26:37 +08:00
|
|
|
|
<div style={{ background: "#fff", padding: 8 }}>
|
2026-08-11 11:20:20 +08:00
|
|
|
|
|
2026-08-10 09:17:49 +08:00
|
|
|
|
<table className="staff-resume-table">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">姓名</td>
|
|
|
|
|
|
<td className="resume-value">{display(info.userName)}</td>
|
|
|
|
|
|
<td className="resume-label">性别</td>
|
|
|
|
|
|
<td className="resume-value">
|
|
|
|
|
|
{GENDER_MAP[info.genderCode] || "-"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">出生日期</td>
|
|
|
|
|
|
<td className="resume-value">{display(info.birthDate)}</td>
|
|
|
|
|
|
<td className="resume-label">现住址</td>
|
|
|
|
|
|
<td className="resume-value">{display(info.currentAddress)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">办公地址</td>
|
|
|
|
|
|
<td className="resume-value">{display(info.officeAddress)}</td>
|
|
|
|
|
|
<td className="resume-label">职业资格等级及证书号码</td>
|
|
|
|
|
|
<td className="resume-value">{qualificationText}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">注册安全工程师证书编号</td>
|
|
|
|
|
|
<td className="resume-value">{display(regCert?.certNo)}</td>
|
|
|
|
|
|
<td className="resume-label">身份证件号码</td>
|
|
|
|
|
|
<td className="resume-value">{maskIdCard(info.idCardNo)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">联系电话</td>
|
|
|
|
|
|
<td className="resume-value">{maskPhone(info.account)}</td>
|
|
|
|
|
|
<td className="resume-label">学历学位</td>
|
|
|
|
|
|
<td className="resume-value">{educationText}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">毕业院校及专业</td>
|
|
|
|
|
|
<td className="resume-value">{schoolMajorText}</td>
|
|
|
|
|
|
<td className="resume-label">
|
|
|
|
|
|
出版学术专著、专利、科技发明及获奖情况
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="resume-value">
|
|
|
|
|
|
{info.publications ? info.publications : "无"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label-wide">
|
|
|
|
|
|
自我申报的专业能力及认定方式
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="resume-value-full" colSpan={3}>
|
|
|
|
|
|
{capabilityText}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label-wide">申报专业能力证明材料</td>
|
|
|
|
|
|
<td className="resume-value-full" colSpan={3}>
|
|
|
|
|
|
{regCertFiles.length
|
|
|
|
|
|
? regCertFiles.map((item) => (
|
|
|
|
|
|
<div key={item.uid || item.url}>
|
|
|
|
|
|
<a
|
|
|
|
|
|
className="resume-link"
|
|
|
|
|
|
href={item.url}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.fileName || item.name || "查看注册安全工程师资格证"}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
: "-"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label-wide">主要学习工作经历</td>
|
|
|
|
|
|
<td className="resume-value-full" colSpan={3}>
|
|
|
|
|
|
{display(info.workExperience)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
2026-08-10 18:14:55 +08:00
|
|
|
|
<tr>
|
|
|
|
|
|
<td className="resume-label">本人签字</td>
|
|
|
|
|
|
<td className="resume-sign"></td>
|
|
|
|
|
|
<td className="resume-label">从业机构确认</td>
|
|
|
|
|
|
<td className="resume-sign"></td>
|
|
|
|
|
|
</tr>
|
2026-08-10 09:17:49 +08:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|