From c96233c0a1a8af3b72f9dc9ad78ea935c26b2ced Mon Sep 17 00:00:00 2001 From: tangjie <122778500@qq.com> Date: Thu, 23 Jul 2026 17:41:28 +0800 Subject: [PATCH] feat --- src/api/safetyEvalBusiness/index.js | 20 ++ src/enumerate/constant/index.js | 14 +- .../SafetyEvalBusiness/MyProject/index.js | 48 ++- .../SafetyEvalBusiness/MyProject/index.less | 28 ++ .../ProjectFlow/ContractContent.js | 2 + .../ProjectFlow/RiskAnalysisContent.js | 1 + .../ProjectFlow/SurveyContent.js | 319 ++++++++++-------- .../ProjectFlow/TeamContent.js | 4 +- .../SafetyEvalBusiness/ProjectFlow/index.js | 152 +++++---- .../SafetyEvalBusiness/ProjectFlow/index.less | 46 +++ .../SafetyEvalBusiness/RiskWarning/index.js | 19 +- 11 files changed, 423 insertions(+), 230 deletions(-) create mode 100644 src/pages/Container/SafetyEvalBusiness/MyProject/index.less diff --git a/src/api/safetyEvalBusiness/index.js b/src/api/safetyEvalBusiness/index.js index 2b9ab08..27a5826 100644 --- a/src/api/safetyEvalBusiness/index.js +++ b/src/api/safetyEvalBusiness/index.js @@ -145,4 +145,24 @@ export const projectWaringStat = declareRequest( export const projectWaringRemindPage = declareRequest( "projectWaringRemindPageLoading", "Get > /safetyEval/project-waring/remind-page", +); + +// ===== 我的项目统计 ===== + +export const evalProjectStatistics = declareRequest( + "evalProjectStatisticsLoading", + "Get > /safetyEval/institution/eval-project/projectStatistics", + 'evalProjectStatisticsData: {} | res.data || {}', +); + +// ===== 现场踏勘附件 ===== + +export const evalSurveyList = declareRequest( + "evalSurveyListLoading", + "Get > /safetyEval/institution/eval-survey/list", +); + +export const evalSurveySaveAttach = declareRequest( + "evalSurveySaveAttachLoading", + "Post > @/safetyEval/institution/eval-survey/saveSurveyAttach", ); \ No newline at end of file diff --git a/src/enumerate/constant/index.js b/src/enumerate/constant/index.js index 3f063d7..f57f86e 100644 --- a/src/enumerate/constant/index.js +++ b/src/enumerate/constant/index.js @@ -299,7 +299,6 @@ export const ENTERPRISE_SCALE_OPTIONS = [ { label: "微型", value: "MICRO" }, ]; - /** 评价类型选项 */ export const EVAL_TYPE_OPTIONS = [ { label: "安全预评价", value: "PRE" }, @@ -369,4 +368,15 @@ export const WARING_STATUS_OPTIONS = [ { label: "紧急预警(1个月)", value: "urgent" }, { label: "已逾期", value: "overdue" }, { label: "正常", value: "normal" }, -]; \ No newline at end of file +]; + +export const NODE_LABELS = { + RISK_ANALYSIS: "风险分析", + BUSINESS_CONTRACT: "商务合同", + PROJECT_GROUP: "项目组", + SITE_SURVEY: "现场勘查", + REPORT_PREPARATION: "报告编制", + INTERNAL_REVIEW: "内审", + TECHNICAL_REVIEW: "技术审核", + PROCESS_CONTROL: "过程控制", +}; diff --git a/src/pages/Container/SafetyEvalBusiness/MyProject/index.js b/src/pages/Container/SafetyEvalBusiness/MyProject/index.js index b71e076..851bcbf 100644 --- a/src/pages/Container/SafetyEvalBusiness/MyProject/index.js +++ b/src/pages/Container/SafetyEvalBusiness/MyProject/index.js @@ -1,24 +1,35 @@ import React, { useState, useEffect } from "react"; import { - Form, Table, Tag, Button, Card, Row, Col, Statistic, Select, Modal, message, + Form, Table, Tag, Button, Card, Row, Col, Statistic, Select, Modal, message, Tooltip, } from "antd"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; +import { NODE_LABELS } from "~/enumerate/constant"; import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { tools } from "@cqsjjb/jjb-common-lib"; +import "./index.less"; const { router } = tools; +const parseProjectNode = (data) => { + if (Array.isArray(data)) return data; + try { + return JSON.parse(data || '[]'); + } catch { + return []; + } +}; + const MyProject = (props) => { const [searchForm] = Form.useForm(); const [dataSource, setDataSource] = useState([]); const [total, setTotal] = useState(0); - const { evalProjectLoading } = props.safetyEvalBusiness; + const { evalProjectLoading, evalProjectStatisticsData } = props.safetyEvalBusiness; const getData = async (pagination) => { const params = { @@ -36,6 +47,7 @@ const MyProject = (props) => { useEffect(() => { searchForm.setFieldsValue(router.query); + props.evalProjectStatistics(); getData(); }, []); @@ -88,6 +100,30 @@ const MyProject = (props) => { return phase ? {phase} : ""; }, }, + { + title: "已完成节点", width: 200, + render: (_, record) => { + const nodes = parseProjectNode(record.projectNode); + if (nodes.length === 0) return 暂无; + const total = nodes.length; + const done = nodes.filter((n) => n.state === 1).length; + + return ( +
+
+ {nodes.map((n, idx) => ( + + + + ))} +
+
+ {done}/{total} · {Math.round((done / total) * 100)}% +
+
+ ); + }, + }, { title: "操作", width: 140, fixed: "right", render: (_, record) => ( @@ -105,25 +141,25 @@ const MyProject = (props) => { - +
负责人/编制人/审核人
- +
当前角色可操作节点
- +
需定位打卡和人脸识别
- +
需优先处理
diff --git a/src/pages/Container/SafetyEvalBusiness/MyProject/index.less b/src/pages/Container/SafetyEvalBusiness/MyProject/index.less new file mode 100644 index 0000000..4454d65 --- /dev/null +++ b/src/pages/Container/SafetyEvalBusiness/MyProject/index.less @@ -0,0 +1,28 @@ +.mp { + &-progress-dots { + display: flex; + gap: 4px; + align-items: center; + } + + &-progress-dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + + &-done { background: #52c41a; } + &-wait { background: #e8e8e8; } + } + + &-progress-text { + font-size: 11px; + color: #999; + margin-top: 4px; + } + + &-progress-empty { + color: #999; + font-size: 12px; + } +} \ No newline at end of file diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/ContractContent.js b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/ContractContent.js index 4e6fd6d..0365c36 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/ContractContent.js +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/ContractContent.js @@ -63,6 +63,7 @@ const ContractContent = (props) => { if (res?.success !== false && res?.data) { form.setFieldsValue({ ...res.data, + scanFileUrl: res.data.scanFileUrl? JSON.parse(res.data.scanFileUrl) :undefined, contractStartDate: res.data.contractStartDate ? dayjs(res.data.contractStartDate) : undefined, @@ -118,6 +119,7 @@ const ContractContent = (props) => { }); if (res?.success !== false) { message.success("合同保存成功"); + props.getDetail(); } } finally { } diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/RiskAnalysisContent.js b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/RiskAnalysisContent.js index d78873c..91505f3 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/RiskAnalysisContent.js +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/RiskAnalysisContent.js @@ -99,6 +99,7 @@ const RiskAnalysisContent = (props) => { ); if (result.success) { message.success(statusCode === 2 ? "风险分析已完成" : "已暂存"); + props.getDetail() } } }; diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/SurveyContent.js b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/SurveyContent.js index 65f886d..3904bfa 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/SurveyContent.js +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/SurveyContent.js @@ -1,65 +1,78 @@ -import React, { useState } from "react"; -import { Tag, Button, Card, Table, Flex, Typography, Steps } from "antd"; -import { CheckCircleFilled } from "@ant-design/icons"; +import React, { useState, useEffect } from "react"; +import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message } from "antd"; +import { CheckCircleFilled, DownloadOutlined } from "@ant-design/icons"; +import { Connect } from "@cqsjjb/jjb-dva-runtime"; +import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace"; +import { tools } from "@cqsjjb/jjb-common-lib"; +import AttachmentUpload from "~/components/AttachmentUpload"; +import "./index.less"; + +const { router } = tools; const STAGES = [ - { key: "notice", index: "01", label: "从业告知",}, - { key: "inspection", index: "02", label: "现场勘查", }, - { key: "rectification", index: "03", label: "整改通知", }, - { key: "enterprise", index: "04", label: "企业整改",}, - { key: "review", index: "05", label: "整改复查", }, + { key: "notice", index: "01", label: "从业告知" }, + { key: "inspection", index: "02", label: "现场勘查" }, + { key: "rectification", index: "03", label: "整改通知" }, + { key: "enterprise", index: "04", label: "企业整改" }, + { key: "review", index: "05", label: "整改复查" }, ]; +const ATTACH_TYPE = { + notice: "PRACTICE_NOTIFICATION", + inspection: "SITE_INSPECTION_FORM", + rectification: "RECTIFICATION_NOTICE", + enterprise: "ENTERPRISE_RECTIFICATION", + review: "RECTIFICATION_REVIEW", +}; + +const downloadTemplate = async () => { + try { + const res = await fetch("https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a608cb30dc53ed98d0b13ae.pdf"); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "从业告知书模板.pdf"; + a.click(); + URL.revokeObjectURL(url); + } catch { + message.error("下载失败,请重试"); + } +}; + const NoticePanel = () => (
-
+
01 安全评价检测检验机构从业告知书 支持在线填写后打印;阶段试行期间,以机构盖章版上传归档作为完成依据。
- 盖章件已上传
-
办理方式 在线填写用于生成、打印标准告知书 打印盖章后上传扫描件或清晰照片 完成节点必须留存盖章件
- - 已上传} style={{ marginBottom: 12 }}> - - - PDF -
- 安全评价检测检验机构从业告知书(盖章版).pdf - 2.4MB · 2026-07-07 10:28 上传 -
-
- - - - -
-
- -
-
机构公章清晰
-
填写内容完整
-
归档版本一致
+
+
+ 📄 +
+
从业告知书模板
+
PDF 格式,可下载后在线填写并打印盖章
+
+
+
- - - - -
); const InspectionPanel = () => (
-
+
02 现场人员打卡与检查表 @@ -96,132 +109,152 @@ const InspectionPanel = () => ( 签到日期:2026-07-15
- - - - - XLSX -
- 华安化工园区现场安全检查表.xlsx - 1.8MB · 李明华 · 2026-07-15 17:06 上传 -
-
- - 已归档 - - - -
-
- - - - - -
); -const SimpleUploadPanel = ({ index, title, desc, icon, fileType, fileName, fileSize, uploader, date }) => ( +const SimpleUploadPanel = ({ index, title, desc }) => (
-
+
{index} {title} {desc}
- 已完成
- - - -
{icon}
- 上传{title} - 支持 PDF、Word、JPG、PNG - -
上传后本节点自动标记完成
-
- - - - 已归档文件 - 已上传 - - - {fileType} -
- {fileName} - {fileSize} · {uploader} · {date} 上传 -
-
- - - - -
-
- - - - -
); -const SurveyContent = () => { +const STEP_PANELS = { + notice: () => , + inspection: () => , + rectification: () => ( + + ), + enterprise: () => ( + + ), + review: () => ( + + ), +}; + +const SurveyContent = (props) => { + const [form] = Form.useForm(); const [activeStage, setActiveStage] = useState("notice"); + const [attachMap, setAttachMap] = useState({}); + const { evalSurveyListLoading, evalSurveySaveAttachLoading } = props.safetyEvalBusiness; + const projectId = router.query?.id; + const currentIdx = STAGES.findIndex((s) => s.key === activeStage); + const isFirst = currentIdx === 0; + const isLast = currentIdx === STAGES.length - 1; + + const loadAttachments = async () => { + if (!projectId) return; + const res = await props.evalSurveyList({ projectId }); + if (res?.success !== false) { + const map = {}; + (res?.data || []).forEach((item) => { + const key = item.attachTypeCode; + try { + const parsed = JSON.parse(item.fileUrl); + map[key] = Array.isArray(parsed) ? parsed : []; + } catch { + map[key] = []; + } + }); + setAttachMap(map); + // 回填所有步骤的附件到表单 + const values = {}; + Object.entries(ATTACH_TYPE).forEach(([, typeCode]) => { + values[typeCode] = map[typeCode] || []; + }); + form.setFieldsValue(values); + } + }; + + useEffect(() => { + loadAttachments(); + }, []); + + const goPrev = () => { + if (currentIdx > 0) setActiveStage(STAGES[currentIdx - 1].key); + }; + + const goNext = () => { + if (currentIdx < STAGES.length - 1) setActiveStage(STAGES[currentIdx + 1].key); + }; + + const handleSave = async () => { + const values = form.getFieldsValue(); + const payload = []; + for (const [key, typeCode] of Object.entries(ATTACH_TYPE)) { + const fileList = values[typeCode] || []; + const validFiles = fileList.filter((f) => f.status === "done" && f.url); + if (validFiles.length === 0) { + setActiveStage(key); + message.warning(`请先在"${STAGES.find((s) => s.key === key).label}"步骤上传附件`); + return; + } + payload.push({ + projectId, + attachTypeCode: typeCode, + fileUrl: JSON.stringify(fileList), + }); + } + const res = await props.evalSurveySaveAttach(payload); + if (res?.success !== false) { + message.success("保存成功"); + props.getDetail(); + } + }; return (
-
- 项目组专职人员 ≥ 2人 - 从业告知、现场勘查、整改与复查全过程留痕 -
+ setActiveStage(STAGES[idx].key)} + style={{ marginBottom: 16 }} + items={STAGES.map((s) => ({ title: s.label }))} + /> - - s.key === activeStage)} - - onChange={(idx) => setActiveStage(STAGES[idx].key)} - style={{ marginBottom: 16 }} - items={STAGES.map((s) => ({ - title: s.label, - status: "process", - }))} - /> + {STEP_PANELS[activeStage]()} - {activeStage === "notice" && } - {activeStage === "inspection" && } - {activeStage === "rectification" && ( - +
+ {Object.entries(ATTACH_TYPE).map(([key, typeCode]) => ( +
+ +
+ ))} +
+ + + {!isFirst && } + {isLast ? ( + + ) : ( + )} - {activeStage === "enterprise" && ( - - )} - {activeStage === "review" && ( - - )} - +
); }; -export default SurveyContent; \ No newline at end of file +export default Connect( + [NS_SAFETY_EVAL_BUSINESS], + true, +)(SurveyContent); \ No newline at end of file diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/TeamContent.js b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/TeamContent.js index edd1e85..ce1462a 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/TeamContent.js +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/TeamContent.js @@ -214,9 +214,9 @@ const TeamContent = (props) => { - + - + setModalOpen(false)} width={700} diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.js b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.js index 552e205..7357ab7 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.js +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.js @@ -21,76 +21,100 @@ const PROCESS_TABS = [ key: "risk", index: "1", label: "风险分析", - state: "已完成", - stateType: "success", role: "市场部经办人 / 市场部经理", }, { key: "contract", index: "2", label: "业务合同", - state: "已完成", - stateType: "success", role: "机构管理员 / 项目负责人", }, - { - key: "team", - index: "3", - label: "项目组", - state: "已完成", - stateType: "success", - role: "评价部经理 / 机构主账号", - }, - { - key: "survey", - index: "4", - label: "现场踏勘", - state: "已完成", - stateType: "success", - role: "项目组成员", - }, - { - key: "report", - index: "5", - label: "报告编制", - state: "编制中", - stateType: "processing", - role: "报告编制人", - }, - { - key: "internal", - index: "6", - label: "内部审核", - state: "待办理", - stateType: "default", - role: "内部审核人", - }, - { - key: "technical", - index: "7", - label: "技术审核", - state: "待办理", - stateType: "default", - role: "技术负责人", - }, - { - key: "control", - index: "8", - label: "过程控制", - state: "前置未完成", - stateType: "warning", - role: "过程控制负责人", - locked: true, - }, + { key: "team", index: "3", label: "项目组", role: "评价部经理 / 机构主账号" }, + { key: "survey", index: "4", label: "现场踏勘", role: "项目组成员" }, + { key: "report", index: "5", label: "报告编制", role: "报告编制人" }, + { key: "internal", index: "6", label: "内部审核", role: "内部审核人" }, + { key: "technical", index: "7", label: "技术审核", role: "技术负责人" }, + { key: "control", index: "8", label: "过程控制", role: "过程控制负责人" }, ]; const { router } = tools; +const TYPE_TO_TAB_KEY = { + RISK_ANALYSIS: "risk", + BUSINESS_CONTRACT: "contract", + PROJECT_GROUP: "team", + SITE_SURVEY: "survey", + REPORT_PREPARATION: "report", + INTERNAL_REVIEW: "internal", + TECHNICAL_REVIEW: "technical", + PROCESS_CONTROL: "control", +}; + +const parseProjectNode = (data) => { + if (Array.isArray(data)) return data; + try { + return JSON.parse(data || "[]"); + } catch { + return []; + } +}; + +const buildProcessTabs = (projectNode) => { + const nodes = parseProjectNode(projectNode); + const nodeMap = {}; + nodes.forEach((n) => { + nodeMap[n.type] = n; + }); + + const allPrevDone = (idx) => { + for (let i = 0; i < idx; i++) { + const key = PROCESS_TABS[i].key; + const type = Object.keys(TYPE_TO_TAB_KEY).find( + (k) => TYPE_TO_TAB_KEY[k] === key, + ); + if (!type) continue; + const node = nodeMap[type]; + if (!node || node.state !== 1) return false; + } + return true; + }; + + return PROCESS_TABS.map((item, idx) => { + const type = Object.keys(TYPE_TO_TAB_KEY).find( + (k) => TYPE_TO_TAB_KEY[k] === item.key, + ); + const node = type ? nodeMap[type] : null; + + if (idx === 7) { + // 过程控制:前7个全部完成才解锁 + const unlocked = allPrevDone(7); + return { + ...item, + state: unlocked ? "待办理" : "前置未完成", + stateType: unlocked ? "default" : "warning", + locked: !unlocked, + }; + } + + if (node) { + return { + ...item, + state: node.state === 1 ? "已完成" : "待办理", + stateType: node.state === 1 ? "success" : "default", + locked: false, + }; + } + return { ...item, locked: false }; + }); +}; + const ProjectFlow = (props) => { const [activeKey, setActiveKey] = useState("risk"); const [visible, setVisible] = useState(false); - const { wpsDoc } = props.safetyEvalBusiness || {}; + const { wpsDoc, evalProjectDetailData } = props.safetyEvalBusiness || {}; const projectId = router.query?.id; + const processTabs = buildProcessTabs(evalProjectDetailData?.projectNode); + const handleTabChange = (key) => { if (key === "report") { openReportEditor(); @@ -103,29 +127,33 @@ const ProjectFlow = (props) => { setVisible(true); }; + const getDetail = () => { + props.evalProjectDetail({ + id: projectId, + }); + }; + useEffect(() => { props.queryMyOrCreateDoc({ docxUrl: "", }); - props.evalProjectDetail({ - id: projectId, - }); + getDetail(); }, []); - const tabItems = PROCESS_TABS.map((item) => { + const tabItems = processTabs.map((item) => { let children = null; switch (item.key) { case "risk": - children = ; + children = ; break; case "contract": - children = ; + children = ; break; case "team": - children = ; + children = ; break; case "survey": - children = ; + children = ; break; case "internal": diff --git a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.less b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.less index f59e03f..5ca438c 100644 --- a/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.less +++ b/src/pages/Container/SafetyEvalBusiness/ProjectFlow/index.less @@ -1,4 +1,50 @@ .pf { + // 区块头部 + &-section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + margin-bottom: 16px; + background: #fafcff; + border: 1px solid #e8edf5; + border-radius: 8px; + border-left: 4px solid #1677ff; + } + + // 模板下载卡片 + &-template-card { + background: #fafcff; + border: 1px dashed #1677ff; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + + &-template-left { + display: flex; + align-items: center; + gap: 10px; + } + + &-template-icon { + font-size: 22px; + } + + &-template-name { + font-size: 13px; + font-weight: 500; + color: #172033; + } + + &-template-desc { + font-size: 12px; + color: #999; + } + // 权限标签区域 &-permission { margin-bottom: 12px; diff --git a/src/pages/Container/SafetyEvalBusiness/RiskWarning/index.js b/src/pages/Container/SafetyEvalBusiness/RiskWarning/index.js index 0aceb0c..341100a 100644 --- a/src/pages/Container/SafetyEvalBusiness/RiskWarning/index.js +++ b/src/pages/Container/SafetyEvalBusiness/RiskWarning/index.js @@ -115,7 +115,7 @@ const RiskWarning = (props) => { width: 100, render: (code) => { const cfg = RISK_LEVEL_MAP[code]; - return cfg ? {cfg.text} : {code}; + return cfg ? {cfg.text} : ""; }, }, { title: "预警类型", dataIndex: "warningTypeName", width: 100 }, @@ -126,14 +126,14 @@ const RiskWarning = (props) => { width: 260, }, { title: "责任人", dataIndex: "responsiblePerson", width: 90 }, - { title: "预警时间", dataIndex: "warningTime", width: 110 }, + { title: "预警时间", dataIndex: "warningTime", width: 120 }, { title: "状态", dataIndex: "statusCode", width: 90, render: (code) => { const cfg = RISK_STATUS_MAP[code]; - return cfg ? {cfg.text} : {code}; + return cfg ? {cfg.text} : ""; }, }, { @@ -242,18 +242,7 @@ const RiskWarning = (props) => { onReset={handleReset} /> -
- - 处理按钮会打开处置面板;标记闭环会直接更新行状态。 - -
+