Merge remote-tracking branch 'origin/dev-tmp1' into dev-tmp1

dev-tmp1
huwei 2026-08-27 09:27:09 +08:00
commit e55b4eaf80
14 changed files with 725 additions and 115 deletions

View File

@ -13,8 +13,8 @@ module.exports = {
//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

@ -301,6 +301,18 @@ export const evalProcessControlFiles = declareRequest(
"processControlFiles: [] | res.data || []", "processControlFiles: [] | res.data || []",
); );
/** 项目档案文件列表-机构端 */
export const evalProjectArchiveFileOrgPage = declareRequest(
"evalProjectArchiveFileOrgLoading",
"Get > /safetyEval/eval-project-archive-file/org/list",
);
/** 新增项目档案文件(机构补充上传) */
export const evalProjectArchiveFileAdd = declareRequest(
"evalProjectArchiveFileAddLoading",
"Post > @/safetyEval/eval-project-archive-file",
);
// ===== 监管端-安评项目实时监控 ===== // ===== 监管端-安评项目实时监控 =====
export const evalProjectMonitorStat = declareRequest( export const evalProjectMonitorStat = declareRequest(
@ -346,6 +358,12 @@ export const evalProjectArchivePage = declareRequest(
"Get > /safetyEval/institution/eval-project/archive-supervision-page", "Get > /safetyEval/institution/eval-project/archive-supervision-page",
); );
/** 项目档案文件列表-监管端 */
export const evalProjectArchiveFilePage = declareRequest(
"evalProjectArchiveFileLoading",
"Get > /safetyEval/eval-project-archive-file/monitor/page",
);
// ===== 机构端-安评报告库 ===== // ===== 机构端-安评报告库 =====
export const evalReportSummary = declareRequest( export const evalReportSummary = declareRequest(

View File

@ -444,11 +444,11 @@ export const WARING_STATUS_OPTIONS = [
export const NODE_LABELS = { export const NODE_LABELS = {
RISK_ANALYSIS: "风险分析", RISK_ANALYSIS: "风险分析",
BUSINESS_CONTRACT: "务合同", BUSINESS_CONTRACT: "务合同",
PROJECT_GROUP: "项目组", PROJECT_GROUP: "项目组",
SITE_SURVEY: "现场", SITE_SURVEY: "现场勘",
REPORT_PREPARATION: "报告编制", REPORT_PREPARATION: "报告编制",
INTERNAL_REVIEW: "内审", INTERNAL_REVIEW: "内",
TECHNICAL_REVIEW: "技术审核", TECHNICAL_REVIEW: "技术审核",
PROCESS_CONTROL: "过程控制", PROCESS_CONTROL: "过程控制",
ARCHIVE: "项目归档", ARCHIVE: "项目归档",
@ -784,3 +784,47 @@ export const QUESTION_TYPE_MAP = QUESTION_TYPE_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }), (acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{}, {},
); );
/** 项目档案类型eval_project_archive_file.archive_type */
export const ARCHIVE_TYPE_OPTIONS = [
{ label: "项目风险分析", value: "RISK_ANALYSIS" },
{ label: "技术服务合同", value: "TECH_SERVICE_CONTRACT" },
{ label: "项目组织文件", value: "PROJECT_ORG_FILE" },
{ label: "实施计划", value: "IMPLEMENT_PLAN" },
{ label: "从业告知", value: "PRACTICE_NOTICE" },
{ label: "现场记录", value: "SITE_RECORD" },
{ label: "现场勘察资料", value: "SITE_SURVEY" },
{ label: "现场检查资料", value: "SITE_CHECK" },
{ label: "整改复查资料", value: "RECTIFICATION_REVIEW" },
{ label: "评价报告", value: "EVALUATION_REPORT" },
{ label: "审核记录", value: "REVIEW_RECORD" },
{ label: "过程控制文件", value: "PROCESS_CONTROL" },
{ label: "签发文件", value: "SIGN_ISSUE" },
{ label: "专家评审材料", value: "EXPERT_REVIEW" },
{ label: "项目附图", value: "PROJECT_ATTACHMENT" },
{ label: "报告修改材料", value: "REPORT_MODIFICATION" },
{ label: "往来文件", value: "CORRESPONDENCE" },
{ label: "其他", value: "OTHER" },
];
export const ARCHIVE_TYPE_MAP = ARCHIVE_TYPE_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);
/** 档案文件来源record_sourceauto_process_record流程自动记录 / upload_additional_files补充上传 */
export const ARCHIVE_RECORD_SOURCE_MAP = {
auto_process_record: "系统形成",
upload_additional_files: "机构补充上传",
};
/** 档案文件来源选项与徽标文案(机构端档案内容列表) */
export const ARCHIVE_RECORD_SOURCE_OPTIONS = [
{ label: "流程自动记录", value: "auto_process_record" },
{ label: "补充上传", value: "upload_additional_files" },
];
export const ARCHIVE_RECORD_SOURCE_BADGE_MAP = ARCHIVE_RECORD_SOURCE_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);

View File

@ -94,12 +94,14 @@ function ClassAddPage(props) {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
// 培训日期范围拆为后端 startDate/endDateyyyy-MM-dd // 培训日期范围拆为后端 startDate/endDate;结束时间为当天最大值(与延期一致
const { trainingDateRange, ...rest } = values; const { trainingDateRange, ...rest } = values;
const payload = { const payload = {
...rest, ...rest,
startDate: trainingDateRange?.[0]?.format("YYYY-MM-DD"), startDate: trainingDateRange?.[0]?.format("YYYY-MM-DD"),
endDate: trainingDateRange?.[1]?.format("YYYY-MM-DD"), endDate: trainingDateRange?.[1]
?.endOf("day")
.format("YYYY-MM-DD HH:mm:ss"),
...(isEdit ? { id: router.query.classId } : {}), ...(isEdit ? { id: router.query.classId } : {}),
}; };
const res = await (isEdit const res = await (isEdit

View File

@ -523,6 +523,7 @@ const EvalProjectMonitorDetail = (props) => {
<WpsEditor <WpsEditor
onCancel={() => setWpsVisible(false)} onCancel={() => setWpsVisible(false)}
fileId={wpsDoc?.fileId} fileId={wpsDoc?.fileId}
fileName={wpsDoc?.name}
readOnly readOnly
/> />
)} )}

View File

@ -249,7 +249,7 @@
border-radius: 4px; border-radius: 4px;
background: #eff6ff; background: #eff6ff;
color: #475569; color: #475569;
font-size: 13px; font-size: 14px;
line-height: 1.65; line-height: 1.65;
strong { strong {

View File

@ -1,48 +1,54 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useMemo } from "react";
import { import {
Tag,
Button, Button,
Card,
Table, Table,
Input, Input,
Flex, Flex,
Popover, Select,
Image,
Row, Row,
Col, Col,
DatePicker, DatePicker,
Form, Form,
Modal,
message, message,
} from "antd"; } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
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 { useDebounce } from "~/utils";
import LayoutFooterPortal from "~/components/LayoutFooterPortal"; import LayoutFooterPortal from "~/components/LayoutFooterPortal";
import PreviewUrlButton from "~/components/PreviewUrlButton";
import ArchiveSupplementModal from "./ArchiveSupplementModal";
import {
NODE_LABELS,
ARCHIVE_RECORD_SOURCE_OPTIONS,
ARCHIVE_RECORD_SOURCE_BADGE_MAP,
} from "~/enumerate/constant";
const { router } = tools; const { router } = tools;
const ArchiveContent = ({ readOnly, ...props }) => { const ArchiveContent = ({ readOnly, ...props }) => {
const projectId = router.query?.id; const projectId = router.query?.id;
const { processControlDetail, processControlFiles, evalProjectDetailData } = const { processControlDetail, evalProjectDetailData } =
props.safetyEvalBusiness || {}; props.safetyEvalBusiness || {};
const { processControl, archiveFlag } = processControlDetail || {}; const { processControl, archiveFlag } = processControlDetail || {};
const [files, setFiles] = useState([]);
const [archiveForm] = Form.useForm(); const [archiveForm] = Form.useForm();
const [archiveFiles, setArchiveFiles] = useState([]);
const [sourceFilter, setSourceFilter] = useState("");
const [keyword, setKeyword] = useState("");
const [supplementOpen, setSupplementOpen] = useState(false);
const [viewRecord, setViewRecord] = useState(null);
const fetchDetail = () => { const fetchDetail = () => {
props.evalProcessControlDetail({ projectId }); props.evalProcessControlDetail({ projectId });
}; };
const fetchFiles = () => {
props.evalProcessControlFiles({ projectId });
};
useEffect(() => { useEffect(() => {
if (projectId) { if (projectId) {
fetchDetail(); fetchDetail();
fetchFiles();
} }
}, [projectId]); }, [projectId]);
@ -59,11 +65,34 @@ const ArchiveContent = ({ readOnly, ...props }) => {
} }
}, [processControlDetail]); }, [processControlDetail]);
const fetchArchiveFiles = () => {
props.evalProjectArchiveFileOrgPage({ projectId }).then((res) => {
if (res?.success !== false) {
setArchiveFiles(res?.data || []);
}
});
};
useEffect(() => { useEffect(() => {
if (processControlFiles) { if (!projectId) return;
setFiles(processControlFiles); fetchArchiveFiles();
} }, [projectId]);
}, [processControlFiles]);
/** 本地筛选:来源 + 文件名称/档案类型关键字(防抖 400ms */
const debouncedKeyword = useDebounce(keyword, 400);
const filteredArchiveFiles = useMemo(() => {
const kw = debouncedKeyword.trim().toLowerCase();
return archiveFiles.filter((item) => {
const matchSource = !sourceFilter || item.recordSource === sourceFilter;
const matchKeyword =
!kw ||
(item.files || []).some((f) =>
(f.name || "").toLowerCase().includes(kw),
) ||
(item.archiveType || "").toLowerCase().includes(kw);
return matchSource && matchKeyword;
});
}, [archiveFiles, sourceFilter, debouncedKeyword]);
const isSignatureCompleted = const isSignatureCompleted =
processControl?.reviewerSignatureTaskStatus === "completed"; processControl?.reviewerSignatureTaskStatus === "completed";
@ -80,91 +109,149 @@ const ArchiveContent = ({ readOnly, ...props }) => {
} }
}; };
const isImage = (name) => /** 解析文件地址:相对路径拼接 window.fileUrl */
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(name || ""); const resolveFileUrl = (raw) => {
if (!raw) return "";
const u = String(raw);
return /^https?:\/\//i.test(u) ? u : `${window.fileUrl || ""}${u}`;
};
const fileColumns = [ /** 单文件下载:跨域地址下 a.download 无效fetch 转 blob 后再触发下载 */
{ title: "流程名称", dataIndex: "processName", ellipsis: true }, const downloadArchiveFile = async (f) => {
{ title: "条目名称", dataIndex: "itemName", ellipsis: true }, const url = resolveFileUrl(f.url);
if (!url) return;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blobUrl = window.URL.createObjectURL(await res.blob());
const link = document.createElement("a");
link.href = blobUrl;
link.download = f.name || "";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
/** 批量下载 files 数组(逐个串行下载,失败不影响其余文件) */
const handleBatchDownload = async (files) => {
for (const f of files || []) {
try {
await downloadArchiveFile(f);
} catch {
message.error(`文件下载失败:${f.name || f.url}`);
}
}
};
const archiveColumns = [
{ {
title: "文件", title: "序号",
dataIndex: "projectDocUrl", width: 60,
render: (val) => { render: (_, __, index) => index + 1,
let fileList = []; },
if (typeof val === "string") { {
try { title: "文件名称",
fileList = JSON.parse(val); dataIndex: "files",
} catch { ellipsis: true,
fileList = []; render: (v) => {
} if(Array.isArray(v)){
} else if (Array.isArray(val)) { return v.map(v=>v.name).join(',')
fileList = val;
} }
if (!fileList.length) return "-";
return (
<Flex vertical gap={2}>
<Popover
title="附件列表"
content={
<Flex vertical gap={4}>
{fileList.map((f, i) => {
const url = f.url || f.response?.url;
return isImage(f.name) ? (
<Flex key={f.uid || i} align="center" gap={4}>
{<Image src={url} width={80} />}
</Flex>
) : (
<a
key={f.uid || i}
style={{ cursor: "pointer" }}
onClick={() => window.open(url)}
>
{f.name || "查看文件"}
</a>
);
})}
</Flex>
}
trigger="hover"
placement="topLeft"
>
<a
onClick={(e) => e.preventDefault()}
style={{ cursor: "pointer" }}
>
{fileList.length}个附件
</a>
</Popover>
</Flex>
);
}, },
}, },
{
title: "档案类型",
dataIndex: "archiveType",
width: 180,
},
{
title: "来源/业务节点",
dataIndex: "recordSource",
width: 150,
render: (v, r) => (
<Flex vertical gap={4}>
<span
className={`pf-archive__source-tag${v === "upload_additional_files" ? " pf-archive__source-tag--upload" : ""
}`}
>
{ARCHIVE_RECORD_SOURCE_BADGE_MAP[v] || v || "-"}
</span>
<span className="pf-archive__node">{NODE_LABELS[r.businessNode] || "-"}</span>
</Flex>
),
},
{
title: "形成/上传人员",
dataIndex: "createName",
width: 120,
render: (v) => v || "-",
},
{
title: "形成时间",
dataIndex: "createTime",
width: 200,
render: (v) => v || "-",
},
{
title: "操作",
width: 110,
render: (_, r) => (
<TableAction>
<Button
type="link"
size="small"
disabled={!r.files?.length}
onClick={() => setViewRecord(r)}
>
查看
</Button>
<Button
type="link"
size="small"
disabled={!r.files?.length}
onClick={() => handleBatchDownload(r.files)}
>
下载
</Button>
</TableAction>
),
},
]; ];
return ( return (
<Card <div
title="项目归档"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
> >
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
项目归档 <Flex justify="space-between" className="pf-archive__toolbar">
</Tag> <Input.Search
<span className="pf-desc"> placeholder="搜索文件名称或档案类型"
项目过程文件清单确认签字完成后归档入库 allowClear
</span> onChange={(e) => setKeyword(e.target.value)}
</div> onSearch={setKeyword}
className="pf-archive__keyword-input"
/>
<Select
value={sourceFilter}
onChange={setSourceFilter}
options={[
{ label: "全部来源", value: "" },
...ARCHIVE_RECORD_SOURCE_OPTIONS,
]}
className="pf-archive__source-select"
/>
</Flex>
<Table <Table
dataSource={files} rowKey="id"
columns={fileColumns}
rowKey={(record) =>
`${record.processName}-${record.materialName}-${record.itemName}`
}
pagination={false}
size="small" size="small"
dataSource={filteredArchiveFiles}
columns={archiveColumns}
loading={props.safetyEvalBusiness?.evalProjectArchiveFileOrgLoading}
pagination={false}
/> />
<Form form={archiveForm} layout="vertical" disabled={readOnly} scrollToFirstError> <Form form={archiveForm} layout="vertical" disabled={readOnly} scrollToFirstError>
@ -197,18 +284,50 @@ const ArchiveContent = ({ readOnly, ...props }) => {
{!readOnly && ( {!readOnly && (
<LayoutFooterPortal slotId="pf-footer-slot"> <LayoutFooterPortal slotId="pf-footer-slot">
<Flex justify="flex-end" gap={8}> <Flex justify="flex-end" gap={8}>
<Button disabled={archiveFlag} onClick={() => setSupplementOpen(true)}>
补充上传
</Button>
<Button <Button
type="primary" type="primary"
onClick={handleArchive} onClick={handleArchive}
loading={props.safetyEvalBusiness?.processControlArchiveLoading} loading={props.safetyEvalBusiness?.processControlArchiveLoading}
disabled={!canArchive || archiveFlag} disabled={!canArchive || archiveFlag}
> >
{archiveFlag ? '已归档' : '双方签字并确认入库'} {archiveFlag ? '已归档' : '确认归档入库'}
</Button> </Button>
</Flex> </Flex>
</LayoutFooterPortal> </LayoutFooterPortal>
)} )}
</Card>
<ArchiveSupplementModal
open={supplementOpen}
projectId={projectId}
loading={props.safetyEvalBusiness?.evalProjectArchiveFileAddLoading}
onAdd={props.evalProjectArchiveFileAdd}
onClose={() => setSupplementOpen(false)}
onSuccess={() => {
setSupplementOpen(false);
fetchArchiveFiles();
}}
/>
<Modal
open={!!viewRecord}
title="查看档案文件"
width={640}
footer={null}
destroyOnHidden
onCancel={() => setViewRecord(null)}
>
<Flex gap={8}>
{(viewRecord?.files || []).map((f, i) => (
<PreviewUrlButton key={f.url || i} url={f.url}>
{f.name || "查看文件"}
</PreviewUrlButton>
))}
</Flex>
</Modal>
</div>
); );
}; };

