2026-08-10 09:17:49 +08:00
|
|
|
|
import { Button, Modal, message } from "antd";
|
|
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
|
import html2canvas from "html2canvas";
|
|
|
|
|
|
import { jsPDF } from "jspdf";
|
|
|
|
|
|
import { fetchOrgPersonnelDetail } from "~/utils/qualFiling/personnelHelper";
|
|
|
|
|
|
import { ABILITY_SOURCE_MAP } from "~/enumerate/enterpriseOptions";
|
|
|
|
|
|
import {
|
|
|
|
|
|
CERT_LEVEL_LABEL_BY_TYPE,
|
|
|
|
|
|
CAPABILITY_MAP,
|
|
|
|
|
|
} from "~/enumerate/constant";
|
|
|
|
|
|
import "./index.less";
|
|
|
|
|
|
|
|
|
|
|
|
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 resumeRef = useRef(null);
|
|
|
|
|
|
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 () => {
|
|
|
|
|
|
if (!resumeRef.current) return;
|
|
|
|
|
|
setDownloading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const canvas = await html2canvas(resumeRef.current, {
|
|
|
|
|
|
scale: 2,
|
|
|
|
|
|
useCORS: true,
|
|
|
|
|
|
backgroundColor: "#ffffff",
|
|
|
|
|
|
logging: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
const imgData = canvas.toDataURL("image/jpeg", 0.95);
|
|
|
|
|
|
const pdf = new jsPDF({
|
|
|
|
|
|
orientation: "portrait",
|
|
|
|
|
|
unit: "mm",
|
|
|
|
|
|
format: "a4",
|
|
|
|
|
|
});
|
|
|
|
|
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
|
|
|
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
|
|
|
|
const margin = 10;
|
|
|
|
|
|
const contentWidth = pageWidth - margin * 2;
|
|
|
|
|
|
const contentHeight = (canvas.height * contentWidth) / canvas.width;
|
|
|
|
|
|
|
|
|
|
|
|
let heightLeft = contentHeight;
|
|
|
|
|
|
let position = margin;
|
|
|
|
|
|
|
|
|
|
|
|
pdf.addImage(imgData, "JPEG", margin, position, contentWidth, contentHeight);
|
|
|
|
|
|
heightLeft -= pageHeight - margin * 2;
|
|
|
|
|
|
|
|
|
|
|
|
while (heightLeft > 0) {
|
|
|
|
|
|
position = margin - (contentHeight - heightLeft);
|
|
|
|
|
|
pdf.addPage();
|
|
|
|
|
|
pdf.addImage(imgData, "JPEG", margin, position, contentWidth, contentHeight);
|
|
|
|
|
|
heightLeft -= pageHeight - margin * 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fileName = `安全评价师简历表_${info.userName || currentId || ""}.pdf`;
|
|
|
|
|
|
pdf.save(fileName);
|
|
|
|
|
|
message.success("简历下载成功");
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn("[StaffResumeModal] download pdf failed:", err);
|
|
|
|
|
|
message.error("简历下载失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setDownloading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
destroyOnHidden
|
|
|
|
|
|
title={null}
|
|
|
|
|
|
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>,
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div ref={resumeRef} style={{ background: "#fff", padding: 8 }}>
|
|
|
|
|
|
<div className="staff-resume-title">安全评价师简历表</div>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|