555 lines
20 KiB
JavaScript
555 lines
20 KiB
JavaScript
import React, { useState, useMemo } from "react";
|
||
import { Button, Image, Input, Select, Table, Tabs, Tag } from "antd";
|
||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||
import { NS_QUAL_REVIEW, NS_EQUIP_INFO } from "~/enumerate/namespace";
|
||
import StaffViewModal from "~/components/StaffViewModal";
|
||
import PreviewUrlButton from "~/components/PreviewUrlButton/index";
|
||
import { EquipViewModal } from "~/pages/Container/EnterpriseInfo/EquipInfo";
|
||
import {
|
||
QUALIFICATION_INDUSTRY_OPTIONS,
|
||
QUALIFICATION_INDUSTRY_OPTIONS_MAP,
|
||
TITLE_LEVEL_MAP,
|
||
} from "~/enumerate/enterpriseOptions";
|
||
import { CAPABILITY_MAP } from "~/enumerate/constant";
|
||
import { getCalibrationTag } from "../mockData";
|
||
|
||
const { TextArea } = Input;
|
||
|
||
/**
|
||
* 共享组件:资质备案初审 6 个标签页(详情 / 审核共用)
|
||
* 对应原型 mod-qual-review-form / mod-qual-review-detail:
|
||
* 1. 申请基本信息 2. 管理人员 3. 专职安全评价师
|
||
* 4. 申请材料 5. 设备清单 6. 机构负责人签字
|
||
* @param {Object} props
|
||
* @param {Object} props.detail - 备案详情数据
|
||
* @param {boolean} [props.isReview=false] - 审核模式(材料表格开启"符合性"列)
|
||
* @param {Object} [props.compliance={}] - 审核模式下的符合性状态
|
||
* @param {Function} [props.onComplianceChange] - 符合性变动回调
|
||
*/
|
||
function calcAge(birthDate) {
|
||
if (!birthDate) return "-";
|
||
const birth = new Date(birthDate);
|
||
if (isNaN(birth.getTime())) return "-";
|
||
const now = new Date();
|
||
let age = now.getFullYear() - birth.getFullYear();
|
||
const m = now.getMonth() - birth.getMonth();
|
||
if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
|
||
return age >= 0 ? age : "-";
|
||
}
|
||
|
||
/**
|
||
* 职称名称:后端 QualFilingPersonnelInfoCO 返回 titleCodeArr(编码数组),
|
||
* 转中文名称展示(高级/中级/初级)。变更时间:2026-08-10(对齐后端字段)
|
||
*/
|
||
function titleNames(titleCodeArr) {
|
||
// 兼容:初审返回数组,备案变更返回字符串(可能为 "SENIOR" / "SENIOR,MIDDLE" / JSON 数组字符串)。变更时间:2026-08-10
|
||
let list = Array.isArray(titleCodeArr) ? titleCodeArr : [];
|
||
if (!Array.isArray(titleCodeArr) && typeof titleCodeArr === "string") {
|
||
try {
|
||
const parsed = JSON.parse(titleCodeArr);
|
||
list = Array.isArray(parsed) ? parsed : [titleCodeArr];
|
||
} catch {
|
||
list = String(titleCodeArr)
|
||
.split(/[,,、]/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
}
|
||
}
|
||
const names = list
|
||
.map((item) => {
|
||
const code = typeof item === "string" ? item : item?.titleCode || item?.title;
|
||
return item?.titleName || TITLE_LEVEL_MAP[code] || code || "";
|
||
})
|
||
.filter(Boolean);
|
||
// 空数组返回空字符串,便于调用方回退 titleName 等字段
|
||
return names.length ? names.join("、") : "";
|
||
}
|
||
|
||
/**
|
||
* 从业年限:后端返回 joinWorkDate(参加工作时间),按当前日期推算年数。
|
||
* 变更时间:2026-08-10(对齐后端字段)
|
||
*/
|
||
function workYearsText(joinWorkDate) {
|
||
if (!joinWorkDate) return "-";
|
||
const d = new Date(String(joinWorkDate).replace(/-/g, "/"));
|
||
if (Number.isNaN(d.getTime())) return "-";
|
||
const now = new Date();
|
||
let years = now.getFullYear() - d.getFullYear();
|
||
const m = now.getMonth() - d.getMonth();
|
||
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) years--;
|
||
return years >= 0 ? `${years}年` : "-";
|
||
}
|
||
|
||
const gridStyle = {
|
||
display: "grid",
|
||
gridTemplateColumns: "1fr 1fr",
|
||
gap: "0.6rem 1rem",
|
||
};
|
||
|
||
const fieldStyle = { display: "flex", flexDirection: "column", gap: "0.3rem" };
|
||
const labelStyle = { fontSize: "0.8rem", color: "#64748b", fontWeight: 500 };
|
||
const inputStyle = { background: "#f8fafc" };
|
||
const fullSpan = { gridColumn: "1 / -1" };
|
||
|
||
/** 只读字段 */
|
||
function ReadonlyField({ label, value, span }) {
|
||
return (
|
||
<div style={span ? { ...fieldStyle, ...span } : fieldStyle}>
|
||
<label style={labelStyle}>{label}</label>
|
||
<Input readOnly value={value || "-"} style={inputStyle} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const FilingTabs = ({
|
||
detail = {},
|
||
isReview = false,
|
||
compliance = {},
|
||
onComplianceChange,
|
||
/**
|
||
* 额外 Tab(如机构备案详情「7. 变更记录」),数组元素与 antd Tabs items 同构。
|
||
* 不传时保持原 6 Tab,不影响初审/确认/专家核验展示。变更时间:2026-08-10
|
||
*/
|
||
extraTabs,
|
||
equipInfoGet,
|
||
}) => {
|
||
const [viewId, setViewId] = useState("");
|
||
const [equipViewOpen, setEquipViewOpen] = useState(false);
|
||
const [equipId, setEquipId] = useState("");
|
||
|
||
const personnelRows = useMemo(
|
||
() =>
|
||
(detail.personnelList || []).map((item) => ({
|
||
...item,
|
||
age: item.age ?? calcAge(item.birthDate),
|
||
})),
|
||
[detail.personnelList],
|
||
);
|
||
|
||
/**
|
||
* 管理人员判定:technicalDirectorFlag(技术负责人) 或 processControlLeaderFlag(过程控制负责人) 为 true;
|
||
* 法定代表人/负责人按岗位名兜底归入管理人员。
|
||
* 备案变更详情人员 CO(QualFilingPersonnelChangeInfoCO)暂未返回 flag 字段,靠岗位名兜底判断(待后端补充 flag)。
|
||
* 变更时间:2026-08-10(按后端人员标识字段拆分)
|
||
*/
|
||
const isManager = (item) =>
|
||
item.technicalDirectorFlag === true ||
|
||
item.processControlLeaderFlag === true ||
|
||
/法定代表人|负责人/.test(item.positionName || "");
|
||
|
||
const managerRows = useMemo(() => {
|
||
return personnelRows.filter(isManager);
|
||
}, [personnelRows]);
|
||
|
||
/** 专职安全评价师:非管理人员的其余人员 */
|
||
const evaluatorRows = useMemo(() => {
|
||
return personnelRows.filter((item) => !isManager(item));
|
||
}, [personnelRows]);
|
||
|
||
const attachmentList = useMemo(() => {
|
||
const v = detail.attachmentUrl;
|
||
if (!v) return [];
|
||
try {
|
||
const p = JSON.parse(v);
|
||
return Array.isArray(p) ? p : [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}, [detail.attachmentUrl]);
|
||
|
||
const businessScope = useMemo(() => {
|
||
const list = Array.isArray(detail.businessScope) ? detail.businessScope : [];
|
||
return QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => ({
|
||
...opt,
|
||
checked: list.includes(opt.value),
|
||
}));
|
||
}, [detail.businessScope]);
|
||
|
||
/** 人员表格列(管理/评价师共用基础) */
|
||
const basePersonCols = (withScope) => [
|
||
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
|
||
{ title: "姓名", dataIndex: "personName", width: 110 },
|
||
{ title: "岗位类别", dataIndex: "positionName", width: 140, ellipsis: true },
|
||
{
|
||
title: "学历/专业",
|
||
width: 160,
|
||
// 后端字段:educationName(学历) / basicDisciplineMajorName(专业);变更时间:2026-08-10
|
||
render: (_, r) =>
|
||
[r.educationName, r.basicDisciplineMajorName].filter(Boolean).join(" / ") || "-",
|
||
},
|
||
{
|
||
title: "职称",
|
||
width: 120,
|
||
render: (_, r) => titleNames(r.titleCodeArr) || r.titleName || "-",
|
||
},
|
||
{
|
||
title: "从业年限",
|
||
width: 100,
|
||
render: (_, r) =>
|
||
r.workYears || r.yearsOfWorking || workYearsText(r.joinWorkDate),
|
||
},
|
||
{
|
||
title: "专业能力",
|
||
width: 180,
|
||
render: (_, r) => {
|
||
// 能力编码:兼容 professionalCapabilityCodeList 与后端 capabilityAssessment;变更时间:2026-08-10
|
||
// 备案变更返回 capabilityAssessment 字符串(JSON 数组),需解析后映射
|
||
let capList = r.capabilityAssessment;
|
||
if (typeof capList === "string") {
|
||
try {
|
||
const parsed = JSON.parse(capList);
|
||
capList = Array.isArray(parsed) ? parsed : [];
|
||
} catch {
|
||
capList = [];
|
||
}
|
||
}
|
||
const codes =
|
||
r.professionalCapabilityCodeList ||
|
||
(Array.isArray(capList)
|
||
? capList.map((c) => (typeof c === "string" ? c : c?.professionalCapabilityCode)).filter(Boolean)
|
||
: []) ||
|
||
[];
|
||
return codes.length
|
||
? codes.map((code) => CAPABILITY_MAP[code] || code).join("、")
|
||
: "-";
|
||
},
|
||
},
|
||
...(withScope
|
||
? [
|
||
{
|
||
title: "申请业务范围",
|
||
width: 200,
|
||
ellipsis: true,
|
||
render: (_, r) => {
|
||
const scopes = r.appliedScope || r.scopeList;
|
||
if (Array.isArray(scopes)) {
|
||
return scopes.map((s) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[s] || s).join("、");
|
||
}
|
||
return scopes || "-";
|
||
},
|
||
},
|
||
]
|
||
: []),
|
||
{
|
||
title: "操作",
|
||
width: 80,
|
||
fixed: "right",
|
||
render: (_, record) => (
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => setViewId(record.sourcePersonnelId || record.id)}
|
||
>
|
||
查看
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
const materialCols = isReview
|
||
? [
|
||
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
|
||
{ title: "申请材料", dataIndex: "materialContent", ellipsis: true },
|
||
{ title: "格式", dataIndex: "materialFormat", width: 80 },
|
||
{
|
||
title: "状态",
|
||
width: 100,
|
||
render: (_, record) =>
|
||
record.uploadStatusCode === 2 ? (
|
||
<Tag color="success">{record.uploadStatusName || "已上传"}</Tag>
|
||
) : (
|
||
<Tag color="warning">{record.uploadStatusName || "待上传"}</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: "符合性",
|
||
width: 110,
|
||
render: (_, record) => (
|
||
<Select
|
||
style={{ width: "100%", fontSize: "0.75rem" }}
|
||
size="small"
|
||
placeholder="请选择"
|
||
value={compliance[record.id]}
|
||
allowClear
|
||
defaultValue="pass"
|
||
onChange={(v) => onComplianceChange?.(record.id, v)}
|
||
>
|
||
<Select.Option value="pass">符合</Select.Option>
|
||
<Select.Option value="fail">不符合</Select.Option>
|
||
</Select>
|
||
),
|
||
},
|
||
{
|
||
title: "操作",
|
||
width: 80,
|
||
render: (_, record) => (
|
||
<PreviewUrlButton url={record.attachmentUrl}>预览</PreviewUrlButton>
|
||
),
|
||
},
|
||
]
|
||
: [
|
||
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
|
||
{ title: "申请材料", dataIndex: "materialContent", ellipsis: true },
|
||
{ title: "格式", dataIndex: "materialFormat", width: 80 },
|
||
{
|
||
title: "操作",
|
||
width: 80,
|
||
render: (_, record) => (
|
||
<PreviewUrlButton url={record.attachmentUrl}>预览</PreviewUrlButton>
|
||
),
|
||
},
|
||
];
|
||
|
||
const commitment = detail.commitment || {};
|
||
|
||
const items = [
|
||
{
|
||
key: "info",
|
||
label: "1. 申请基本信息",
|
||
children: (
|
||
<div style={gridStyle}>
|
||
<div style={{ ...fieldStyle, ...fullSpan }}>
|
||
<label style={labelStyle}>拟申请的法定安全评价业务范围 *</label>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
gap: "0.3rem 1rem",
|
||
padding: "0.45rem 0.75rem",
|
||
border: "1px solid #d9d9d9",
|
||
borderRadius: 6,
|
||
background: "#f8fafc",
|
||
}}
|
||
>
|
||
{businessScope.map((opt) => (
|
||
<label
|
||
key={opt.value}
|
||
style={{ fontSize: "0.82rem", color: "#1e293b", cursor: "not-allowed" }}
|
||
>
|
||
<input type="checkbox" checked={opt.checked} disabled /> {opt.label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<ReadonlyField label="备案属地 *" value={detail.filingTerritoryName} />
|
||
<ReadonlyField label="备案单位 *" value={detail.filingUnitName} />
|
||
<ReadonlyField label="备案单位类型 *" value={detail.filingUnitTypeName} />
|
||
<ReadonlyField label="统一社会信用代码 *" value={detail.creditCode} />
|
||
<ReadonlyField label="注册地址 *" value={detail.registerAddress} span={fullSpan} />
|
||
<ReadonlyField label="办公地址 *" value={detail.officeAddress} span={fullSpan} />
|
||
<ReadonlyField label="信息公开网址 *" value={detail.infoDisclosureUrl} />
|
||
<div style={fieldStyle}>
|
||
<label style={labelStyle}>资质证书编号 *</label>
|
||
<Input readOnly value={detail.qualCertNo || "-"} style={inputStyle} />
|
||
<span style={{ color: "#8b95a5", fontSize: "0.72rem" }}>
|
||
初次申请无需填写,已有资质备案时填写
|
||
</span>
|
||
</div>
|
||
{/* 法定代表人:后端 CO 字段为 legalRepresentative(表 legal_representative);变更时间:2026-08-10 */}
|
||
<ReadonlyField
|
||
label="法定代表人 *"
|
||
value={detail.legalRepresentative || detail.legalPersonName || detail.legalPerson}
|
||
/>
|
||
<ReadonlyField label="法定代表人电话 *" value={detail.legalPersonPhone} />
|
||
<ReadonlyField label="传真 *" value={detail.fax} />
|
||
<ReadonlyField label="联系人及电话 *" value={detail.contactPhone} />
|
||
<ReadonlyField label="固定资产总值(万元) *" value={detail.fixedAssetAmount} />
|
||
<ReadonlyField label="工作场所建筑面积(㎡) *" value={detail.workplaceArea} />
|
||
<ReadonlyField label="档案室面积(㎡) *" value={detail.archiveRoomArea} />
|
||
<ReadonlyField label="专职安全评价师数量 *" value={detail.fulltimeEvaluatorCount} />
|
||
<ReadonlyField label="注册安全工程师数量 *" value={detail.registeredEngineerCount} />
|
||
<div style={{ ...fieldStyle, ...fullSpan }}>
|
||
<label style={labelStyle}>单位基本情况介绍(可附页) *</label>
|
||
<TextArea readOnly rows={5} value={detail.unitIntro || ""} style={inputStyle} />
|
||
</div>
|
||
<div style={{ ...fieldStyle, ...fullSpan }}>
|
||
<label style={labelStyle}>营业执照</label>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||
{attachmentList.length ? (
|
||
<>
|
||
<PreviewUrlButton url={attachmentList[0].url} size="small">
|
||
查看营业执照
|
||
</PreviewUrlButton>
|
||
<span style={{ color: "#8b95a5", fontSize: "0.75rem" }}>
|
||
{attachmentList[0].name || "营业执照.pdf"}
|
||
</span>
|
||
</>
|
||
) : (
|
||
<span style={{ color: "#8b95a5", fontSize: "0.8rem" }}>—</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "management",
|
||
label: "2. 管理人员基本情况汇总表",
|
||
children: (
|
||
<Table
|
||
size="small"
|
||
bordered
|
||
rowKey="id"
|
||
pagination={false}
|
||
dataSource={managerRows}
|
||
scroll={{ x: 900 }}
|
||
columns={basePersonCols(false)}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: "evaluators",
|
||
label: "3. 专职安全评价师基本情况汇总表",
|
||
children: (
|
||
<Table
|
||
size="small"
|
||
bordered
|
||
rowKey="id"
|
||
pagination={false}
|
||
dataSource={evaluatorRows}
|
||
scroll={{ x: 1100 }}
|
||
columns={basePersonCols(true)}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: "materials",
|
||
label: "4. 申请材料",
|
||
children: (
|
||
<Table
|
||
size="small"
|
||
bordered
|
||
rowKey="id"
|
||
pagination={false}
|
||
dataSource={Array.isArray(detail.materials) ? detail.materials : []}
|
||
columns={materialCols}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: "equipment",
|
||
label: "5. 设备清单",
|
||
children: (
|
||
<Table
|
||
size="small"
|
||
bordered
|
||
rowKey="id"
|
||
pagination={false}
|
||
dataSource={Array.isArray(detail.equipmentList) ? detail.equipmentList : []}
|
||
columns={[
|
||
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
|
||
{ title: "装备名称", dataIndex: "deviceName", ellipsis: true },
|
||
{ title: "规格型号", dataIndex: "deviceModel", ellipsis: true },
|
||
{ title: "生产厂家", dataIndex: "manufacturer" },
|
||
{
|
||
title: "计量检定情况",
|
||
width: 120,
|
||
render: (_, record) => {
|
||
const tag = getCalibrationTag(record);
|
||
return (
|
||
<Tag color={tag.status === "success" ? "success" : "warning"}>
|
||
{tag.label}
|
||
</Tag>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: "操作",
|
||
width: 80,
|
||
render: (_, record) => (
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => {
|
||
setEquipId(record.sourceEquipmentId);
|
||
setEquipViewOpen(true);
|
||
}}
|
||
>
|
||
查看
|
||
</Button>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: "signature",
|
||
label: "6. 机构负责人签字",
|
||
children: (
|
||
<div
|
||
style={{
|
||
border: "1px solid #d9d9d9",
|
||
borderRadius: 8,
|
||
padding: "1.25rem 1.5rem",
|
||
background: "#fafafa",
|
||
fontSize: "0.85rem",
|
||
lineHeight: 1.9,
|
||
}}
|
||
>
|
||
<p style={{ textAlign: "center", fontWeight: 600, fontSize: "0.95rem", marginBottom: "0.75rem" }}>
|
||
机构负责人签字确认
|
||
</p>
|
||
<p>
|
||
机构端已完成资质申请第六项负责人签字,确认本次提交的基础信息、管理人员、专职安全评价师、申请材料和设备清单真实、准确、完整。
|
||
</p>
|
||
<div style={gridStyle}>
|
||
<ReadonlyField label="签字人" value={commitment.legalRepName} />
|
||
<ReadonlyField label="职务" value="法定代表人 / 机构负责人" />
|
||
<ReadonlyField label="签字方式" value="APP认证签字" />
|
||
<ReadonlyField label="签字时间" value={commitment.signDate} />
|
||
<div style={{ ...fieldStyle, ...fullSpan }}>
|
||
<label style={labelStyle}>签字文件</label>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||
{commitment.legalRepSignatureUrl ? (
|
||
<>
|
||
<PreviewUrlButton url={commitment.legalRepSignatureUrl} size="small">
|
||
查看签字件
|
||
</PreviewUrlButton>
|
||
<Tag color="success">已签字</Tag>
|
||
</>
|
||
) : (
|
||
<Tag color="warning">未签字</Tag>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{commitment.legalRepSignatureUrl && (
|
||
<Image
|
||
src={commitment.legalRepSignatureUrl}
|
||
style={{ width: 120, height: 120, objectFit: "contain", marginTop: 8 }}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
...(Array.isArray(extraTabs) ? extraTabs : []),
|
||
];
|
||
|
||
return (
|
||
<>
|
||
<Tabs items={items} />
|
||
|
||
<StaffViewModal
|
||
open={!!viewId}
|
||
currentId={viewId}
|
||
onCancel={() => setViewId("")}
|
||
/>
|
||
|
||
{equipViewOpen && (
|
||
<EquipViewModal
|
||
open={equipViewOpen}
|
||
currentId={equipId}
|
||
equipInfoGet={equipInfoGet}
|
||
onCancel={() => {
|
||
setEquipViewOpen(false);
|
||
setEquipId("");
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Connect([NS_QUAL_REVIEW, NS_EQUIP_INFO], true)(FilingTabs);
|