View File

@ -0,0 +1,85 @@
import React from "react";
import { Form, Input, Modal, message } from "antd";
import AttachmentUpload from "~/components/AttachmentUpload";
const { TextArea } = Input;
/** 补充上传项目档案弹窗(原型 modal-archive-supplement */
function ArchiveSupplementModal({
open,
projectId,
loading,
onAdd,
onClose,
onSuccess,
}) {
const [form] = Form.useForm();
const handleOk = async () => {
const values = await form.validateFields();
const fileList = values.files || [];
if (fileList.some((f) => f.status === "uploading")) {
message.warning("文件正在上传中,请等待上传完成");
return;
}
const doneFiles = fileList.filter((f) => f.status === "done" && f.url);
if (!doneFiles.length) {
message.warning("请先上传资料文件");
return;
}
const total = doneFiles.length;
const res = await onAdd({
projectId,
archiveType: values.archiveType,
businessNode: "文档上传",
recordSource: "upload_additional_files",
remark: values.remark,
files: doneFiles,
});
if (res?.success === false) return;
message.success("补充档案已上传");
onSuccess?.();
};
return (
<Modal
open={open}
title="补充上传项目档案"
width={680}
okText="确认上传"
cancelText="取消"
confirmLoading={loading}
destroyOnHidden
onOk={handleOk}
onCancel={onClose}
>
<Form form={form} layout="vertical" preserve={false}>
<Form.Item
label="资料名称"
name="fileName"
rules={[{ required: true, message: "请输入资料名称" }]}
>
<Input placeholder="请输入资料名称" allowClear maxLength={100} />
</Form.Item>
<Form.Item
label="档案类型"
name="archiveType"
rules={[{ required: true, message: "请填写档案类型" }]}
>
<Input placeholder="自行填写档案类型" allowClear maxLength={50} />
</Form.Item>
<Form.Item label="资料说明" name="remark">
<TextArea rows={3} placeholder="填写资料内容或补充说明" maxLength={500} />
</Form.Item>
<AttachmentUpload
name="files"
label="资料上传"
rules={[{ required: true, message: "请选择需要上传的文件" }]}
extra="支持上传多个文件"
/>
</Form>
</Modal>
);
}
export default ArchiveSupplementModal;

