dev-tmp1
tangjie 2026-08-25 17:55:37 +08:00
parent 5991b95195
commit a874c2cb0b
10 changed files with 591 additions and 95 deletions

View File

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

View File

@ -784,3 +784,47 @@ export const QUESTION_TYPE_MAP = QUESTION_TYPE_OPTIONS.reduce(
(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

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

View File

@ -1,13 +1,10 @@
import React, { useState, useEffect } from "react";
import {
Tag,
Button,
Card,
Table,
Input,
Flex,
Popover,
Image,
Select,
Row,
Col,
DatePicker,
@ -16,33 +13,43 @@ import {
} from "antd";
import dayjs from "dayjs";
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 { tools } from "@cqsjjb/jjb-common-lib";
import LayoutFooterPortal from "~/components/LayoutFooterPortal";
import PreviewUrlButton from "~/components/PreviewUrlButton";
import ArchiveSupplementModal from "./ArchiveSupplementModal";
import {
ARCHIVE_RECORD_SOURCE_OPTIONS,
ARCHIVE_RECORD_SOURCE_BADGE_MAP,
} from "~/enumerate/constant";
const { router } = tools;
const ArchiveContent = ({ readOnly, ...props }) => {
const projectId = router.query?.id;
const { processControlDetail, processControlFiles, evalProjectDetailData } =
const { processControlDetail, evalProjectDetailData } =
props.safetyEvalBusiness || {};
const { processControl, archiveFlag } = processControlDetail || {};
const [files, setFiles] = useState([]);
const [archiveForm] = Form.useForm();
const [archiveFiles, setArchiveFiles] = useState([]);
const [archiveTotal, setArchiveTotal] = useState(0);
const [archiveQuery, setArchiveQuery] = useState({
pageIndex: 1,
pageSize: 10,
recordSource: "",
});
const [supplementOpen, setSupplementOpen] = useState(false);
const fetchDetail = () => {
props.evalProcessControlDetail({ projectId });
};
const fetchFiles = () => {
props.evalProcessControlFiles({ projectId });
};
useEffect(() => {
if (projectId) {
fetchDetail();
fetchFiles();
}
}, [projectId]);
@ -59,11 +66,26 @@ const ArchiveContent = ({ readOnly, ...props }) => {
}
}, [processControlDetail]);
const fetchArchiveFiles = () => {
props
.evalProjectArchiveFileOrgPage({
projectId,
pageIndex: archiveQuery.pageIndex,
pageSize: archiveQuery.pageSize,
recordSource: archiveQuery.recordSource || undefined,
})
.then((res) => {
if (res?.success !== false) {
setArchiveFiles(res?.data || []);
setArchiveTotal(res?.totalCount || 0);
}
});
};
useEffect(() => {
if (processControlFiles) {
setFiles(processControlFiles);
}
}, [processControlFiles]);
if (!projectId) return;
fetchArchiveFiles();
}, [projectId, archiveQuery]);
const isSignatureCompleted =
processControl?.reviewerSignatureTaskStatus === "completed";
@ -80,91 +102,127 @@ const ArchiveContent = ({ readOnly, ...props }) => {
}
};
const isImage = (name) =>
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(name || "");
/** 解析文件地址:相对路径拼接 window.fileUrl */
const resolveFileUrl = (raw) => {
if (!raw) return "";
const u = String(raw);
return /^https?:\/\//i.test(u) ? u : `${window.fileUrl || ""}${u}`;
};
const fileColumns = [
{ title: "流程名称", dataIndex: "processName", ellipsis: true },
{ title: "条目名称", dataIndex: "itemName", ellipsis: true },
const handleDownload = (file) => {
const url = resolveFileUrl(file.fileUrl);
if (!url) return;
const a = document.createElement("a");
a.href = url;
a.download = file.fileName || "";
a.target = "_blank";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const archiveColumns = [
{
title: "文件",
dataIndex: "projectDocUrl",
render: (val) => {
let fileList = [];
if (typeof val === "string") {
try {
fileList = JSON.parse(val);
} catch {
fileList = [];
}
} else if (Array.isArray(val)) {
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: "序号",
width: 60,
render: (_, __, index) =>
(archiveQuery.pageIndex - 1) * archiveQuery.pageSize + index + 1,
},
{
title: "文件名称",
dataIndex: "fileName",
ellipsis: true,
render: (v) => v || "-",
},
{
title: "档案类型",
dataIndex: "archiveType",
width: 130,
},
{
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">{r.businessNode || "-"}</span>
</Flex>
),
},
{
title: "形成/上传人员",
dataIndex: "createName",
width: 120,
render: (v) => v || "-",
},
{
title: "形成时间",
dataIndex: "createTime",
width: 160,
align: "right",
render: (v) => v || "-",
},
{
title: "操作",
width: 110,
render: (_, r) => (
<TableAction>
<PreviewUrlButton url={r.fileUrl}>查看</PreviewUrlButton>
<Button
type="link"
size="small"
disabled={!r.fileUrl}
onClick={() => handleDownload(r)}
>
下载
</Button>
</TableAction>
),
},
];
return (
<Card
title="项目归档"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
<div
>
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
项目归档
</Tag>
<span className="pf-desc">
项目过程文件清单确认签字完成后归档入库
</span>
</div>
<Flex justify="flex-end" className="pf-archive__toolbar">
<Select
value={archiveQuery.recordSource}
onChange={(v) =>
setArchiveQuery((q) => ({ ...q, recordSource: v, pageIndex: 1 }))
}
options={[
{ label: "全部来源", value: "" },
...ARCHIVE_RECORD_SOURCE_OPTIONS,
]}
className="pf-archive__source-select"
/>
</Flex>
<Table
dataSource={files}
columns={fileColumns}
rowKey={(record) =>
`${record.processName}-${record.materialName}-${record.itemName}`
}
pagination={false}
rowKey="id"
size="small"
dataSource={archiveFiles}
columns={archiveColumns}
loading={props.safetyEvalBusiness?.evalProjectArchiveFileOrgLoading}
pagination={{
total: archiveTotal,
current: archiveQuery.pageIndex,
pageSize: archiveQuery.pageSize,
showQuickJumper: true,
showTotal: (t) => `${t}`,
onChange: (page, pageSize) =>
setArchiveQuery((q) => ({ ...q, pageIndex: page, pageSize })),
}}
/>
<Form form={archiveForm} layout="vertical" disabled={readOnly} scrollToFirstError>
@ -197,18 +255,33 @@ const ArchiveContent = ({ readOnly, ...props }) => {
{!readOnly && (
<LayoutFooterPortal slotId="pf-footer-slot">
<Flex justify="flex-end" gap={8}>
<Button disabled={archiveFlag} onClick={() => setSupplementOpen(true)}>
补充上传
</Button>
<Button
type="primary"
onClick={handleArchive}
loading={props.safetyEvalBusiness?.processControlArchiveLoading}
disabled={!canArchive || archiveFlag}
>
{archiveFlag ? '已归档' : '双方签字并确认入库'}
{archiveFlag ? '已归档' : '确认归档入库'}
</Button>
</Flex>
</LayoutFooterPortal>
)}
</Card>
<ArchiveSupplementModal
open={supplementOpen}
projectId={projectId}
loading={props.safetyEvalBusiness?.evalProjectArchiveFileAddLoading}
onAdd={props.evalProjectArchiveFileAdd}
onClose={() => setSupplementOpen(false)}
onSuccess={() => {
setSupplementOpen(false);
fetchArchiveFiles();
}}
/>
</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.file || [];
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,
file: 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="file"
label="资料上传"
rules={[{ required: true, message: "请选择需要上传的文件" }]}
extra="支持上传多个文件"
/>
</Form>
</Modal>
);
}
export default ArchiveSupplementModal;

View File

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

View File

@ -290,4 +290,35 @@
&-hidden-control {
display: none;
}
}
// 档案内容 - 档案文件列表
.pf-archive {
&__toolbar {
margin-bottom: 12px;
}
&__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,185 @@
import React, { useEffect, useState } from "react";
import { Button, Input, Modal, Table, Tag } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import PreviewUrlButton from "~/components/PreviewUrlButton";
import {
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 [query, setQuery] = useState({ pageIndex: 1, pageSize: 10, fileName: "" });
const [list, setList] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
if (!record?.id) return;
fetchFiles({ projectId: record.id, ...query }).then((res) => {
if (res?.success !== false) {
setList(res?.data || []);
setTotal(res?.totalCount || 0);
}
});
}, [record?.id, query]);
const handleDownload = (file) => {
const url = resolveFileUrl(file.fileUrl);
if (!url) return;
const a = document.createElement("a");
a.href = url;
a.download = file.fileName || "";
a.target = "_blank";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const columns = [
{
title: "序号",
width: 60,
render: (_, __, index) =>
(query.pageIndex - 1) * query.pageSize + index + 1,
},
{
title: "文件名称",
dataIndex: "fileName",
ellipsis: true,
render: (v, r) => (
<div>
<div>{v || "-"}</div>
<div className="epd-archive__file-source">
{r.recordSource === "upload_additional_files"
? "评价机构补充上传"
: "业务节点自动归集"}
</div>
</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) => v || "-",
},
{
title: "操作",
width: 120,
render: (_, r) => (
<TableAction>
<PreviewUrlButton url={r.fileUrl}>查看</PreviewUrlButton>
<Button
type="link"
size="small"
disabled={!r.fileUrl}
onClick={() => handleDownload(r)}
>
下载
</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
onSearch={(v) => setQuery((q) => ({ ...q, fileName: v, pageIndex: 1 }))}
className="epd-archive__search"
/>
<Table
rowKey="id"
size="small"
columns={columns}
dataSource={list}
loading={loading}
scroll={{ x: 800, y: 400 }}
pagination={{
total,
current: query.pageIndex,
pageSize: query.pageSize,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
onChange: (page, pageSize) =>
setQuery((q) => ({ ...q, pageIndex: page, pageSize })),
}}
/>
</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 { tools } from "@cqsjjb/jjb-common-lib";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import ArchiveModal from "./ArchiveModal";
import "./index.less";
@ -27,6 +28,7 @@ function EvalProjectDatabase(props) {
const [searchForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [archiveRecord, setArchiveRecord] = useState(null);
const {
safetyEvalBusiness,
@ -113,14 +115,16 @@ function EvalProjectDatabase(props) {
{
title: "最近更新",
dataIndex: "updateTime",
width: 180,
},
{
title: "操作",
dataIndex: "operate",
fix: 'right',
fixed: "right",
width: 200,
render: (_, record) => {
return <TableAction>
<a onClick={() => setArchiveRecord(record)}>查看档案</a>
<a onClick={() => {
props.history.push(`/container/MonitorManage/EvalProjectMonitor/Detail?id=${record.id}`)
}}>项目过程</a>
@ -189,6 +193,13 @@ function EvalProjectDatabase(props) {
}}
onChange={handlePageChange}
/>
{archiveRecord&&<ArchiveModal
record={archiveRecord}
loading={safetyEvalBusiness?.evalProjectArchiveFileLoading}
fetchFiles={props.evalProjectArchiveFilePage}
onClose={() => setArchiveRecord(null)}
/>}
</PageLayout>
);
}

View File

@ -3,9 +3,59 @@
margin: 12px 0 16px;
padding: 10px 16px;
color: rgba(0, 0, 0, 65%);
font-size: 13px;
font-size: 14px;
line-height: 1.6;
background: #fffbe6;
border: 1px solid #ffe58f;
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;
}
}