tangjie 2026-08-08 13:51:42 +08:00
parent 9908a0093a
commit 942fc4a55f
5 changed files with 514 additions and 477 deletions

View File

@ -13,9 +13,9 @@ module.exports = {
// API_HOST: "https://gbs-gateway.qhdsafety.com", // API_HOST: "https://gbs-gateway.qhdsafety.com",
// API_HOST: "http://192.168.0.134", // API_HOST: "http://192.168.0.134",
// API_HOST: "http://192.168.0.150", //太浅 API_HOST: "http://192.168.0.150", //太浅
// API_HOST: "http://192.168.0.152", // API_HOST: "http://192.168.0.152",
API_HOST: "http://192.168.0.103", //huwei //API_HOST: "http://192.168.0.103", //huwei
}, },
production: { production: {
// 应用后端分支名称,部署上线需要 // 应用后端分支名称,部署上线需要

View File

@ -1,72 +1,59 @@
import { Button, Descriptions, Image, Modal, Table, Upload } from "antd"; import { Button, Descriptions, Image, Modal, Table } from "antd";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import FilePreviewModal, {
FilePreviewContent,
} from "~/components/FilePreviewModal";
import { resolvePreviewSrc } from "~/components/CertPreviewImg";
import {
fetchStaffCertDetail,
fetchStaffCertListByPersonnelId,
} from "~/api/staffCertificate";
import { fetchOrgPersonnelDetail } from "~/utils/qualFiling/personnelHelper"; import { fetchOrgPersonnelDetail } from "~/utils/qualFiling/personnelHelper";
import { import {
QUALIFICATION_INDUSTRY_OPTIONS_MAP, REGISTER_ENGINEER_CATEGORY_MAP,
PERSONNEL_POST_TYPE_MAP,
ABILITY_SOURCE_MAP,
TITLE_LEVEL_MAP, TITLE_LEVEL_MAP,
} from "~/enumerate/enterpriseOptions"; } from "~/enumerate/enterpriseOptions";
import {
CERT_LEVEL_LABEL_BY_TYPE,
CAPABILITY_MAP,
} from "~/enumerate/constant";
const GENDER_MAP = { 1: "男", 2: "女" }; const GENDER_MAP = { 1: "男", 2: "女" };
const REGISTER_ENGINEER_MAP = { 1: "是", 2: "否" }; const REGISTER_ENGINEER_MAP = { 1: "是", 2: "否" };
const EVALUATOR_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 [];
}
};
export default function StaffViewModal({ export default function StaffViewModal({
open, open,
currentId, currentId,
requestDetails = fetchOrgPersonnelDetail, requestDetails = fetchOrgPersonnelDetail,
requestCertList = fetchStaffCertListByPersonnelId,
requestCertDetails = fetchStaffCertDetail,
onCancel, onCancel,
}) { }) {
const [info, setInfo] = useState({}); const [info, setInfo] = useState({});
const [certificates, setCertificates] = useState([]);
const [detailLoading, setDetailLoading] = useState(false); 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);
const requestDetailsRef = useRef(requestDetails); const requestDetailsRef = useRef(requestDetails);
const requestCertListRef = useRef(requestCertList);
const requestCertDetailsRef = useRef(requestCertDetails);
requestDetailsRef.current = requestDetails; requestDetailsRef.current = requestDetails;
requestCertListRef.current = requestCertList;
requestCertDetailsRef.current = requestCertDetails;
useEffect(() => { useEffect(() => {
if (!open || !currentId) { if (!open || !currentId) {
setInfo({}); setInfo({});
setCertificates([]);
return; return;
} }
let cancelled = false; let cancelled = false;
setDetailLoading(true); setDetailLoading(true);
const loadCertList = requestCertListRef.current; Promise.resolve(requestDetailsRef.current({ id: currentId }))
Promise.all([ .catch((err) => {
Promise.resolve(requestDetailsRef.current({ id: currentId })).catch( console.warn("[StaffViewModal] load detail failed:", err);
(err) => { return { data: {} };
console.warn("[StaffViewModal] load detail failed:", err); })
return { data: {} }; .then((detailRes) => {
},
),
typeof loadCertList === "function"
? Promise.resolve(loadCertList(currentId)).catch((err) => {
console.warn("[StaffViewModal] load certificates failed:", err);
return [];
})
: Promise.resolve([]),
])
.then(([detailRes, certList]) => {
if (!cancelled) { if (!cancelled) {
setInfo(detailRes?.data || {}); setInfo(detailRes?.data || {});
setCertificates(Array.isArray(certList) ? certList : []);
} }
}) })
.finally(() => { .finally(() => {
@ -79,51 +66,35 @@ export default function StaffViewModal({
}; };
}, [open, currentId]); }, [open, currentId]);
const openCertPreview = async (record) => { const proofMaterialFiles = parseFileList(info.proofMaterialUrl);
setCertPreview(record);
setCertPreviewInfo(null);
if (typeof requestCertDetailsRef.current !== "function") {
setCertPreviewInfo(record);
return;
}
setCertPreviewLoading(true);
try {
const res = await requestCertDetailsRef.current({ id: record.id });
setCertPreviewInfo(res?.data || record);
} catch (err) {
console.warn("[StaffViewModal] load cert detail failed:", err);
setCertPreviewInfo(record);
} finally {
setCertPreviewLoading(false);
}
};
const proofMaterialUrl = info.proofMaterialUrl const titleInfo = Array.isArray(info.titleCodeArr)
? JSON.parse(info.proofMaterialUrl) ? info.titleCodeArr.map((item) => ({
titleName: TITLE_LEVEL_MAP[item.titleCode] || item.titleCode || "-",
proveUrlList: parseFileList(item.proveUrl),
}))
: []; : [];
const titleCodeArr = Array.isArray(info.titleCodeArr) const regCert = info.registeredSafetyEngineerCert;
? info.titleCodeArr.map((item) => { const regCertLevelMap = CERT_LEVEL_LABEL_BY_TYPE.registered_safety_engineer;
let proveUrlList = []; const regCertFiles = parseFileList(regCert?.certAttachmentUrl);
if (item.proveUrl) {
try { const evaluatorCert = info.evaluatorCert;
proveUrlList = const evaluatorFlag = info.evaluatorFlag || (evaluatorCert ? 1 : 2);
typeof item.proveUrl === "string" const evalCertLevelMap = CERT_LEVEL_LABEL_BY_TYPE.evaluator;
? JSON.parse(item.proveUrl) const evalCertFiles = parseFileList(evaluatorCert?.certAttachmentUrl);
: item.proveUrl;
} catch { const capabilityList = Array.isArray(info.capabilityAssessment)
proveUrlList = []; ? info.capabilityAssessment.map((item) => ({
} capabilityName:
} CAPABILITY_MAP[item.professionalCapabilityCode] ||
return { item.professionalCapabilityCode ||
industryName: "-",
QUALIFICATION_INDUSTRY_OPTIONS_MAP[item.industryCode] || sourceName:
item.industryCode || ABILITY_SOURCE_MAP[item.professionalCapabilitySourceCode] ||
"-", item.professionalCapabilitySourceCode ||
titleName: TITLE_LEVEL_MAP[item.titleCode] || item.titleCode || "-", "-",
proveUrlList, }))
};
})
: []; : [];
const renderFileList = (files) => { const renderFileList = (files) => {
@ -150,258 +121,136 @@ export default function StaffViewModal({
}); });
}; };
const certPreviewFiles = certPreviewInfo?.certImgs?.length
? certPreviewInfo.certImgs
: certPreviewInfo?.certImgFiles || [];
const certPreviewUrl = resolvePreviewSrc(certPreviewFiles[0]);
const certPreviewName =
certPreviewFiles[0]?.fileName ||
certPreviewFiles[0]?.name ||
certPreviewInfo?.certName;
return ( return (
<> <Modal
<Modal open={open}
open={open} destroyOnHidden
destroyOnHidden title="查看"
title="查看" width={900}
width={900} loading={detailLoading}
loading={detailLoading} cancelText="关闭"
cancelText="关闭" okButtonProps={{ style: { display: "none" } }}
okButtonProps={{ style: { display: "none" } }} onCancel={onCancel}
onCancel={onCancel} >
> <Descriptions bordered column={2} labelStyle={{ width: 120 }}>
<Descriptions bordered column={2} labelStyle={{ width: 120 }}> <Descriptions.Item label="姓名">{info.userName}</Descriptions.Item>
<Descriptions.Item label="姓名">{info.userName}</Descriptions.Item> <Descriptions.Item label="性别">
<Descriptions.Item label="性别"> {GENDER_MAP[info.genderCode]}
{GENDER_MAP[info.genderCode]} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="出生日期">
<Descriptions.Item label="出生日期"> {info.birthDate}
{info.birthDate} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="参加工作时间">
<Descriptions.Item label="参加工作时间"> {info.joinWorkDate}
{info.joinWorkDate} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="账号">{info.account}</Descriptions.Item>
<Descriptions.Item label="账号">{info.account}</Descriptions.Item> <Descriptions.Item label="部门">{info.deptName}</Descriptions.Item>
<Descriptions.Item label="部门">{info.deptName}</Descriptions.Item> <Descriptions.Item label="岗位">{info.postName}</Descriptions.Item>
<Descriptions.Item label="岗位">{info.postName}</Descriptions.Item> <Descriptions.Item label="身份证号">
<Descriptions.Item label="身份证号"> {info.idCardNo}
{info.idCardNo} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="现住地址">
<Descriptions.Item label="现住地址" > {info.currentAddress}
{info.currentAddress} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="办公地址">
<Descriptions.Item label="办公地址" > {info.officeAddress}
{info.officeAddress} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="学历类型">
<Descriptions.Item label="学历类型"> {info.educationTypeName || "-"}
{info.educationTypeName || "-"} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="毕业院校">
<Descriptions.Item label="毕业院校"> {info.graduateSchool}
{info.graduateSchool} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="学历层次">
<Descriptions.Item label="学历层次"> {info.educationLevelName || "-"}
{info.educationLevelName || "-"} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="基础学科专业">
<Descriptions.Item label="基础学科专业"> {info.basicDisciplineMajorName || "-"}
{info.basicDisciplineMajorName || "-"} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="人员岗位类型">
{PERSONNEL_POST_TYPE_MAP[info.personnelPositionType] || "-"}
</Descriptions.Item>
<Descriptions.Item label="主要学习工作经历" span={2}>
{info.workExperience || "-"}
</Descriptions.Item>
<Descriptions.Item label="学历证明材料" span={2}>
{renderFileList(proofMaterialFiles)}
</Descriptions.Item>
</Descriptions>
<Descriptions.Item label="是否注册安全工程师"> <div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
{REGISTER_ENGINEER_MAP[info.registerEngineerFlag] ?? "-"} 注安信息
</Descriptions.Item> </div>
<Descriptions.Item label="是否技术负责人"> <Descriptions bordered column={2} labelStyle={{ width: 120 }}>
{info.technicalDirectorFlag === true <Descriptions.Item label="是否注册安全工程师">
? "是" {REGISTER_ENGINEER_MAP[info.registerEngineerFlag] ?? "-"}
: info.technicalDirectorFlag === false </Descriptions.Item>
? "否" <Descriptions.Item label="注安专业类别">
: "-"} {REGISTER_ENGINEER_CATEGORY_MAP[regCert?.certCategoryCode] || "-"}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="是否过程控制负责人"> <Descriptions.Item label="注安等级">
{info.processControlLeaderFlag === true {regCertLevelMap[regCert?.certLevel] || "-"}
? "是" </Descriptions.Item>
: info.processControlLeaderFlag === false <Descriptions.Item label="证书编号">
? "否" {regCert?.certNo || "-"}
: "-"} </Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="注安证书" span={2}>
{renderFileList(regCertFiles)}
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
评价师信息
</div>
<Descriptions bordered column={2} labelStyle={{ width: 120 }}>
<Descriptions.Item label="是否评价师">
{EVALUATOR_MAP[evaluatorFlag] ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="评价师等级">
{evalCertLevelMap[evaluatorCert?.certLevel] || "-"}
</Descriptions.Item>
<Descriptions.Item label="证书编号">
{evaluatorCert?.certNo || "-"}
</Descriptions.Item>
<Descriptions.Item label="评价师证书" span={2}>
{renderFileList(evalCertFiles)}
</Descriptions.Item>
</Descriptions>
<Descriptions.Item <div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
label="出版学术专著、专利、获奖、发表学术论文等" 职称信息
span={2} </div>
> <Table
{info.publications || "-"} size="small"
</Descriptions.Item> bordered
<Descriptions.Item label="自我申报的专业能力及认定方式" span={2}> pagination={false}
{info.abilityDeclaration || "-"} rowKey={(_, idx) => idx}
</Descriptions.Item> dataSource={titleInfo}
<Descriptions.Item label="主要学习工作经历" span={2}> columns={[
{info.workExperience || "-"} { title: "职称", dataIndex: "titleName", width: 120 },
</Descriptions.Item> {
<Descriptions.Item label="学历证明材料" span={2}> title: "证明材料",
{proofMaterialUrl.length dataIndex: "proveUrlList",
? proofMaterialUrl.map((item) => { render: renderFileList,
const isImage = },
/\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(item.url); ]}
return (
<div key={item.id}>
{isImage ? (
<Image
src={item.url}
style={{ maxWidth: 200, cursor: "pointer" }}
preview={{
mask: item.fileName || item.name || "预览",
}}
/>
) : (
<Button
type="link"
onClick={() => window.open(item.url)}
>
{item.fileName || item.name || item.url}
</Button>
)}
</div>
);
})
: "-"}
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
职称信息
</div>
<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,
},
]}
/>
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
证照信息
</div>
<Table
size="small"
bordered
rowKey="id"
pagination={false}
dataSource={certificates}
locale={{ emptyText: "暂无证照信息" }}
columns={[
{ title: "证照名称", dataIndex: "certName", ellipsis: true },
{ title: "证书编号", dataIndex: "certNo", ellipsis: true },
{ title: "证书类别", dataIndex: "certTypeName", ellipsis: true },
{ title: "发证机关", dataIndex: "issueOrg", ellipsis: true },
{ title: "有效开始日期", dataIndex: "validStartDate", width: 120 },
{ title: "有效结束日期", dataIndex: "validEndDate", width: 120 },
{
title: "操作",
width: 90,
render: (_, record) => (
<Button
type="link"
size="small"
onClick={() => openCertPreview(record)}
>
查看证书
</Button>
),
},
]}
/>
</Modal>
<FilePreviewModal
open={!!attachmentPreview}
title="附件预览"
fileName={attachmentPreview?.fileName}
url={attachmentPreview?.url}
onCancel={() => setAttachmentPreview(null)}
/> />
<Modal <div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
open={!!certPreview} 专业能力信息
destroyOnHidden </div>
title="证书预览" <Table
width={720} size="small"
loading={certPreviewLoading} bordered
cancelText="关闭" pagination={false}
okText={certPreviewUrl ? "新窗口打开" : "关闭"} rowKey={(_, idx) => idx}
okButtonProps={{ dataSource={capabilityList}
style: certPreviewUrl ? undefined : { display: "none" }, locale={{ emptyText: "暂无专业能力信息" }}
}} columns={[
onCancel={() => { { title: "专业能力", dataIndex: "capabilityName" },
setCertPreview(null); { title: "来源", dataIndex: "sourceName", width: 280 },
setCertPreviewInfo(null); ]}
}} />
onOk={() => { </Modal>
if (certPreviewUrl) {
window.open(certPreviewUrl, "_blank");
}
setCertPreview(null);
setCertPreviewInfo(null);
}}
>
{certPreviewInfo && (
<div>
<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="证书类别">
{certPreviewInfo.certTypeName || "-"}
</Descriptions.Item>
<Descriptions.Item label="发证机关">
{certPreviewInfo.issueOrg || "-"}
</Descriptions.Item>
<Descriptions.Item label="有效开始日期">
{certPreviewInfo.validStartDate || "-"}
</Descriptions.Item>
<Descriptions.Item label="有效结束日期">
{certPreviewInfo.validEndDate || "-"}
</Descriptions.Item>
<Descriptions.Item label="查看证书">
{certPreviewInfo.certAttachmentUrl &&
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>
);
})}
</Descriptions.Item>
</Descriptions>
</div>
)}
</Modal>
</>
); );
} }

