代码提交-员工信息管理新增功能,查看简历,下载简历
parent
a388bd3401
commit
6f90fc66df
File diff suppressed because it is too large
Load Diff
|
|
@ -33,6 +33,8 @@
|
|||
"dayjs": "^1.11.7",
|
||||
"echarts": "^6.1.0",
|
||||
"history": "^4.10.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
|||
2171
pnpm-lock.yaml
2171
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,270 @@
|
|||
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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
.staff-resume-modal {
|
||||
.staff-resume-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
.staff-resume-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #bfbfbf;
|
||||
padding: 10px 12px;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.resume-label {
|
||||
width: 18%;
|
||||
background: #f5f5f5;
|
||||
color: #595959;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.resume-value {
|
||||
width: 32%;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.resume-label-wide {
|
||||
width: 22%;
|
||||
background: #f5f5f5;
|
||||
color: #595959;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.resume-value-full {
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.7;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.resume-link {
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ import { getBirthDateFromIdCard } from "~/utils";
|
|||
import { idCardRule, mobileRule } from "~/utils/validators";
|
||||
import AttachmentUpload from "~/components/AttachmentUpload";
|
||||
import StaffViewModal from "~/components/StaffViewModal";
|
||||
import StaffResumeModal from "~/components/StaffResumeModal";
|
||||
import "./index.less";
|
||||
|
||||
const { router } = tools;
|
||||
|
|
@ -58,6 +59,7 @@ const evalCertLevelMap = CERT_LEVEL_LABEL_BY_TYPE.evaluator;
|
|||
function PersonnelInfoPage(props) {
|
||||
const [formModalOpen, setFormModalOpen] = useState(false);
|
||||
const [viewModalOpen, setViewModalOpen] = useState(false);
|
||||
const [resumeModalOpen, setResumeModalOpen] = useState(false);
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
const [resetPasswordModalOpen, setResetPasswordModalOpen] = useState(false);
|
||||
const [currentId, setCurrentId] = useState("");
|
||||
|
|
@ -263,6 +265,16 @@ function PersonnelInfoPage(props) {
|
|||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCurrentId(record.id);
|
||||
setResumeModalOpen(true);
|
||||
}}
|
||||
>
|
||||
查看简历
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
|
|
@ -339,6 +351,16 @@ function PersonnelInfoPage(props) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{resumeModalOpen && (
|
||||
<StaffResumeModal
|
||||
open={resumeModalOpen}
|
||||
currentId={currentId}
|
||||
onCancel={() => {
|
||||
setResumeModalOpen(false);
|
||||
setCurrentId("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{importModalOpen && (
|
||||
<StaffImportModal
|
||||
open={importModalOpen}
|
||||
|
|
|
|||
Loading…
Reference in New Issue