549 lines
19 KiB
JavaScript
549 lines
19 KiB
JavaScript
import React, { useState, useMemo } from "react";
|
||
import { Button, Image, Input, Modal, Select, Table, Tabs, Tag } from "antd";
|
||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||
import { NS_QUAL_REVIEW } from "~/enumerate/namespace";
|
||
import StaffViewModal from "~/components/StaffViewModal";
|
||
import PreviewUrlButton from "~/components/PreviewUrlButton/index";
|
||
import { QUALIFICATION_INDUSTRY_OPTIONS, QUALIFICATION_INDUSTRY_OPTIONS_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 : "-";
|
||
}
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 设备详情弹窗(原型 modal-equip-view 缺失,按列表字段补全)
|
||
*/
|
||
function EquipViewModal({ record, onCancel }) {
|
||
const open = !!record;
|
||
const calibration = record ? getCalibrationTag(record) : null;
|
||
const rows = record
|
||
? [
|
||
{ label: "装备名称", value: record.deviceName },
|
||
{ label: "规格型号", value: record.deviceModel },
|
||
{ label: "生产厂家", value: record.manufacturer },
|
||
{ label: "设备价值(元)", value: record.equipmentValue },
|
||
{ label: "计量检定情况", value: null },
|
||
]
|
||
: [];
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
title="装备详情"
|
||
width={560}
|
||
cancelText="关闭"
|
||
okText="关闭"
|
||
onOk={onCancel}
|
||
onCancel={onCancel}
|
||
footer={
|
||
<Button type="primary" onClick={onCancel}>
|
||
关闭
|
||
</Button>
|
||
}
|
||
>
|
||
{record && (
|
||
<div style={gridStyle}>
|
||
{rows.map((row) =>
|
||
row.value != null ? (
|
||
<ReadonlyField key={row.label} label={row.label} value={row.value} />
|
||
) : (
|
||
<div key={row.label} style={fieldStyle}>
|
||
<label style={labelStyle}>{row.label}</label>
|
||
<div>
|
||
{calibration && (
|
||
<Tag
|
||
color={calibration.status === "success" ? "success" : "warning"}
|
||
>
|
||
{calibration.label}
|
||
</Tag>
|
||
)}
|
||
</div>
|
||
</div>
|
||
),
|
||
)}
|
||
<div style={{ ...fieldStyle, ...fullSpan }}>
|
||
<label style={labelStyle}>检定报告</label>
|
||
<div>
|
||
{record.calibrationReportUrl ? (
|
||
<PreviewUrlButton url={record.calibrationReportUrl}>预览检定报告</PreviewUrlButton>
|
||
) : (
|
||
<span style={{ color: "#64748b", fontSize: "0.8rem" }}>未上传</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
const FilingTabs = ({
|
||
detail = {},
|
||
isReview = false,
|
||
compliance = {},
|
||
onComplianceChange,
|
||
}) => {
|
||
const [viewId, setViewId] = useState("");
|
||
const [equipRecord, setEquipRecord] = useState(null);
|
||
const [previewImage, setPreviewImage] = useState("");
|
||
|
||
const personnelRows = useMemo(
|
||
() =>
|
||
(detail.personnelList || []).map((item) => ({
|
||
...item,
|
||
age: item.age ?? calcAge(item.birthDate),
|
||
})),
|
||
[detail.personnelList],
|
||
);
|
||
|
||
/** 管理人员:岗位含"法定代表人/负责人";无匹配时回退展示全部人员 */
|
||
const managerRows = useMemo(() => {
|
||
const filtered = personnelRows.filter((item) =>
|
||
/法定代表人|技术负责人|过程控制负责人|负责人/.test(item.positionName || ""),
|
||
);
|
||
return filtered.length ? filtered : personnelRows;
|
||
}, [personnelRows]);
|
||
|
||
/** 专职安全评价师:评价师证书号存在或岗位含"评价师";无匹配时回退展示全部人员 */
|
||
const evaluatorRows = useMemo(() => {
|
||
const filtered = personnelRows.filter(
|
||
(item) => item.evaluatorCertNo || /评价师/.test(item.positionName || ""),
|
||
);
|
||
return filtered.length ? filtered : personnelRows;
|
||
}, [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 openPreview = (file) => {
|
||
const url = file?.url;
|
||
if (!url) return;
|
||
if (/\.(jpe?g|png|gif|webp|bmp)(\?|#|$)/i.test(`${file.name || ""} ${url}`.toLowerCase())) {
|
||
setPreviewImage(url);
|
||
} else {
|
||
window.open(url, "_blank");
|
||
}
|
||
};
|
||
|
||
/** 人员表格列(管理/评价师共用基础) */
|
||
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,
|
||
render: (_, r) =>
|
||
[r.educationLevelName, r.majorName || r.educationTypeName].filter(Boolean).join(" / ") || "-",
|
||
},
|
||
{ title: "职称", dataIndex: "titleName", width: 120, render: (v) => v || "-" },
|
||
{
|
||
title: "从业年限",
|
||
width: 100,
|
||
render: (_, r) => r.workYears || r.yearsOfWorking || "-",
|
||
},
|
||
{
|
||
title: "专业能力",
|
||
width: 180,
|
||
render: (_, r) => {
|
||
const codes = r.professionalCapabilityCodeList || [];
|
||
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>
|
||
<ReadonlyField label="法定代表人 *" value={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 ? (
|
||
<>
|
||
<Button
|
||
size="small"
|
||
onClick={() => openPreview(attachmentList[0])}
|
||
>
|
||
查看营业执照
|
||
</Button>
|
||
<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={() => setEquipRecord(record)}>
|
||
查看
|
||
</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 ? (
|
||
<>
|
||
<Button
|
||
size="small"
|
||
onClick={() =>
|
||
openPreview({
|
||
url: commitment.legalRepSignatureUrl,
|
||
name: "机构负责人签字件",
|
||
})
|
||
}
|
||
>
|
||
查看签字件
|
||
</Button>
|
||
<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>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<>
|
||
<Tabs items={items} />
|
||
|
||
<StaffViewModal
|
||
open={!!viewId}
|
||
currentId={viewId}
|
||
onCancel={() => setViewId("")}
|
||
/>
|
||
|
||
<EquipViewModal record={equipRecord} onCancel={() => setEquipRecord(null)} />
|
||
|
||
<Image
|
||
src={previewImage}
|
||
preview={{
|
||
visible: !!previewImage,
|
||
onVisibleChange: (v) => !v && setPreviewImage(""),
|
||
}}
|
||
style={{ display: "none" }}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Connect([NS_QUAL_REVIEW], true)(FilingTabs);
|