View File

@ -173,7 +173,6 @@ const ProjectFlow = (props) => {
}; };
const openReportEditor = async () => { const openReportEditor = async () => {
// 2026-08-18 前置拦截:报告文档 fileId 缺省(文档尚未生成/加载完成)时提示,不打开编辑器,避免 SDK 报错
if (!wpsDoc?.fileId) { if (!wpsDoc?.fileId) {
message.warning("报告文档尚未生成,请先创建报告后再查看"); message.warning("报告文档尚未生成,请先创建报告后再查看");
return; return;
@ -272,7 +271,7 @@ const ProjectFlow = (props) => {
destroyOnHidden destroyOnHidden
/> />
{visible && ( {visible && (
<WpsEditor onCancel={() => setVisible(false)} fileId={wpsDoc?.fileId} readOnly={readOnly || evalProjectDetailData?.archiveFlag === 1} /> <WpsEditor onCancel={() => setVisible(false)} fileId={wpsDoc?.fileId} fileName={wpsDoc?.name} readOnly={readOnly || evalProjectDetailData?.archiveFlag === 1} />
)} )}
</PageLayout> </PageLayout>
); );

View File

@ -290,4 +290,39 @@
&-hidden-control { &-hidden-control {
display: none; display: none;
} }
}
// 档案内容 - 档案文件列表
.pf-archive {
&__toolbar {
margin-bottom: 12px;
}
&__keyword-input {
width: 260px;
}
&__source-select {
width: 140px;
}
&__source-tag {
display: inline-block;
width: fit-content;
padding: 2px 10px;
color: #fff;
font-size: 14px;
line-height: 20px;
background: #52c41a;
border-radius: 4px;
&--upload {
background: #1677ff;
}
}
&__node {
color: #94a3b8;
font-size: 14px;
}
} }

View File

@ -0,0 +1,227 @@
import React, { useEffect, useMemo, useState } from "react";
import { Button, Flex, Input, Modal, Table, Tag, message } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import PreviewUrlButton from "~/components/PreviewUrlButton";
import { useDebounce } from "~/utils";
import {
NODE_LABELS,
ARCHIVE_TYPE_MAP,
ARCHIVE_RECORD_SOURCE_MAP,
} from "~/enumerate/constant";
/** 解析文件地址:相对路径拼接 window.fileUrl与 PreviewUrlButton 保持一致) */
const resolveFileUrl = (raw) => {
if (!raw) return "";
const u = String(raw);
return /^https?:\/\//i.test(u) ? u : `${window.fileUrl || ""}${u}`;
};
/** 项目档案查看弹窗(原型 modal-reg-project-archive */
function ArchiveModal({ record, loading, fetchFiles, onClose }) {
const [list, setList] = useState([]);
const [keyword, setKeyword] = useState("");
const [viewRecord, setViewRecord] = useState(null);
useEffect(() => {
if (!record?.id) return;
fetchFiles({ projectId: record.id, pageSize: 500 }).then((res) => {
if (res?.success !== false) {
setList(res?.data || []);
}
});
}, [record?.id]);
const debouncedKeyword = useDebounce(keyword, 400);
const filteredList = useMemo(() => {
const kw = debouncedKeyword.trim().toLowerCase();
if (!kw) return list;
return list.filter(
(item) =>
(item.files || []).some((f) =>
(f.name || "").toLowerCase().includes(kw),
) || (item.archiveType || "").toLowerCase().includes(kw),
);
}, [list, debouncedKeyword]);
/** 单文件下载:跨域地址下 a.download 无效fetch 转 blob 后再触发下载 */
const downloadArchiveFile = async (f) => {
const url = resolveFileUrl(f.url);
if (!url) return;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blobUrl = window.URL.createObjectURL(await res.blob());
const link = document.createElement("a");
link.href = blobUrl;
link.download = f.name || "";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
/** 批量下载 files 数组(逐个串行下载,失败不影响其余文件) */
const handleBatchDownload = async (files) => {
for (const f of files || []) {
try {
await downloadArchiveFile(f);
} catch {
message.error(`文件下载失败:${f.name || f.url}`);
}
}
};
const columns = [
{
title: "序号",
width: 60,
render: (_, __, index) => index + 1,
},
{
title: "文件名称",
dataIndex: "files",
ellipsis: true,
render: (v, r) => (
<div>
{Array.isArray(v) && v.length
? v.map((f) => f.name).join(",")
: "-"}
</div>
),
},
{
title: "档案类型",
dataIndex: "archiveType",
width: 130,
},
{
title: "来源",
dataIndex: "recordSource",
width: 120,
render: (v) =>
v ? (
<Tag color={v === "upload_additional_files" ? "warning" : "processing"}>
{ARCHIVE_RECORD_SOURCE_MAP[v]}
</Tag>
) : (
"-"
),
},
{
title: "业务节点",
dataIndex: "businessNode",
width: 130,
render: (v) => NODE_LABELS[v] || v || "-",
},
{
title: "操作",
width: 120,
render: (_, r) => (
<TableAction>
<Button
type="link"
size="small"
disabled={!r.files?.length}
onClick={() => setViewRecord(r)}
>
查看
</Button>
<Button
type="link"
size="small"
disabled={!r.files?.length}
onClick={() => handleBatchDownload(r.files)}
>
下载
</Button>
</TableAction>
),
},
];
const processCount = record?.processFileCount ?? 0;
const additionalCount = record?.additionalFileCount ?? 0;
return (
<Modal
open={!!record}
title={
<div>
<div>项目档案查看</div>
<div className="epd-archive__subtitle">
查看项目执行过程中由系统形成及机构补充上传的档案资料
</div>
</div>
}
width={1000}
footer={<Button onClick={onClose}>关闭</Button>}
destroyOnHidden
onCancel={onClose}
className="epd-archive"
>
<div className="epd-archive__meta">
<div>
<span>项目名称</span>
<strong>{record?.projectName || "-"}</strong>
<small>{record?.projectNo}</small>
</div>
<div>
<span>评价机构</span>
<strong>{record?.orgName || "-"}</strong>
</div>
<div>
<span>评价类型</span>
<strong>{record?.evalTypeName || "-"}</strong>
</div>
<div>
<span>档案数量</span>
<strong>{processCount + additionalCount} </strong>
<small>
流程文档 {processCount} / 补充文件 {additionalCount}
</small>
</div>
</div>
<Input.Search
placeholder="搜索文件名称或档案类型"
allowClear
onChange={(e) => setKeyword(e.target.value)}
onSearch={setKeyword}
className="epd-archive__search"
/>
<Table
rowKey="id"
size="small"
columns={columns}
dataSource={filteredList}
loading={loading}
scroll={{ x: 800, y: 400 }}
pagination={false}
/>
<Modal
open={!!viewRecord}
title="查看档案文件"
width={640}
footer={null}
destroyOnHidden
onCancel={() => setViewRecord(null)}
>
<Flex gap={8}>
{(viewRecord?.files || []).map((f, i) => (
<PreviewUrlButton key={f.url || i} url={f.url}>
{f.name || "查看文件"}
</PreviewUrlButton>
))}
</Flex>
</Modal>
</Modal>
);
}
export default ArchiveModal;

View File

@ -10,6 +10,7 @@ import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { EVAL_TYPE_OPTIONS, EVAL_TYPE_MAP } from "~/enumerate/constant"; import { EVAL_TYPE_OPTIONS, EVAL_TYPE_MAP } from "~/enumerate/constant";
import { tools } from "@cqsjjb/jjb-common-lib"; import { tools } from "@cqsjjb/jjb-common-lib";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction"; import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import ArchiveModal from "./ArchiveModal";
import "./index.less"; import "./index.less";
@ -27,6 +28,7 @@ function EvalProjectDatabase(props) {
const [searchForm] = Form.useForm(); const [searchForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]); const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [archiveRecord, setArchiveRecord] = useState(null);
const { const {
safetyEvalBusiness, safetyEvalBusiness,
@ -113,14 +115,16 @@ function EvalProjectDatabase(props) {
{ {
title: "最近更新", title: "最近更新",
dataIndex: "updateTime", dataIndex: "updateTime",
width: 180,
}, },
{ {
title: "操作", title: "操作",
dataIndex: "operate", dataIndex: "operate",
fix: 'right', fixed: "right",
width: 200, width: 200,
render: (_, record) => { render: (_, record) => {
return <TableAction> return <TableAction>
<a onClick={() => setArchiveRecord(record)}>查看档案</a>
<a onClick={() => { <a onClick={() => {
props.history.push(`/container/MonitorManage/EvalProjectMonitor/Detail?id=${record.id}`) props.history.push(`/container/MonitorManage/EvalProjectMonitor/Detail?id=${record.id}`)
}}>项目过程</a> }}>项目过程</a>
@ -189,6 +193,13 @@ function EvalProjectDatabase(props) {
}} }}
onChange={handlePageChange} onChange={handlePageChange}
/> />
{archiveRecord&&<ArchiveModal
record={archiveRecord}
loading={safetyEvalBusiness?.evalProjectArchiveFileLoading}
fetchFiles={props.evalProjectArchiveFilePage}
onClose={() => setArchiveRecord(null)}
/>}
</PageLayout> </PageLayout>
); );
} }

View File

@ -3,9 +3,59 @@
margin: 12px 0 16px; margin: 12px 0 16px;
padding: 10px 16px; padding: 10px 16px;
color: rgba(0, 0, 0, 65%); color: rgba(0, 0, 0, 65%);
font-size: 13px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
background: #fffbe6; background: #fffbe6;
border: 1px solid #ffe58f; border: 1px solid #ffe58f;
border-radius: 6px; border-radius: 6px;
} }
/* 项目档案查看弹窗 */
.epd-archive {
&__subtitle {
margin-top: 4px;
color: #64748b;
font-size: 14px;
font-weight: normal;
}
&__meta {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 6px;
margin-bottom: 16px;
padding: 12px 16px;
background: #f8fafc;
border-radius: 6px;
> div {
display: flex;
flex-direction: column;
span {
color: #64748b;
font-size: 14px;
}
strong {
color: #0f172a;
font-size: 14px;
}
small {
color: #94a3b8;
font-size: 14px;
}
}
}
&__search {
margin-bottom: 12px;
max-width: 320px;
}
&__file-source {
color: #94a3b8;
font-size: 14px;
}
}

