dev-tmp1
tangjie 2026-08-28 16:07:33 +08:00
parent de95e1620e
commit 9f4fc71d90
8 changed files with 177 additions and 85 deletions

View File

@ -12,9 +12,9 @@ module.exports = {
// 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095 // 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095
//API_HOST: "http://localhost:80", //API_HOST: "http://localhost:80",
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: "https://gbs-gateway.qhdsafety.com", API_HOST: "https://gbs-gateway.qhdsafety.com",
// API_HOST: "http://192.168.0.103", //huwei // API_HOST: "http://192.168.0.103", //huwei
}, },
production: { production: {

View File

@ -0,0 +1,58 @@
安全评价检测检验机构从业告知书
机构名称
{account}
机构资质证书编号
{qualificationCertNo}
机构信息
公开网址
{infoDisclosureUrl}
办公地址
{businessAddress}
邮政
编码
法定代表人
{legalRepresentative}
联系人
{contactName}
联系
电话
{contactPhone}
项目名称
{projectName}
项目地址
{projectAddress}
项目所属行业
{industryText}
项目组组长
{projectLeader}
联系
电话
技术服务期限
计划现场勘验
(检测检验)时间
项目组成员、专业及工作任务(安全评价机构填写)
姓 名
专 业
工作任务
{#members}{name}
{major}
{task}{/members}
现场检测检验人员(安全生产检测检验机构填写)
姓 名
检测检验项目
我单位承接了 □安全评价/□安全生产检测检验项目,拟于近期开展技术服务活动,现按照规定将有关信息告知如下。
机构(盖章)
年 月

View File

@ -179,6 +179,16 @@ export const evalProjectMemberPage = declareRequest(
"Get > /safetyEval/institution/eval-project-member/page", "Get > /safetyEval/institution/eval-project-member/page",
); );
export const evalProjectEstablishedAttachSave = declareRequest(
"projectEstablishedAttachSaveLoading",
"Post > /safetyEval/institution/eval-project-member/saveProjectEstablishedAttach",
);
export const evalProjectEstablishedAttachGet = declareRequest(
"projectEstablishedAttachGetLoading",
"Get > /safetyEval/institution/eval-project-member/getProjectEstablishedAttach",
);
export const evalProjectMemberSave = declareRequest( export const evalProjectMemberSave = declareRequest(
"memberSaveLoading", "memberSaveLoading",
"Post > @/safetyEval/institution/eval-project-member/save", "Post > @/safetyEval/institution/eval-project-member/save",

View File

@ -1,6 +1,6 @@
import { Form, Image, Upload, message } from "antd"; import { Button, Form, Image, Upload, message } from "antd";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined, UploadOutlined } from "@ant-design/icons";
/** 批量上传接口:一次请求上传 1~N 个文件,表单字段名为 files */ /** 批量上传接口:一次请求上传 1~N 个文件,表单字段名为 files */
const BATCH_UPLOAD_ACTION = `${window.process.env.app.API_HOST}/safetyEval/file/upload/batch`; const BATCH_UPLOAD_ACTION = `${window.process.env.app.API_HOST}/safetyEval/file/upload/batch`;
@ -8,7 +8,23 @@ const BATCH_UPLOAD_ACTION = `${window.process.env.app.API_HOST}/safetyEval/file/
const isImage = (url) => const isImage = (url) =>
/\.(png|jpe?g|gif|bmp|webp|svg)(\?.*)?$/i.test(url || ""); /\.(png|jpe?g|gif|bmp|webp|svg)(\?.*)?$/i.test(url || "");
export default function AttachmentUpload({ name, label, disabled, maxCount, accept, extra ,rules, style, keepOriginFile }) { /**
* @param {string} [listType] - 上传列表样式默认 picture-card text 时为按钮触发形态
* @param {string} [uploadText] - 触发区文案默认上传附件
*/
export default function AttachmentUpload({
name,
label,
disabled,
maxCount,
accept,
extra,
rules,
style,
keepOriginFile,
listType = "picture-card",
uploadText = "上传附件",
}) {
const [previewImage, setPreviewImage] = useState(""); const [previewImage, setPreviewImage] = useState("");
// 同一次选择的文件先入队,等微任务结束后合并为一个批量请求 // 同一次选择的文件先入队,等微任务结束后合并为一个批量请求
const queueRef = useRef([]); const queueRef = useRef([]);
@ -104,7 +120,7 @@ export default function AttachmentUpload({ name, label, disabled, maxCount, acce
> >
<Upload <Upload
disabled={disabled} disabled={disabled}
listType="picture-card" listType={listType}
// maxCount 为 1 时仍单选,其余场景允许一次选择多个文件 // maxCount 为 1 时仍单选,其余场景允许一次选择多个文件
multiple={!maxCount || maxCount > 1} multiple={!maxCount || maxCount > 1}
customRequest={customRequest} customRequest={customRequest}
@ -136,10 +152,16 @@ export default function AttachmentUpload({ name, label, disabled, maxCount, acce
} }
}} }}
> >
<button style={{ border: 0, background: "none" }} type="button"> {listType === "text" ? (
<PlusOutlined /> <Button type="dashed" size="small" icon={<UploadOutlined />}>
<div style={{ marginTop: 8 }}>上传附件</div> {uploadText}
</button> </Button>
) : (
<button style={{ border: 0, background: "none" }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>{uploadText}</div>
</button>
)}
</Upload> </Upload>
</Form.Item> </Form.Item>
{previewImage && <Image {previewImage && <Image

View File

@ -143,6 +143,10 @@ const EvalProject = (props) => {
> >
删除 删除
</Button> </Button>
<Button type="link" size="small" onClick={() =>
props.history.push(`/container/SafetyEvalBusiness/ProjectFlow?id=${record.id}&readOnly=1`)}>
项目节点
</Button>
</TableAction> </TableAction>
), ),
}, },

View File

@ -235,44 +235,7 @@ const ControlContent = ({ readOnly, ...props }) => {
</div> </div>
<Image src={processControl?.reviewerSignatureUrl} width={120} height={80} /> <Image src={processControl?.reviewerSignatureUrl} width={120} height={80} />
</div>} </div>}
{!readOnly&&<Collapse
style={{ marginTop: 12 }}
items={[
{
key: "checklist",
label: "过程控制完整核查清单13项",
children: (
<ol
style={{
margin: 0,
paddingLeft: 20,
fontSize: 14,
color: "#475569",
lineHeight: 1.8,
}}
>
<li>合同签订前是否进行项目风险分析</li>
<li>是否签订技术服务合同</li>
<li>是否下达项目任务通知单</li>
<li>是否有项目组成立和项目负责人任命记录</li>
<li>是否编制项目实施计划</li>
<li>现场检查记录是否完整有效</li>
<li>是否有书面整改通知单并由被评价单位盖章或签字确认</li>
<li>
是否进行现场复查复查单是否双方签名或盖章并注明日期
</li>
<li>是否有现场勘查整改复查影像资料</li>
<li>是否完成内审技审并保留审核记录签字和日期</li>
<li>委托方提供资料是否齐全并由被评价单位盖章</li>
<li>
委托方资料评价过程资料和评价报告存档是否符合要求
</li>
<li>存档资料是否完整且具有可追溯性</li>
</ol>
),
},
]}
/>}
</Card> </Card>
)} )}

View File

@ -1,12 +1,14 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message, Popover, Image, Checkbox } from "antd"; import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message, Popover, Image, Checkbox } from "antd";
import { DownloadOutlined, QuestionOutlined, PictureOutlined } from "@ant-design/icons"; import { DownloadOutlined, QuestionOutlined, PictureOutlined } from "@ant-design/icons";
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace"; import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib"; import { tools } from "@cqsjjb/jjb-common-lib";
import AttachmentUpload from "~/components/AttachmentUpload"; import AttachmentUpload from "~/components/AttachmentUpload";
import { ROLE_DEFINITIONS_MAP } from '~/enumerate/constant' import { ROLE_DEFINITIONS_MAP, ROLE_DEFINITIONS } from '~/enumerate/constant'
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP } from "~/enumerate/enterpriseOptions";
import LayoutFooterPortal from "~/components/LayoutFooterPortal"; import LayoutFooterPortal from "~/components/LayoutFooterPortal";
import { fillDocxTemplate } from "~/utils/fillDocxTemplate";
import "./index.less"; import "./index.less";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -36,22 +38,9 @@ const attachRequiredRules = (label) => [
}, },
]; ];
const downloadTemplate = async () => {
try {
const res = await fetch("https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a6bff3ee4b021a75404adfb.docx");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "从业告知书模板.docx";
a.click();
URL.revokeObjectURL(url);
} catch {
message.error("下载失败,请重试");
}
};
const NoticePanel = ({ readOnly }) => (
const NoticePanel = ({ readOnly, downloadTemplate }) => (
<div> <div>
<div className="pf-section-header"> <div className="pf-section-header">
<div> <div>
@ -138,8 +127,8 @@ const InspectionPanel = ({ dataSource = [], evalProjectDetailData = {}, readOnly
{ title: "项目角色", dataIndex: "role", render: (v) => Array.isArray(v) ? v.map(item => ROLE_DEFINITIONS_MAP[item]).join('、') : v, ellipsis: true }, { title: "项目角色", dataIndex: "role", render: (v) => Array.isArray(v) ? v.map(item => ROLE_DEFINITIONS_MAP[item]).join('、') : v, ellipsis: true },
{ title: "人脸识别", dataIndex: "faceState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "通过" : "未通过"}</Tag> }, { title: "人脸识别", dataIndex: "faceState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "通过" : "未通过"}</Tag> },
{ title: "GPS定位", dataIndex: "gpsState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "正常" : "异常"}</Tag> }, { title: "GPS定位", dataIndex: "gpsState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "正常" : "异常"}</Tag> },
{ title: "签到时间", dataIndex: "singInTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 0);}}, { title: "签到时间", dataIndex: "singInTime", render: (time, record) => { return renderTimeWithFace(time, record, 0); } },
{ title: "签退时间", dataIndex: "signOutTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 1);}}, { title: "签退时间", dataIndex: "signOutTime", render: (time, record) => { return renderTimeWithFace(time, record, 1); } },
{ title: "在场时长", render: (_, r) => calcDuration(r.singInTime, r.signOutTime) }, { title: "在场时长", render: (_, r) => calcDuration(r.singInTime, r.signOutTime) },
{ {
title: "现场照片", dataIndex: "sceneUrl", render: (v) => { title: "现场照片", dataIndex: "sceneUrl", render: (v) => {
@ -256,7 +245,7 @@ const SurveyContent = ({ readOnly, ...props }) => {
const [activeStage, setActiveStage] = useState("PRACTICE_NOTIFICATION"); const [activeStage, setActiveStage] = useState("PRACTICE_NOTIFICATION");
const [attachMap, setAttachMap] = useState({}); const [attachMap, setAttachMap] = useState({});
const [surveyPersonnel, setSurveyPersonnel] = useState([]); const [surveyPersonnel, setSurveyPersonnel] = useState([]);
const { evalSurveyListLoading, evalSurveySaveAttachLoading, evalProjectDetailData } = props.safetyEvalBusiness; const { evalSurveySaveAttachLoading, evalProjectDetailData } = props.safetyEvalBusiness;
const projectId = router.query?.id; const projectId = router.query?.id;
const currentIdx = STAGES.findIndex((s) => s.key === activeStage); const currentIdx = STAGES.findIndex((s) => s.key === activeStage);
const isFirst = currentIdx === 0; const isFirst = currentIdx === 0;
@ -295,6 +284,57 @@ const SurveyContent = ({ readOnly, ...props }) => {
} }
}; };
const downloadTemplate = async () => {
const orgInfo = JSON.parse(sessionStorage.getItem('orgInfo'));
const industryText = String(evalProjectDetailData.industryCode || "")
.split(",")
.map((code) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[code])
.join("、");
// 项目组成员(同项目组 tab 页数据):姓名/专业/工作任务
const memberRes = await props.evalProjectMemberPage({ projectId });
const members = (memberRes?.data || []).map((item) => ({
name: item.personnelName,
major: item.capacity || "",
task:
item.responsibility ||
(item.role || [])
.map((r) => ROLE_DEFINITIONS.find((d) => d.value === r)?.summary)
.filter(Boolean)
.join("、"),
isLeader: (item.role || []).includes("PROJECT_LEAD"),
}));
// 项目组组长取自项目组成员中的 PROJECT_LEAD展示同 namesByRole如"张三(化工)"
const projectLeader = members
.filter((m) => m.isLeader)
.map((m) => `${m.name}${m.major ? `${m.major}` : ""}`)
.join("、");
const data = {
account: orgInfo.account,
qualificationCertNo: orgInfo.qualificationCertNo,
infoDisclosureUrl: orgInfo.infoDisclosureUrl,
businessAddress: orgInfo.businessAddress,
legalRepresentative: orgInfo.legalRepresentative,
contactName: orgInfo.contactName,
contactPhone: orgInfo.contactPhone,
projectName: evalProjectDetailData.projectName,
projectAddress: evalProjectDetailData.customerAddress || "",
industryText,
projectLeader,
members,
};
try {
await fillDocxTemplate(
'https://gbs-cqaqpj.oss-cn-beijing.aliyuncs.com/jjb/6a913235e4b0b3767a78622d.docx',
data,
`安全评价检测检验机构从业告知书.docx`,
{ download: true },
);
message.success("导出成功");
} catch {
message.error("导出失败,请重试");
}
};
useEffect(() => { useEffect(() => {
loadAttachments(); loadAttachments();
}, []); }, []);
@ -388,14 +428,14 @@ const SurveyContent = ({ readOnly, ...props }) => {
); );
return { return {
title: s.label, title: s.label,
...(uploaded ? { status: "finish" } : {status: "wait"}), ...(uploaded ? { status: "finish" } : { status: "wait" }),
}; };
})} })}
/> />
<Form form={form} layout="vertical" disabled={readOnly} scrollToFirstError> <Form form={form} layout="vertical" disabled={readOnly} scrollToFirstError>
<div style={{ display: activeStage === "PRACTICE_NOTIFICATION" ? "block" : "none" }}> <div style={{ display: activeStage === "PRACTICE_NOTIFICATION" ? "block" : "none" }}>
<NoticePanel readOnly={readOnly} /> <NoticePanel readOnly={readOnly} downloadTemplate={downloadTemplate} />
</div> </div>
<div style={{ display: activeStage === "SITE_INSPECTION_FORM" ? "block" : "none" }}> <div style={{ display: activeStage === "SITE_INSPECTION_FORM" ? "block" : "none" }}>
<InspectionPanel dataSource={surveyPersonnel} evalProjectDetailData={evalProjectDetailData} readOnly={readOnly} /> <InspectionPanel dataSource={surveyPersonnel} evalProjectDetailData={evalProjectDetailData} readOnly={readOnly} />

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
Tag, Tag,
Button, Button,
@ -7,6 +7,7 @@ import {
Modal, Modal,
Radio, Radio,
Checkbox, Checkbox,
Form,
message, message,
Spin, Spin,
Card, Card,
@ -18,6 +19,7 @@ import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib"; import { tools } from "@cqsjjb/jjb-common-lib";
import { ROLE_DEFINITIONS, EVAL_TYPE_MAP } from "~/enumerate/constant"; import { ROLE_DEFINITIONS, EVAL_TYPE_MAP } from "~/enumerate/constant";
import StatCardGroup from "~/components/StatCardGroup"; import StatCardGroup from "~/components/StatCardGroup";
import AttachmentUpload from "~/components/AttachmentUpload";
import LayoutFooterPortal from "~/components/LayoutFooterPortal"; import LayoutFooterPortal from "~/components/LayoutFooterPortal";
import { fillDocxTemplate } from "~/utils/fillDocxTemplate"; import { fillDocxTemplate } from "~/utils/fillDocxTemplate";
@ -33,6 +35,8 @@ const TeamContent = ({ readOnly, ...props }) => {
evalProjectMemberParticipantSave, evalProjectMemberParticipantSave,
evalProjectMemberParticipantList, evalProjectMemberParticipantList,
evalSignTaskSave, evalSignTaskSave,
evalProjectEstablishedAttachSave,
evalProjectEstablishedAttachGet,
} = props; } = props;
const { const {
evalProjectDetailData, evalProjectDetailData,
@ -49,6 +53,9 @@ const TeamContent = ({ readOnly, ...props }) => {
const [selectedRole, setSelectedRole] = useState(null); const [selectedRole, setSelectedRole] = useState(null);
const [selectedPersons, setSelectedPersons] = useState([]); const [selectedPersons, setSelectedPersons] = useState([]);
const [teamMembers, setTeamMembers] = useState([]); const [teamMembers, setTeamMembers] = useState([]);
// 人员任命书附件(项目成立附件 PROJECT_ESTABLISHED局部表单承载上传保存记录主键供更新
const [attachForm] = Form.useForm();
const establishedAttachIdRef = useRef(null);
// 参与风险分析人员签字 // 参与风险分析人员签字
const [participantModalOpen, setParticipantModalOpen] = useState(false); const [participantModalOpen, setParticipantModalOpen] = useState(false);
const [selectedParticipantIds, setSelectedParticipantIds] = useState([]); const [selectedParticipantIds, setSelectedParticipantIds] = useState([]);
@ -77,6 +84,39 @@ const TeamContent = ({ readOnly, ...props }) => {
if (signRes?.success !== false) { if (signRes?.success !== false) {
setSigners(signRes?.data || []); setSigners(signRes?.data || []);
} }
// 人员任命书附件回显
const attachRes = await evalProjectEstablishedAttachGet({ projectId });
if (attachRes?.data) {
establishedAttachIdRef.current = attachRes.data.id || null;
let files = [];
try {
files = JSON.parse(attachRes.data.fileUrl) || [];
} catch {}
attachForm.setFieldsValue({
appointmentFiles: Array.isArray(files) ? files : [],
});
}
};
// 人员任命书上传/删除后自动保存;接口不回传主键,保存后重查同步 id 供下次更新
const handleAttachChange = async (_, { appointmentFiles: files = [] }) => {
if (readOnly) return;
if (files.some((f) => f.status === "uploading")) return;
if (files.length && files.some((f) => !(f.status === "done" && f.url))) return;
if (!files.length && !establishedAttachIdRef.current) return;
const res = await evalProjectEstablishedAttachSave({
...(establishedAttachIdRef.current
? { id: establishedAttachIdRef.current }
: {}),
projectId,
attachTypeCode: "PROJECT_ESTABLISHED",
fileUrl: JSON.stringify(files),
});
if (res?.success !== false) {
message.success("人员任命书已保存");
const getRes = await evalProjectEstablishedAttachGet({ projectId });
if (getRes?.data?.id) establishedAttachIdRef.current = getRes.data.id;
}
}; };
useEffect(() => { useEffect(() => {
@ -520,20 +560,33 @@ const TeamContent = ({ readOnly, ...props }) => {
pagination={false} pagination={false}
scroll={{ x: 800 }} scroll={{ x: 800 }}
/> />
{!readOnly && ( <Flex justify="space-between" align="flex-start" style={{ marginTop: 8 }}>
<Button {!readOnly && (
type="dashed" <Button
size="small" type="dashed"
style={{ marginTop: 8 }} size="small"
disabled={signers.length >= 1} disabled={signers.length >= 1}
onClick={() => { onClick={() => {
setSelectedParticipantIds([]); setSelectedParticipantIds([]);
setParticipantModalOpen(true); setParticipantModalOpen(true);
}} }}
> >
添加参与人员 添加参与人员
</Button> </Button>
)} )}
<Form form={attachForm} disabled={readOnly} onValuesChange={handleAttachChange}>
<AttachmentUpload
name="appointmentFiles"
label="人员任命书"
rules={[{ required: true, message: "请上传人员任命书" }]}
maxCount={10}
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
listType="text"
uploadText="上传人员任命书"
extra="支持 PDF、Word、PNG、JPG 格式,最多上传 10 个文件,上传完成自动保存"
/>
</Form>
</Flex>
</Card> </Card>
{!readOnly &&<details className="pf-role-guide"> {!readOnly &&<details className="pf-role-guide">