dev-tmp1
tangjie 2026-08-12 17:12:19 +08:00
parent 81209bb537
commit c05a311bd6
6 changed files with 1317 additions and 1052 deletions

View File

@ -28,6 +28,8 @@
"@eigenpal/docx-editor-react": "^1.9.0",
"@rc-component/motion": "^1.0.0",
"@rc-component/util": "^1.11.1",
"pizzip": "latest",
"docxtemplater": "latest",
"antd": "^5.29.2",
"china-division": "^2.7.0",
"dayjs": "^1.11.7",

File diff suppressed because it is too large Load Diff

View File

@ -456,7 +456,7 @@ function OrgAccountPage(props) {
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="businessAddress" label="企事业单位经营地址">
<Form.Item name="businessAddress" label="经营地址">
<Input />
</Form.Item>
</Col>

View File

@ -13,12 +13,8 @@ import {
DatePicker,
Space,
ConfigProvider,
Card,
} from "antd";
import {
DownloadOutlined,
ExportOutlined,
} from "@ant-design/icons";
import { DownloadOutlined, ExportOutlined } from "@ant-design/icons";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
@ -27,6 +23,7 @@ import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { NS_REGULATOR_EVAL_REPORT, NS_ORG_INFO } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { tools } from "@cqsjjb/jjb-common-lib";
import { renderDocxBlob, fillDocxTemplate } from "~/utils/fillDocxTemplate";
import dayjs from "dayjs";
import "./index.less";
@ -36,6 +33,10 @@ const { RangePicker } = DatePicker;
const API_HOST = window.process?.env?.app?.API_HOST || "";
/** 监督检查单 docx 模板(线上) */
const CHECKLIST_TEMPLATE_URL =
"https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a7c36c2e4b0435c62b76a34.docx";
/** 原型 CSS--radius 8 / --radius-lg 12 */
const PROTO_THEME = {
token: { borderRadius: 8, borderRadiusLG: 12, borderRadiusSM: 6 },
@ -264,6 +265,7 @@ const EvalReportDatabase = (props) => {
const [spotCheckData, setSpotCheckData] = useState([]);
const [orgOptions, setOrgOptions] = useState([]);
const [spotCheckForm] = Form.useForm();
const [viewChecklistLoading, setViewChecklistLoading] = useState(false);
const {
regulatorEvalReportPageLoading,
@ -274,6 +276,9 @@ const EvalReportDatabase = (props) => {
regulatorSpotCheckReportLoading,
} = props.regulatorEvalReport || {};
const { registeredOrgListData } = props.orgInfo || {};
const fetchSummary = async () => {
const res = await props.regulatorEvalReportSummary();
if (res?.success !== false) {
@ -289,8 +294,12 @@ const EvalReportDatabase = (props) => {
};
// RangePicker 拆成接口字段
if (params.archiveTimeRange?.length === 2) {
params.archiveTimeStart = dayjs(params.archiveTimeRange[0]).format("YYYY-MM-DD");
params.archiveTimeEnd = dayjs(params.archiveTimeRange[1]).format("YYYY-MM-DD");
params.archiveTimeStart = dayjs(params.archiveTimeRange[0]).format(
"YYYY-MM-DD",
);
params.archiveTimeEnd = dayjs(params.archiveTimeRange[1]).format(
"YYYY-MM-DD",
);
}
delete params.archiveTimeRange;
@ -361,11 +370,96 @@ const EvalReportDatabase = (props) => {
}
};
const getChecklistData = () => {
const data = registeredOrgListData.find(
(o) => o.id === spotCheckForm.getFieldValue("orgId"),
);
const unitName = data.unitName;
return {
unitName,
fileName: `安全评价报告监督检查单_${unitName}.docx`,
data: {
unitName,
creditCode: data.creditCode || "",
qualificationCertNo: data.qualificationCertNo || "",
legalRepresentative: data.legalRepresentative || "",
businessAddress: data.businessAddress || "",
principalName: data.principalName || "",
principalPhone: data.principalPhone || "",
checkTypeName: getOptionLabel(
CHECK_TYPE_OPTIONS,
spotCheckForm.getFieldValue("checkType"),
),
planCheckDate: spotCheckForm.getFieldValue("planCheckDate")
? dayjs(spotCheckForm.getFieldValue("planCheckDate")).format(
"YYYY年M月D日",
)
: "",
checkLeader: spotCheckForm.getFieldValue("checkLeader") || "",
checkLocation: spotCheckForm.getFieldValue("checkLocation") || "",
checkMembers: spotCheckForm.getFieldValue("checkMembers") || "",
reports: spotCheckData.map((item,index) => ({
index: index + 1,
reportNo: item.reportNo || "",
reportName: item.reportName || "",
evalTypeName: item.evalTypeName || "",
industryName: item.industryName || "",
issueDate: item.issueDate || "",
})),
},
};
};
const uploadDocx = async (blob, fileName) => {
const formData = new FormData();
formData.append("file", new File([blob], fileName), fileName);
const res = await fetch(
`${window.process.env.app.API_HOST}/safetyEval/file/upload`,
{
method: "POST",
headers: { token: sessionStorage.getItem("token") },
body: formData,
},
);
const json = await res.json();
const url = json?.data?.url;
if (!url) throw new Error("上传失败");
return url;
};
const handleViewChecklist = async () => {
setViewChecklistLoading(true);
try {
const { fileName, data } = getChecklistData();
const blob = await renderDocxBlob(CHECKLIST_TEMPLATE_URL, data);
const url = await uploadDocx(blob, fileName);
window.open(url, "_blank");
message.success("检查单已生成");
} finally {
setViewChecklistLoading(false);
}
};
const handleDownloadChecklist = async () => {
const { fileName, data } = getChecklistData();
await fillDocxTemplate(
CHECKLIST_TEMPLATE_URL,
data,
fileName,
{ download: true },
);
message.success("检查单已下载");
};
const handleSearch = (values) => {
const next = { ...values, current: 1, size: router.query.size || 20 };
if (next.archiveTimeRange?.length === 2) {
next.archiveTimeStart = dayjs(next.archiveTimeRange[0]).format("YYYY-MM-DD");
next.archiveTimeEnd = dayjs(next.archiveTimeRange[1]).format("YYYY-MM-DD");
next.archiveTimeStart = dayjs(next.archiveTimeRange[0]).format(
"YYYY-MM-DD",
);
next.archiveTimeEnd = dayjs(next.archiveTimeRange[1]).format(
"YYYY-MM-DD",
);
} else {
next.archiveTimeStart = undefined;
next.archiveTimeEnd = undefined;
@ -515,15 +609,6 @@ const EvalReportDatabase = (props) => {
}
};
const handleOpenSpotcheckList = async (record) => {
setCurrentReport(record);
setSpotcheckVisible(true);
setSpotcheckList([]);
const res = await props.regulatorSpotcheckList({ reportId: record.id });
if (res?.success !== false) {
setSpotcheckList(res?.data || []);
}
};
const handleOpenSpotcheckStart = (record) => {
setCurrentReport(record);
@ -592,7 +677,6 @@ const EvalReportDatabase = (props) => {
title: "报告编号",
dataIndex: "reportNo",
width: 150,
},
{
title: "报告名称",
@ -640,12 +724,8 @@ const EvalReportDatabase = (props) => {
fixed: "right",
render: (_, record) => (
<TableAction size={4} wrap>
<a onClick={() => handleViewDetail(record)}>
查看报告
</a>
<a onClick={() => handleOpenAi(record)}>
AI分析
</a>
<a onClick={() => handleViewDetail(record)}>查看报告</a>
<a onClick={() => handleOpenAi(record)}>AI分析</a>
</TableAction>
),
},
@ -669,8 +749,10 @@ const EvalReportDatabase = (props) => {
dataIndex: "checkResultName",
width: 90,
render: (val, record) => {
if (record.checkResultCode === 1) return <span className="redb-tag redb-tag-success">合格</span>;
if (record.checkResultCode === 2) return <span className="redb-tag redb-tag-danger">不合格</span>;
if (record.checkResultCode === 1)
return <span className="redb-tag redb-tag-success">合格</span>;
if (record.checkResultCode === 2)
return <span className="redb-tag redb-tag-danger">不合格</span>;
return val || "-";
},
},
@ -697,8 +779,10 @@ const EvalReportDatabase = (props) => {
dataIndex: "reviewResultName",
width: 100,
render: (val, record) => {
if (record.reviewResultCode === 1) return <span className="redb-tag redb-tag-success">通过</span>;
if (record.reviewResultCode === 2) return <span className="redb-tag redb-tag-danger">不通过</span>;
if (record.reviewResultCode === 1)
return <span className="redb-tag redb-tag-success">通过</span>;
if (record.reviewResultCode === 2)
return <span className="redb-tag redb-tag-danger">不通过</span>;
return val || "-";
},
},
@ -709,11 +793,7 @@ const EvalReportDatabase = (props) => {
render: (_, record) => {
// 有整改反馈且尚未复核时可复核
if (record.rectifyFeedback && !record.reviewResultCode) {
return (
<a onClick={() => handleOpenReview(record)}>
复核
</a>
);
return <a onClick={() => handleOpenReview(record)}>复核</a>;
}
return "-";
},
@ -796,7 +876,10 @@ const EvalReportDatabase = (props) => {
/>
</Form.Item>,
<Form.Item key="archiveTimeRange" name="archiveTimeRange">
<RangePicker style={{ width: "100%" }} placeholder={["报送时间起", "报送时间止"]} />
<RangePicker
style={{ width: "100%" }}
placeholder={["报送时间起", "报送时间止"]}
/>
</Form.Item>,
]}
onFinish={handleSearch}
@ -811,10 +894,18 @@ const EvalReportDatabase = (props) => {
<Button type="primary" size="small" onClick={handleOpenSpotCheck}>
抽检
</Button>
<Button size="small" icon={<DownloadOutlined />} onClick={handleBatchDownload}>
<Button
size="small"
icon={<DownloadOutlined />}
onClick={handleBatchDownload}
>
批量下载
</Button>
<Button size="small" icon={<ExportOutlined />} onClick={handleOpenExport}>
<Button
size="small"
icon={<ExportOutlined />}
onClick={handleOpenExport}
>
导出目录
</Button>
</div>
@ -888,12 +979,18 @@ const EvalReportDatabase = (props) => {
[
"评价类型",
detailData.evalTypeName ||
getOptionLabel(EVAL_TYPE_OPTIONS, detailData.evalTypeCode),
getOptionLabel(
EVAL_TYPE_OPTIONS,
detailData.evalTypeCode,
),
],
[
"所属行业",
detailData.industryName ||
getOptionLabel(INDUSTRY_OPTIONS, detailData.industryCode),
getOptionLabel(
INDUSTRY_OPTIONS,
detailData.industryCode,
),
],
[
"签发日期",
@ -904,7 +1001,9 @@ const EvalReportDatabase = (props) => {
[
"报送时间",
detailData.archiveTime
? dayjs(detailData.archiveTime).format("YYYY-MM-DD HH:mm")
? dayjs(detailData.archiveTime).format(
"YYYY-MM-DD HH:mm",
)
: "-",
],
]}
@ -923,11 +1022,15 @@ const EvalReportDatabase = (props) => {
onClick={() =>
handleFileDownload(
detailData.fileUrl,
detailData.fileName || detailData.reportName || "正式安全评价报告.pdf"
detailData.fileName ||
detailData.reportName ||
"正式安全评价报告.pdf",
)
}
>
{detailData.fileName || detailData.reportName || "正式安全评价报告.pdf"}
{detailData.fileName ||
detailData.reportName ||
"正式安全评价报告.pdf"}
</span>,
getFileExtLabel(detailData.fileName),
"V1.0",
@ -935,17 +1038,16 @@ const EvalReportDatabase = (props) => {
]}
/>
</ModalSection>
<ModalSection
title="合同文件"
desc="查看上传的合同文件。"
>
<ModalSection title="合同文件" desc="查看上传的合同文件。">
<ProtoTable
headers={["文件名称", "文件类型"]}
rows={parseFileList(detailData.contractFileUrls).map((f) => [
<span
key={f.url}
style={{ color: "#1677ff", cursor: "pointer" }}
onClick={() => handleFileDownload(f.url, f.name || "未命名文件")}
onClick={() =>
handleFileDownload(f.url, f.name || "未命名文件")
}
>
{f.name || "未命名文件"}
</span>,
@ -953,17 +1055,16 @@ const EvalReportDatabase = (props) => {
])}
/>
</ModalSection>
<ModalSection
title="其他过程文档"
desc="查看上传的过程文档。"
>
<ModalSection title="其他过程文档" desc="查看上传的过程文档。">
<ProtoTable
headers={["文件名称", "文件类型"]}
rows={parseFileList(detailData.processDocUrls).map((f) => [
<span
key={f.url}
style={{ color: "#1677ff", cursor: "pointer" }}
onClick={() => handleFileDownload(f.url, f.name || "未命名文件")}
onClick={() =>
handleFileDownload(f.url, f.name || "未命名文件")
}
>
{f.name || "未命名文件"}
</span>,
@ -1009,7 +1110,10 @@ const EvalReportDatabase = (props) => {
{aiData && (
<>
<ModalSection title="分析概览" desc={aiData.analysis.overview}>
<div className="redb-summary-strip" style={{ marginTop: "0.8rem", marginBottom: 0 }}>
<div
className="redb-summary-strip"
style={{ marginTop: "0.8rem", marginBottom: 0 }}
>
{aiData.analysis.metrics.map((m) => (
<span key={m.label}>
{m.label}<strong>{m.value}</strong>
@ -1033,7 +1137,9 @@ const EvalReportDatabase = (props) => {
<td>{row.dim}</td>
<td>{row.result}</td>
<td>
<span className={`redb-tag ${levelClass[row.level] || ""}`}>
<span
className={`redb-tag ${levelClass[row.level] || ""}`}
>
{row.level}
</span>
</td>
@ -1067,7 +1173,13 @@ const EvalReportDatabase = (props) => {
title="导出设置"
desc="导出当前查询范围内的数据,系统将记录导出人和导出时间。"
>
<Form form={exportForm} layout="vertical" preserve={false} className="redb-form-grid" scrollToFirstError>
<Form
form={exportForm}
layout="vertical"
preserve={false}
className="redb-form-grid"
scrollToFirstError
>
<Form.Item
name="scope"
label="导出范围"
@ -1082,7 +1194,12 @@ const EvalReportDatabase = (props) => {
]}
/>
</Form.Item>
<Form.Item name="fileFormat" label="文件格式" initialValue="CSV" className="redb-field">
<Form.Item
name="fileFormat"
label="文件格式"
initialValue="CSV"
className="redb-field"
>
<Select
options={[
{ label: "CSV", value: "CSV" },
@ -1091,7 +1208,11 @@ const EvalReportDatabase = (props) => {
]}
/>
</Form.Item>
<Form.Item name="exportRemark" label="导出说明" className="redb-field redb-field-full">
<Form.Item
name="exportRemark"
label="导出说明"
className="redb-field redb-field-full"
>
<TextArea rows={3} placeholder="选填" />
</Form.Item>
</Form>
@ -1114,14 +1235,37 @@ const EvalReportDatabase = (props) => {
title="批量操作确认"
desc="批量分类将按年度、行业和评价类型重新校验目录;批量下载将生成所选报告的压缩文件。"
>
<Form form={batchForm} layout="vertical" preserve={false} className="redb-form-grid" scrollToFirstError>
<Form.Item name="scopeLabel" label="操作范围" className="redb-field">
<Form
form={batchForm}
layout="vertical"
preserve={false}
className="redb-form-grid"
scrollToFirstError
>
<Form.Item
name="scopeLabel"
label="操作范围"
className="redb-field"
>
<Input disabled />
</Form.Item>
<Form.Item name="keepRecord" label="保留原分类记录" className="redb-field">
<Select options={[{ label: "是", value: "是" }, { label: "否", value: "否" }]} />
<Form.Item
name="keepRecord"
label="保留原分类记录"
className="redb-field"
>
<Select
options={[
{ label: "是", value: "是" },
{ label: "否", value: "否" },
]}
/>
</Form.Item>
<Form.Item name="remark" label="操作说明" className="redb-field redb-field-full">
<Form.Item
name="remark"
label="操作说明"
className="redb-field redb-field-full"
>
<TextArea rows={3} placeholder="选填" />
</Form.Item>
</Form>
@ -1182,7 +1326,12 @@ const EvalReportDatabase = (props) => {
destroyOnClose
className="redb-modal"
>
<Form form={spotcheckForm} layout="vertical" preserve={false} scrollToFirstError>
<Form
form={spotcheckForm}
layout="vertical"
preserve={false}
scrollToFirstError
>
<Form.Item
name="checkResultCode"
label="抽查结果"
@ -1196,20 +1345,34 @@ const EvalReportDatabase = (props) => {
/>
</Form.Item>
<Form.Item name="checkOpinion" label="抽查意见">
<TextArea rows={3} placeholder="填写抽查意见" maxLength={500} showCount />
<TextArea
rows={3}
placeholder="填写抽查意见"
maxLength={500}
showCount
/>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, cur) => prev.checkResultCode !== cur.checkResultCode}
shouldUpdate={(prev, cur) =>
prev.checkResultCode !== cur.checkResultCode
}
>
{({ getFieldValue }) =>
getFieldValue("checkResultCode") === 2 ? (
<Form.Item
name="rectifyRequire"
label="整改要求"
rules={[{ required: true, message: "不合格须填写整改要求" }]}
rules={[
{ required: true, message: "不合格须填写整改要求" },
]}
>
<TextArea rows={3} placeholder="请填写整改要求" maxLength={500} showCount />
<TextArea
rows={3}
placeholder="请填写整改要求"
maxLength={500}
showCount
/>
</Form.Item>
) : null
}
@ -1234,7 +1397,12 @@ const EvalReportDatabase = (props) => {
<div>整改反馈{reviewRecord.rectifyFeedback || "-"}</div>
</div>
)}
<Form form={reviewForm} layout="vertical" preserve={false} scrollToFirstError>
<Form
form={reviewForm}
layout="vertical"
preserve={false}
scrollToFirstError
>
<Form.Item
name="reviewResultCode"
label="复核结果"
@ -1273,7 +1441,9 @@ const EvalReportDatabase = (props) => {
>
<div className="redb-modal-section">
<h4>抽检设置</h4>
<p>从选定评价机构已报送的安全评价报告中随机抽取生成监督检查单</p>
<p>
从选定评价机构已报送的安全评价报告中随机抽取生成监督检查单
</p>
</div>
<Form form={spotCheckForm} layout="vertical" preserve={false}>
<Row gutter={16}>
@ -1307,7 +1477,10 @@ const EvalReportDatabase = (props) => {
</Col>
<Col span={12}>
<Form.Item name="planCheckDate" label="计划检查日期">
<DatePicker style={{ width: "100%" }} placeholder="选择日期" />
<DatePicker
style={{ width: "100%" }}
placeholder="选择日期"
/>
</Form.Item>
</Col>
<Col span={12}>
@ -1327,7 +1500,10 @@ const EvalReportDatabase = (props) => {
</Col>
<Col span={24}>
<Form.Item name="checkBasis" label="补充检查依据">
<TextArea rows={2} placeholder="可填写本次专项检查、年度计划或属地监管文件依据" />
<TextArea
rows={2}
placeholder="可填写本次专项检查、年度计划或属地监管文件依据"
/>
</Form.Item>
</Col>
</Row>
@ -1352,6 +1528,20 @@ const EvalReportDatabase = (props) => {
<h4 style={{ margin: 0 }}>随机抽取报告</h4>
</div>
<Space>
<Button
size="small"
type="primary"
loading={viewChecklistLoading}
onClick={handleViewChecklist}
>
查看检查单
</Button>
<Button
size="small"
onClick={handleDownloadChecklist}
>
下载检查单
</Button>
<Button
size="small"
loading={regulatorSpotCheckReportLoading}
@ -1362,16 +1552,29 @@ const EvalReportDatabase = (props) => {
</Space>
</div>
<Table
rowKey="id"
rowKey="archiveTime"
size="small"
columns={[
{ title: "报告名称", dataIndex: "reportName", ellipsis: true },
{ title: "被检查机构", dataIndex: "orgName", width: 200, ellipsis: true },
{
title: "报告名称",
dataIndex: "reportName",
ellipsis: true,
},
{
title: "被检查机构",
dataIndex: "orgName",
width: 200,
ellipsis: true,
},
{
title: "操作",
width: 90,
render: (_, record) => (
<Button type="primary" size="small" onClick={() => handleViewDetail(record)}>
<Button
type="primary"
size="small"
onClick={() => handleViewDetail(record)}
>
查看
</Button>
),

View File

@ -0,0 +1,76 @@
import PizZip from "pizzip";
import Docxtemplater from "docxtemplater";
/** 模板文件缓存,避免重复下载 */
const templateCache = new Map();
/**
* 加载 docx 模板文件内容带缓存相同 URL 只下载一次
* @param {string} templateUrl - 模板文件 URL
* @returns {Promise<ArrayBuffer>}
*/
async function loadTemplate(templateUrl) {
if (templateCache.has(templateUrl)) {
return templateCache.get(templateUrl);
}
const response = await fetch(templateUrl);
const arrayBuffer = await response.arrayBuffer();
templateCache.set(templateUrl, arrayBuffer);
return arrayBuffer;
}
/**
* 加载 docx 模板并填充数据返回生成的 Blob
* @param {string} templateUrl - 模板文件 URL /xxx.docx
* @param {object} data - 占位符数据 { orgName: "xxx" }
* @returns {Promise<Blob>} 填充后的 docx Blob
*/
export async function renderDocxBlob(templateUrl, data) {
const arrayBuffer = await loadTemplate(templateUrl);
const zip = new PizZip(arrayBuffer);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
});
doc.render(data);
return doc.getZip().generate({
type: "blob",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
}
/**
* 加载 docx 模板填充数据后打开预览或触发下载
* @param {string} templateUrl - 模板文件 URL /xxx.docx
* @param {object} data - 占位符数据 { orgName: "xxx" }
* @param {string} fileName - 文件名称
* @param {object} [options] - 可选配置
* @param {boolean} [options.download=false] - true 时触发下载默认在新窗口预览
*/
export async function fillDocxTemplate(
templateUrl,
data,
fileName,
{ download = false } = {},
) {
const blob = await renderDocxBlob(templateUrl, data);
const url = URL.createObjectURL(blob);
if (download) {
const link = document.createElement("a");
link.href = url;
link.download = fileName || "document.docx";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
return;
}
// 默认:新窗口打开预览
window.open(url, "_blank");
// 延迟释放,避免新窗口加载前 URL 失效
setTimeout(() => URL.revokeObjectURL(url), 60000);
}

View File

@ -38,7 +38,7 @@ export const orgAccountResetPassword = declareRequest(
export const registeredOrgList = declareRequest(
"orgInfoLoading",
"Get > /safetyEval/org-info/page",
'data: [] | res.data || [] & totalCount: 0 | res.totalCount || 0'
'registeredOrgListData: [] | res.data || [] & totalCount: 0 | res.totalCount || 0'
);
export const registeredOrgGet = declareRequest(