View File

@ -24,11 +24,16 @@ import ErrorBoundary from "./ErrorBoundary";
const { router } = tools; const { router } = tools;
const App = (props) => { const App = (props) => {
const { onCancel, safetyEvalBusiness, readOnly } = props; const { onCancel, safetyEvalBusiness, readOnly, fileName } = props;
const { createDocLoading, evalReportCompletePreparationLoading } = const { createDocLoading, evalReportCompletePreparationLoading } =
safetyEvalBusiness; safetyEvalBusiness;
const [expandedMap, setExpandedMap] = useState({}); const [expandedMap, setExpandedMap] = useState({});
const [fileId, setFileId] = useState(props.fileId); const [fileId, setFileId] = useState(props.fileId);
// 当前文档类型word / pdf决定 SDK 初始化时使用的 officeType
// 初始类型通过初始报告文件名fileName推断
const [fileType, setFileType] = useState(() =>
/\.pdf$/i.test(props.fileName || "") ? "pdf" : "word"
);
const [templateMap, setTemplateMap] = useState([]); const [templateMap, setTemplateMap] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const sdkRef = useRef(null); const sdkRef = useRef(null);
@ -73,8 +78,14 @@ const App = (props) => {
if (!newFileId) { if (!newFileId) {
throw new Error(res?.errMessage || res?.message || "上传失败"); throw new Error(res?.errMessage || res?.message || "上传失败");
} }
setFileType(/\.pdf$/i.test(file.name) ? "pdf" : "word");
setFileId(newFileId); setFileId(newFileId);
message.success("Word 文件上传成功,已在编辑器中打开");
props.queryMyOrCreateDoc({
docxUrl: "",
projectId:router.query.id,
});
message.success("文件上传成功,已在编辑器中打开");
}) })
.catch((e) => { .catch((e) => {
message.error(e?.message || "上传失败,请重试"); message.error(e?.message || "上传失败,请重试");
@ -103,7 +114,7 @@ const App = (props) => {
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = blobUrl; a.href = blobUrl;
a.download = `安全评价报告_${fileId}.docx`; a.download = `安全评价报告_${fileId}.${fileType === "pdf" ? "pdf" : "docx"}`;
a.click(); a.click();
URL.revokeObjectURL(blobUrl); URL.revokeObjectURL(blobUrl);
message.success("下载成功"); message.success("下载成功");
@ -147,7 +158,10 @@ const App = (props) => {
try { try {
const instance = WebOfficeSDK.init({ const instance = WebOfficeSDK.init({
// 必填项 // 必填项
officeType: WebOfficeSDK.OfficeType.Writer, officeType:
fileType === "pdf"
? WebOfficeSDK.OfficeType.Pdf
: WebOfficeSDK.OfficeType.Writer,
appId: "AK20260805KPWYZJ", appId: "AK20260805KPWYZJ",
mount: document.getElementById("app-wps"), mount: document.getElementById("app-wps"),
fileId: fileId, fileId: fileId,
@ -157,12 +171,12 @@ const App = (props) => {
}, },
...(readOnly ...(readOnly
? { ? {
commonOptions: { commonOptions: {
isBrowserViewMode: true, isBrowserViewMode: true,
// 只读模式下隐藏 WPS 顶部区域(头部和工具栏),页面已有自己的标题栏 // 只读模式下隐藏 WPS 顶部区域(头部和工具栏),页面已有自己的标题栏
isShowTopArea: false, isShowTopArea: false,
}, },
} }
: {}), : {}),
}); });
await instance.ready(); await instance.ready();
@ -173,6 +187,7 @@ const App = (props) => {
} }
})(); })();
return () => { return () => {
console.log(sdkRef.current)
sdkRef.current?.destroy(); sdkRef.current?.destroy();
window.removeEventListener("error", onWindowError, { capture: true }); window.removeEventListener("error", onWindowError, { capture: true });
window.removeEventListener("unhandledrejection", onWindowError, { capture: true }); window.removeEventListener("unhandledrejection", onWindowError, { capture: true });
@ -349,15 +364,19 @@ const App = (props) => {
size="small" size="small"
onClick={() => { onClick={() => {
props props
.createWpsDoc({ data:{ .createWpsDoc({
docxUrl: url, projectId: router.query.id data: {
}}) docxUrl: url, projectId: router.query.id
}
})
.then((res) => { .then((res) => {
const fileId = res?.data?.fileId; const fileId = res?.data?.fileId;
if (!fileId) { if (!fileId) {
return; return;
} }
setFileId(fileId); setFileId(fileId);
// 模板为 docx 文档,重置为 Word 类型
setFileType("word");
}); });
}} }}
> >