View File

@ -162,6 +162,50 @@ export const REGISTER_ENGINEER_OPTIONS = [
{ label: "否", value: 2 }, { label: "否", value: 2 },
]; ];
/** 注安专业类别 */
export const REGISTER_ENGINEER_CATEGORY_OPTIONS = [
{ label: "煤矿安全", value: "COAL_MINE" },
{ label: "金属非金属矿山安全", value: "METAL_NONMETAL_MINE" },
{ label: "化工安全", value: "CHEMICAL" },
{ label: "金属冶炼安全", value: "METAL_SMELTING" },
{ label: "建筑施工安全", value: "CONSTRUCTION" },
{ label: "道路运输安全", value: "ROAD_TRANSPORT" },
{ label: "其他安全", value: "OTHER" },
];
export const REGISTER_ENGINEER_CATEGORY_MAP =
REGISTER_ENGINEER_CATEGORY_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);
/** 人员岗位类型 */
export const PERSONNEL_POST_TYPE_OPTIONS = [
{ label: "技术负责人", value: "TECHNICAL_LEADER" },
{ label: "过程控制负责人", value: "PROCESS_CONTROL_LEADER" },
];
export const PERSONNEL_POST_TYPE_MAP = PERSONNEL_POST_TYPE_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);
/** 能力来源 */
export const ABILITY_SOURCE_OPTIONS = [
{ label: "注册安全工程师", value: "REGISTERED_SAFETY_ENGINEER" },
{ label: "学历", value: "EDUCATION" },
{ label: "职称", value: "PROFESSIONAL_TITLE" },
{ label: "出版学术专著、专利、获奖、发表学术论文等", value: "ACADEMIC_ACHIEVEMENT" },
];
export const ABILITY_SOURCE_MAP = ABILITY_SOURCE_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);
/** 是否评价师 */
export const EVALUATOR_OPTIONS = [
{ label: "是", value: 1 },
{ label: "否", value: 2 },
];
/** 职称等级 */ /** 职称等级 */
export const TITLE_LEVEL_OPTIONS = [ export const TITLE_LEVEL_OPTIONS = [
{ value: "SENIOR", label: "高级" }, { value: "SENIOR", label: "高级" },

View File

@ -14,6 +14,7 @@ import {
Table, Table,
TreeSelect, TreeSelect,
Upload, Upload,
Divider,
} from "antd"; } from "antd";
import { InboxOutlined } from "@ant-design/icons"; import { InboxOutlined } from "@ant-design/icons";
import { Get } from "@cqsjjb/jjb-common-lib/http"; import { Get } from "@cqsjjb/jjb-common-lib/http";
@ -29,14 +30,19 @@ import { NS_STAFF_INFO, NS_ORG_DEPARTMENT } from "~/enumerate/namespace";
import { import {
EDUCATION_LEVEL_OPTIONS, EDUCATION_LEVEL_OPTIONS,
EDUCATION_TYPE_OPTIONS, EDUCATION_TYPE_OPTIONS,
QUALIFICATION_INDUSTRY_OPTIONS,
REGISTER_ENGINEER_OPTIONS, REGISTER_ENGINEER_OPTIONS,
REGISTER_ENGINEER_CATEGORY_OPTIONS,
PERSONNEL_POST_TYPE_OPTIONS,
ABILITY_SOURCE_OPTIONS,
EVALUATOR_OPTIONS,
TITLE_LEVEL_OPTIONS, TITLE_LEVEL_OPTIONS,
} from "~/enumerate/enterpriseOptions"; } from "~/enumerate/enterpriseOptions";
import { CERT_LEVEL_OPTIONS_BY_TYPE, CAPABILITY_OPTIONS } from "~/enumerate/constant";
import { getBirthDateFromIdCard } from "~/utils"; import { getBirthDateFromIdCard } from "~/utils";
import { idCardRule, mobileRule } from "~/utils/validators"; import { idCardRule, mobileRule } from "~/utils/validators";
import AttachmentUpload from "~/components/AttachmentUpload"; import AttachmentUpload from "~/components/AttachmentUpload";
import StaffViewModal from "~/components/StaffViewModal"; import StaffViewModal from "~/components/StaffViewModal";
import "./index.less";
const { router } = tools; const { router } = tools;
const API_HOST = window.process?.env?.app?.API_HOST || ""; const API_HOST = window.process?.env?.app?.API_HOST || "";
@ -213,12 +219,12 @@ function PersonnelInfoPage(props) {
ellipsis: true, ellipsis: true,
render: (text) => text || "-", render: (text) => text || "-",
}, },
{ {
title: "能力", title: "能力",
width: 260, width: 260,
dataIndex: "abilityNames", dataIndex: "abilityNames",
ellipsis: true, ellipsis: true,
render: (text) => text || '暂无', render: (text) => text || "暂无",
}, },
{ {
title: "操作", title: "操作",
@ -245,16 +251,7 @@ function PersonnelInfoPage(props) {
> >
编辑 编辑
</Button> </Button>
<Button
type="link"
size="small"
onClick={() => {
setCurrentId(record.id);
goCertificate(record.id, record.userName);
}}
>
证书能力
</Button>
<Button <Button
type="link" type="link"
size="small" size="small"
@ -361,6 +358,11 @@ function StaffFormModal({
const [deptPositionOptions, setDeptPositionOptions] = useState([]); const [deptPositionOptions, setDeptPositionOptions] = useState([]);
const [positionLoading, setPositionLoading] = useState(false); const [positionLoading, setPositionLoading] = useState(false);
const watchedDeptId = Form.useWatch("deptId", form); const watchedDeptId = Form.useWatch("deptId", form);
const watchedRegisterEngineerFlag = Form.useWatch(
"registerEngineerFlag",
form,
);
const watchedEvaluatorFlag = Form.useWatch("evaluatorFlag", form);
const wasOpenRef = useRef(false); const wasOpenRef = useRef(false);
const inflightDeptRef = useRef(null); const inflightDeptRef = useRef(null);
@ -396,8 +398,7 @@ function StaffFormModal({
}; };
const loadBasicDisciplineMajors = async () => { const loadBasicDisciplineMajors = async () => {
await queryBasicDisciplineMajor({ pageSize: 999 });
await queryBasicDisciplineMajor({ pageSize: 999 });
}; };
useEffect(() => { useEffect(() => {
@ -438,21 +439,37 @@ function StaffFormModal({
setFields.proofMaterialUrl = JSON.parse(setFields.proofMaterialUrl); setFields.proofMaterialUrl = JSON.parse(setFields.proofMaterialUrl);
} }
if (Array.isArray(setFields.titleCodeArr)) { const parseCertAttachment = (cert) => {
setFields.titleCodeArr = setFields.titleCodeArr.map((item) => { if (cert?.certAttachmentUrl) {
if (item.proveUrl && typeof item.proveUrl === "string") { try {
try { cert.certAttachmentUrl = JSON.parse(cert.certAttachmentUrl);
return { ...item, proveUrl: JSON.parse(item.proveUrl) }; } catch {}
} catch { }
return item; };
}
if (setFields.registeredSafetyEngineerCert) {
parseCertAttachment(setFields.registeredSafetyEngineerCert);
}
if (setFields.evaluatorCert) {
setFields.evaluatorFlag = 1;
parseCertAttachment(setFields.evaluatorCert);
}
if (Array.isArray(setFields.titleCodeArr) && setFields.titleCodeArr[0]) {
const first = setFields.titleCodeArr[0];
setFields.titleCode = first.titleCode;
if (first.proveUrl) {
try {
setFields.proveUrl = JSON.parse(first.proveUrl);
} catch {
setFields.proveUrl = first.proveUrl;
} }
return item; }
}); delete setFields.titleCodeArr;
} }
form.setFieldsValue(setFields); form.setFieldsValue(setFields);
}) })
.finally(() => { .finally(() => {
if (!cancelled) setDetailLoading(false); if (!cancelled) setDetailLoading(false);
@ -488,6 +505,15 @@ function StaffFormModal({
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
try { try {
setSubmitting(true); setSubmitting(true);
const stringifyCert = (cert) =>
cert
? {
...cert,
certAttachmentUrl: Array.isArray(cert.certAttachmentUrl)
? JSON.stringify(cert.certAttachmentUrl)
: undefined,
}
: undefined;
const payload = { const payload = {
...values, ...values,
genderName: genderName:
@ -499,13 +525,28 @@ function StaffFormModal({
proofMaterialUrl: Array.isArray(values.proofMaterialUrl) proofMaterialUrl: Array.isArray(values.proofMaterialUrl)
? JSON.stringify(values.proofMaterialUrl) ? JSON.stringify(values.proofMaterialUrl)
: undefined, : undefined,
titleCodeArr: Array.isArray(values.titleCodeArr) titleCodeArr: [
? values.titleCodeArr.map((item) => ({ {
...item, titleCode: values.titleCode,
proveUrl: Array.isArray(item.proveUrl) proveUrl: Array.isArray(values.proveUrl)
? JSON.stringify(item.proveUrl) ? JSON.stringify(values.proveUrl)
: item.proveUrl, : undefined,
})) },
],
registeredSafetyEngineerCert:
values.registerEngineerFlag === 1
? stringifyCert(values.registeredSafetyEngineerCert)
: undefined,
evaluatorCert:
values.evaluatorFlag === 1
? stringifyCert(values.evaluatorCert)
: undefined,
capabilityAssessment: Array.isArray(values.capabilityAssessment)
? values.capabilityAssessment.filter(
(item) =>
item?.professionalCapabilityCode ||
item?.professionalCapabilitySourceCode,
)
: undefined, : undefined,
}; };
if (payload.birthDate && typeof payload.birthDate !== "string") { if (payload.birthDate && typeof payload.birthDate !== "string") {
@ -515,7 +556,6 @@ function StaffFormModal({
payload.joinWorkDate = payload.joinWorkDate.format("YYYY-MM-DD"); payload.joinWorkDate = payload.joinWorkDate.format("YYYY-MM-DD");
} }
if (currentId) payload.id = currentId; if (currentId) payload.id = currentId;
delete payload.proofMaterials;
const request = currentId ? staffInfoEdit : staffInfoAdd; const request = currentId ? staffInfoEdit : staffInfoAdd;
const res = await request(payload); const res = await request(payload);
@ -540,14 +580,6 @@ function StaffFormModal({
confirmLoading={submitting} confirmLoading={submitting}
onCancel={handleCancel} onCancel={handleCancel}
onOk={() => { onOk={() => {
const titleArr = form.getFieldValue("titleCodeArr") || [];
const hasTitleInfo = titleArr.some(
(item) => item?.industryCode && item?.titleCode,
);
if (!hasTitleInfo) {
message.warning("请添加职称信息");
return;
}
form.submit(); form.submit();
}} }}
> >
@ -586,7 +618,11 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="birthDate" label="出生日期" rules={[{ required: true, message: "请选择出生日期" }]}> <Form.Item
name="birthDate"
label="出生日期"
rules={[{ required: true, message: "请选择出生日期" }]}
>
<DatePicker style={{ width: "100%" }} /> <DatePicker style={{ width: "100%" }} />
</Form.Item> </Form.Item>
</Col> </Col>
@ -745,52 +781,190 @@ function StaffFormModal({
maxCount={5} maxCount={5}
/> />
</Col> </Col>
<Col span={12}>
<Form.Item name="personnelPositionType" label="人员岗位类型">
<Select
placeholder="请选择人员岗位类型"
allowClear
options={PERSONNEL_POST_TYPE_OPTIONS}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<div className="register-engineer-section">
<div className="register-engineer-section-title">注安信息</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="registerEngineerFlag"
label="是否注册安全工程师"
initialValue={2}
>
<Select
placeholder="请选择"
options={REGISTER_ENGINEER_OPTIONS}
/>
</Form.Item>
</Col>
{watchedRegisterEngineerFlag === 1 && (
<>
<Col span={12}>
<Form.Item
name={["registeredSafetyEngineerCert", "certCategoryCode"]}
label="注安专业类别"
rules={[{ required: true, message: "请选择注安专业类别" }]}
>
<Select
placeholder="请选择注安专业类别"
allowClear
options={REGISTER_ENGINEER_CATEGORY_OPTIONS}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={["registeredSafetyEngineerCert", "certLevel"]}
label="注安等级"
rules={[{ required: true, message: "请选择注安等级" }]}
>
<Select
placeholder="请选择注安等级"
allowClear
options={TITLE_LEVEL_OPTIONS}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={["registeredSafetyEngineerCert", "certNo"]}
label="证书编号"
rules={[
{ required: true, message: "请输入证书编号" },
{ max: 50, message: "证书编号不能超过50个字符" },
]}
>
<Input placeholder="请输入证书编号" maxLength={50} />
</Form.Item>
</Col>
<Col span={24}>
<AttachmentUpload
name={["registeredSafetyEngineerCert", "certAttachmentUrl"]}
label="注安证书"
extra="最多上传10个pdf,doc,docx,图片格式文件"
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.bmp,.webp"
maxCount={10}
rules={[{ required: true, message: "请上传注安证书" }]}
/>
</Col>
</>
)}
</Row>
</div>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<div className="register-engineer-section">
<div className="register-engineer-section-title">
评价师信息
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="evaluatorFlag"
label="是否评价师"
initialValue={2}
>
<Select
placeholder="请选择"
options={EVALUATOR_OPTIONS}
/>
</Form.Item>
</Col>
{watchedEvaluatorFlag === 1 && (
<>
<Col span={12}>
<Form.Item
name={["evaluatorCert", "certLevel"]}
label="评价师等级"
rules={[{ required: true, message: "请选择评价师等级" }]}
>
<Select
placeholder="请选择评价师等级"
allowClear
options={CERT_LEVEL_OPTIONS_BY_TYPE.evaluator}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={["evaluatorCert", "certNo"]}
label="证书编号"
rules={[
{ required: true, message: "请输入证书编号" },
{ max: 50, message: "证书编号不能超过50个字符" },
]}
>
<Input placeholder="请输入证书编号" maxLength={50} />
</Form.Item>
</Col>
<Col span={24}>
<AttachmentUpload
name={["evaluatorCert", "certAttachmentUrl"]}
label="评价师证书"
extra="最多上传10个pdf,doc,docx,图片格式文件"
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.bmp,.webp"
maxCount={10}
rules={[{ required: true, message: "请上传评价师证书" }]}
/>
</Col>
</>
)}
</Row>
</div>
</Col>
</Row>
<Col span={12}> <Row gutter={16}>
<Form.Item <Col span={24}>
name="registerEngineerFlag" <div className="register-engineer-section">
label="是否注册安全工程师" <div className="register-engineer-section-title">
initialValue={2} 职称信息
> </div>
<Select <Row gutter={16}>
placeholder="请选择" <Col span={12}>
options={REGISTER_ENGINEER_OPTIONS} <Form.Item
/> name="titleCode"
</Form.Item> label="职称"
</Col> rules={[{ required: true, message: "请选择职称" }]}
<Col span={12}> >
<Form.Item name="technicalDirectorFlag" label="是否技术负责人"> <Select
<Select placeholder="请选择职称"
placeholder="请选择" allowClear
allowClear options={TITLE_LEVEL_OPTIONS}
options={[ />
{ label: "是", value: true }, </Form.Item>
{ label: "否", value: false }, </Col>
]} <Col span={12}>
/> <AttachmentUpload
</Form.Item> name="proveUrl"
</Col> label="证明材料"
<Col span={12}> extra="最多上传1个pdf,doc,docx,图片格式文件"
<Form.Item accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.bmp,.webp"
name="processControlLeaderFlag" maxCount={1}
label="是否过程控制负责人" rules={[{ required: true, message: "请上传证明材料" }]}
> />
<Select </Col>
placeholder="请选择" </Row>
allowClear </div>
options={[
{ label: "是", value: true },
{ label: "否", value: false },
]}
/>
</Form.Item>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Form.Item label="职称信息" required> <Form.Item label="专业能力信息">
<Form.List <Form.List
name="titleCodeArr" name="capabilityAssessment"
initialValue={[{}]} initialValue={[{}]}
rules={[{ required: true, message: "请添加职称信息" }]}
> >
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
@ -800,60 +974,38 @@ function StaffFormModal({
pagination={false} pagination={false}
size="small" size="small"
bordered bordered
locale={{ emptyText: "暂无职称信息,点击下方按钮添加" }}
columns={[ columns={[
{ {
title: "行业", title: "专业能力",
width: 200,
render: (_, field) => ( render: (_, field) => (
<Form.Item <Form.Item
name={[field.name, "industryCode"]} name={[field.name, "professionalCapabilityCode"]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
rules={[
{ required: true, message: "请选择行业" },
]}
> >
<Select <Select
placeholder="请选择行业" placeholder="请选择专业能力"
allowClear allowClear
options={QUALIFICATION_INDUSTRY_OPTIONS} options={CAPABILITY_OPTIONS}
/> />
</Form.Item> </Form.Item>
), ),
}, },
{ {
title: "职称", title: "来源",
width: 120, width: 280,
render: (_, field) => ( render: (_, field) => (
<Form.Item <Form.Item
name={[field.name, "titleCode"]} name={[field.name, "professionalCapabilitySourceCode"]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
rules={[
{ required: true, message: "请选择职称" },
]}
> >
<Select <Select
placeholder="请选择职称" placeholder="请选择来源"
allowClear allowClear
options={TITLE_LEVEL_OPTIONS} options={ABILITY_SOURCE_OPTIONS}
/> />
</Form.Item> </Form.Item>
), ),
}, },
{
title: "证明材料",
render: (_, field) => (
<AttachmentUpload
name={[field.name, "proveUrl"]}
rules={[
{ required: true, message: "请上传证明材料" },
]}
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.bmp,.webp"
maxCount={1}
style={{ marginBottom: 0 }}
/>
),
},
{ {
title: "操作", title: "操作",
width: 80, width: 80,
@ -871,47 +1023,21 @@ function StaffFormModal({
}, },
]} ]}
/> />
<Button {fields.length < 2 && (
type="dashed" <Button
onClick={() => add()} type="dashed"
block onClick={() => add()}
style={{ marginTop: 12 }} block
> style={{ marginTop: 12 }}
添加职称信息 >
</Button> 添加专业能力信息
</Button>
)}
</> </>
)} )}
</Form.List> </Form.List>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={24}>
<Form.Item
name="publications"
label="出版学术专著、专利、获奖、发表学术论文等"
rules={[{ max: 500, message: "不能超过1000个字符" }]}
>
<Input.TextArea
rows={2}
placeholder="请输入"
maxLength={500}
showCount
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="abilityDeclaration"
label="自我申报的专业能力及认定方式"
rules={[{ max: 500, message: "不能超过1000个字符" }]}
>
<Input.TextArea
rows={2}
placeholder="请输入"
maxLength={500}
showCount
/>
</Form.Item>
</Col>
<Col span={24}> <Col span={24}>
<Form.Item name="workExperience" label="主要学习工作经历"> <Form.Item name="workExperience" label="主要学习工作经历">
<Input.TextArea <Input.TextArea
@ -1073,7 +1199,12 @@ function StaffImportModal({ open, onCancel, onSuccess }) {
); );
} }
function ResetPasswordModal({ open, currentId, staffInfoResetPassword, onCancel }) { function ResetPasswordModal({
open,
currentId,
staffInfoResetPassword,
onCancel,
}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);

View File

@ -0,0 +1,13 @@
.register-engineer-section {
border: 1px dashed #d9d9d9;
border-radius: 8px;
padding: 16px 16px 0;
margin-bottom: 24px;
&-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 16px;
}
}