2026-06-26 17:28:06 +08:00
|
|
|
|
import { Button, Card, Form, Spin, Table, Tabs, Tag, message } from "antd";
|
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
import FormBuilder from "zy-react-library/components/FormBuilder";
|
2026-06-29 16:45:58 +08:00
|
|
|
|
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
2026-06-26 17:28:06 +08:00
|
|
|
|
import FilePreviewModal from "~/components/FilePreviewModal";
|
|
|
|
|
|
import {
|
|
|
|
|
|
fetchRegisteredOrgDetail,
|
|
|
|
|
|
fetchRegisteredOrgPersonnelList,
|
|
|
|
|
|
fetchRegisteredOrgQualificationGroups,
|
2026-07-07 17:13:33 +08:00
|
|
|
|
} from "~/utils/regulatorOrgInfo";
|
2026-06-26 17:28:06 +08:00
|
|
|
|
import { buildOrgInfoFormOptions } from "../../../../EnterpriseInfo/OrgInfo/formOptions";
|
2026-07-09 09:23:22 +08:00
|
|
|
|
import StaffViewModal from "~/components/StaffViewModal";
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
|
|
const LIST_PATH = "/container/supervision/basicInfo/registeredOrg/list";
|
|
|
|
|
|
const ORG_INFO_FORM_OPTIONS = buildOrgInfoFormOptions();
|
2026-07-13 11:14:27 +08:00
|
|
|
|
const GENDER_MAP = { 1: "男", 2: "女" };
|
|
|
|
|
|
|
|
|
|
|
|
/** 后端字段名 → 前端表单字段名映射 */
|
|
|
|
|
|
const BACKEND_TO_FORM_FIELD = {
|
|
|
|
|
|
unitName: "orgName",
|
|
|
|
|
|
creditCode: "creditCode",
|
|
|
|
|
|
safetyIndustryCategoryName: "safetyIndustryCategory",
|
|
|
|
|
|
districtName: "regionCountyName",
|
|
|
|
|
|
townStreet: "regionStreetName",
|
|
|
|
|
|
villageCommunity: "regionCommunityName",
|
|
|
|
|
|
longitude: "longitude",
|
|
|
|
|
|
latitude: "latitude",
|
|
|
|
|
|
registerAddress: "registerAddress",
|
|
|
|
|
|
businessAddress: "businessAddress",
|
|
|
|
|
|
ownershipTypeName: "ownershipType",
|
|
|
|
|
|
economyIndustryCode: "gbIndustryCode",
|
|
|
|
|
|
legalRepresentative: "legalRepresentative",
|
|
|
|
|
|
legalRepresentativePhone: "legalRepPhone",
|
|
|
|
|
|
principalName: "principal",
|
|
|
|
|
|
principalPhone: "principalPhone",
|
|
|
|
|
|
safetyDeptManager: "safetyDeptHead",
|
|
|
|
|
|
safetyDeptManagerPhone: "safetyDeptPhone",
|
|
|
|
|
|
safetyDeputyPhone: "safetyVpPhone",
|
|
|
|
|
|
productionDate: "productionDate",
|
|
|
|
|
|
businessStatusName: "businessStatus",
|
|
|
|
|
|
infoDisclosureUrl: "disclosureUrl",
|
|
|
|
|
|
workplaceArea: "workplaceArea",
|
|
|
|
|
|
archiveRoomArea: "archiveRoomArea",
|
|
|
|
|
|
fulltimeEvaluatorCount: "fullTimeEvaluatorCount",
|
|
|
|
|
|
registeredEngineerCount: "registeredSafetyEngineerCount",
|
|
|
|
|
|
enterpriseStatusName: "enterpriseStatus",
|
|
|
|
|
|
enterpriseScaleName: "enterpriseScale",
|
|
|
|
|
|
filingTypeName: "filingType",
|
|
|
|
|
|
filingRecordStatusName: "filingRecordStatus",
|
|
|
|
|
|
attachments: "attachments",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function toFormFields(backendData) {
|
|
|
|
|
|
if (!backendData) return {};
|
|
|
|
|
|
const result = {};
|
|
|
|
|
|
Object.entries(BACKEND_TO_FORM_FIELD).forEach(([backendKey, formKey]) => {
|
|
|
|
|
|
if (backendData[backendKey] !== undefined) {
|
|
|
|
|
|
result[formKey] = backendData[backendKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
2026-06-26 17:28:06 +08:00
|
|
|
|
|
|
|
|
|
|
function parseQueryId(location) {
|
|
|
|
|
|
const search = location?.search || window.location.search || "";
|
|
|
|
|
|
return new URLSearchParams(search).get("id");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 15:58:05 +08:00
|
|
|
|
function resolvePreviewUrl(raw) {
|
|
|
|
|
|
if (!raw) return "";
|
|
|
|
|
|
const u = String(raw);
|
|
|
|
|
|
if (/^https?:\/\//i.test(u)) return u;
|
|
|
|
|
|
const base = window.fileUrl || "";
|
|
|
|
|
|
return base ? `${base}${u}` : u;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isImageUrl(url) {
|
|
|
|
|
|
return /\.(jpe?g|png|gif|webp|bmp)(\?|#|$)/i.test(String(url || "").toLowerCase());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:28:06 +08:00
|
|
|
|
function MaterialsTab({ materialGroups, loading, onPreview }) {
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return <Spin />;
|
|
|
|
|
|
}
|
2026-07-13 11:14:27 +08:00
|
|
|
|
const groups = Object.entries(materialGroups || {}).map(([code, list], index) => {
|
|
|
|
|
|
const items = Array.isArray(list) ? list : [];
|
|
|
|
|
|
const scopeName = items[0]?.licenseTypeName || code || `资质${index + 1}`;
|
|
|
|
|
|
const expireDate = items.reduce((latest, item) => {
|
|
|
|
|
|
const d = item.validEndDate;
|
|
|
|
|
|
if (!d) return latest;
|
|
|
|
|
|
return !latest || d > latest ? d : latest;
|
|
|
|
|
|
}, "");
|
|
|
|
|
|
const expireWarning = expireDate
|
|
|
|
|
|
? (() => {
|
|
|
|
|
|
const end = new Date(String(expireDate).replace(/-/g, "/"));
|
|
|
|
|
|
if (Number.isNaN(end.getTime())) return false;
|
|
|
|
|
|
const diff = end.getTime() - Date.now();
|
|
|
|
|
|
return diff > 0 && diff < 90 * 24 * 3600 * 1000;
|
|
|
|
|
|
})()
|
|
|
|
|
|
: false;
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: code || String(index),
|
|
|
|
|
|
scopeName,
|
|
|
|
|
|
expireDate: expireDate || "-",
|
|
|
|
|
|
expireWarning,
|
|
|
|
|
|
materials: items.map((item, idx) => {
|
|
|
|
|
|
const certImgs = item.certImageUrl
|
|
|
|
|
|
? (Array.isArray(item.certImageUrl)
|
|
|
|
|
|
? item.certImageUrl
|
|
|
|
|
|
: String(item.certImageUrl).startsWith("[")
|
|
|
|
|
|
? (() => { try { return JSON.parse(item.certImageUrl); } catch { return [{ url: item.certImageUrl }]; } })()
|
|
|
|
|
|
: [{ url: item.certImageUrl }])
|
|
|
|
|
|
: item.certImgFiles || [];
|
|
|
|
|
|
const previewUrl = certImgs[0]?.url || "";
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: item.id || `${code}-${idx}`,
|
|
|
|
|
|
name: item.certName || item.licenseTypeName || "-",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
status: previewUrl ? "uploaded" : "pending",
|
|
|
|
|
|
previewUrl,
|
|
|
|
|
|
raw: item,
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-06-26 17:28:06 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ marginBottom: 12, color: "rgba(0,0,0,0.45)" }}>
|
|
|
|
|
|
该机构共申请
|
2026-07-13 11:14:27 +08:00
|
|
|
|
<strong>{groups.length}</strong>
|
2026-06-26 17:28:06 +08:00
|
|
|
|
个业务范围资质,以下为各范围对应的申请材料清单
|
|
|
|
|
|
</div>
|
2026-07-13 11:14:27 +08:00
|
|
|
|
{groups.map((group) => (
|
2026-06-26 17:28:06 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
key={group.id}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
title={`业务范围:${group.scopeName}`}
|
|
|
|
|
|
extra={(
|
|
|
|
|
|
<span style={{ fontSize: 12 }}>
|
|
|
|
|
|
证书有效期:
|
|
|
|
|
|
<span style={{ color: group.expireWarning ? "#faad14" : "#52c41a", fontWeight: 500 }}>
|
|
|
|
|
|
{group.expireDate}
|
|
|
|
|
|
{group.expireWarning ? "(即将到期)" : ""}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
pagination={false}
|
|
|
|
|
|
dataSource={group.materials}
|
|
|
|
|
|
columns={[
|
|
|
|
|
|
{ title: "材料名称", dataIndex: "name" },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Button type="link" size="small" onClick={() => onPreview(record)}>预览</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PersonnelTab({ personnel, loading, onView }) {
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return <Spin />;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Table
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
pagination={false}
|
|
|
|
|
|
dataSource={personnel}
|
|
|
|
|
|
columns={[
|
|
|
|
|
|
{ title: "序号", width: 60, render: (_, __, index) => index + 1 },
|
2026-07-13 11:14:27 +08:00
|
|
|
|
{ title: "姓名", dataIndex: "userName", width: 90 },
|
|
|
|
|
|
{ title: "性别", width: 60, render: (_, record) => GENDER_MAP[record.genderCode] || "-" },
|
|
|
|
|
|
{ title: "岗位", dataIndex: "postName", width: 110 },
|
2026-06-26 17:28:06 +08:00
|
|
|
|
{ title: "资质范围", dataIndex: "qualScope", width: 90 },
|
2026-07-13 11:14:27 +08:00
|
|
|
|
{ title: "学历", dataIndex: "educationName", width: 80 },
|
2026-06-26 17:28:06 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "注册安全工程师",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (_, record) => (
|
2026-07-13 11:14:27 +08:00
|
|
|
|
<Tag color={record.registerEngineerFlag === 1 ? "success" : "error"}>
|
|
|
|
|
|
{record.registerEngineerFlag === 1 ? "是" : "否"}
|
2026-06-26 17:28:06 +08:00
|
|
|
|
</Tag>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Button type="link" size="small" onClick={() => onView(record)}>查看</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function RegisteredOrgDetailPage(props) {
|
|
|
|
|
|
const id = useMemo(() => parseQueryId(props.location), [props.location?.search]);
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [orgLoading, setOrgLoading] = useState(Boolean(parseQueryId(props.location)));
|
|
|
|
|
|
const [extrasLoading, setExtrasLoading] = useState(Boolean(parseQueryId(props.location)));
|
|
|
|
|
|
const [orgName, setOrgName] = useState("");
|
2026-07-13 11:14:27 +08:00
|
|
|
|
const [materialGroups, setMaterialGroups] = useState({});
|
2026-06-26 17:28:06 +08:00
|
|
|
|
const [personnel, setPersonnel] = useState([]);
|
|
|
|
|
|
const [filePreview, setFilePreview] = useState(null);
|
|
|
|
|
|
const [viewPersonnelId, setViewPersonnelId] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!id) {
|
|
|
|
|
|
setOrgLoading(false);
|
|
|
|
|
|
setExtrasLoading(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
setOrgLoading(true);
|
|
|
|
|
|
setExtrasLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [orgRes, groups, staffList] = await Promise.all([
|
|
|
|
|
|
fetchRegisteredOrgDetail(id),
|
|
|
|
|
|
fetchRegisteredOrgQualificationGroups(id).catch((err) => {
|
|
|
|
|
|
console.warn("[RegisteredOrgDetail] qualification load failed:", err);
|
2026-07-08 20:55:23 +08:00
|
|
|
|
|
2026-07-13 11:14:27 +08:00
|
|
|
|
return { data: {} };
|
2026-06-26 17:28:06 +08:00
|
|
|
|
}),
|
|
|
|
|
|
fetchRegisteredOrgPersonnelList(id).catch((err) => {
|
|
|
|
|
|
console.warn("[RegisteredOrgDetail] personnel load failed:", err);
|
2026-07-08 20:55:23 +08:00
|
|
|
|
|
2026-06-26 17:28:06 +08:00
|
|
|
|
return [];
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (cancelled) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (orgRes?.data) {
|
2026-07-13 11:14:27 +08:00
|
|
|
|
form.setFieldsValue(toFormFields(orgRes.data));
|
|
|
|
|
|
setOrgName(orgRes.data.unitName || "");
|
2026-06-26 17:28:06 +08:00
|
|
|
|
}
|
2026-07-13 11:14:27 +08:00
|
|
|
|
setMaterialGroups(groups?.data || {});
|
2026-06-26 17:28:06 +08:00
|
|
|
|
setPersonnel(staffList || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (err) {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
console.warn("[RegisteredOrgDetail] load failed:", err);
|
|
|
|
|
|
message.error("加载机构详情失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setOrgLoading(false);
|
|
|
|
|
|
setExtrasLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
|
|
|
|
|
const goBack = () => {
|
|
|
|
|
|
if (props.history?.goBack && props.history.length > 1) {
|
|
|
|
|
|
props.history.goBack();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.history?.push) {
|
|
|
|
|
|
props.history.push(LIST_PATH);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
window.location.href = `/certificate${LIST_PATH}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!id) {
|
|
|
|
|
|
return (
|
2026-06-29 16:45:58 +08:00
|
|
|
|
<PageLayout title="备案信息详情" extra={<Button onClick={goBack}>返回</Button>}>
|
|
|
|
|
|
<p style={{ margin: 0, marginBottom: 24, color: "rgba(0, 0, 0, 0.45)" }}>
|
|
|
|
|
|
缺少机构 ID 参数
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</PageLayout>
|
2026-06-26 17:28:06 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tabItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "info",
|
|
|
|
|
|
label: "备案信息",
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<Spin spinning={orgLoading}>
|
|
|
|
|
|
<FormBuilder
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
span={12}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
useAutoGenerateRequired={false}
|
|
|
|
|
|
options={ORG_INFO_FORM_OPTIONS}
|
|
|
|
|
|
labelCol={{ span: 10 }}
|
|
|
|
|
|
showActionButtons={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "materials",
|
|
|
|
|
|
label: "申请清单材料",
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<MaterialsTab
|
|
|
|
|
|
materialGroups={materialGroups}
|
|
|
|
|
|
loading={extrasLoading}
|
2026-07-07 15:58:05 +08:00
|
|
|
|
onPreview={(record) => {
|
|
|
|
|
|
const url = resolvePreviewUrl(record?.previewUrl);
|
|
|
|
|
|
if (!url) {
|
|
|
|
|
|
message.warning("该材料暂无可预览的附件");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isImageUrl(url)) {
|
|
|
|
|
|
setFilePreview(record);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.open(url, "_blank");
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2026-06-26 17:28:06 +08:00
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "personnel",
|
|
|
|
|
|
label: "人员信息",
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<PersonnelTab
|
|
|
|
|
|
personnel={personnel}
|
|
|
|
|
|
loading={extrasLoading}
|
|
|
|
|
|
onView={(record) => setViewPersonnelId(record.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-08 20:55:23 +08:00
|
|
|
|
<PageLayout title="备案信息详情" history={props.history} previous>
|
2026-06-29 16:45:58 +08:00
|
|
|
|
<p style={{ margin: 0, marginBottom: 24, color: "rgba(0, 0, 0, 0.45)" }}>
|
|
|
|
|
|
{orgName || undefined}
|
|
|
|
|
|
</p>
|
2026-06-26 17:28:06 +08:00
|
|
|
|
<Tabs items={tabItems} />
|
|
|
|
|
|
|
|
|
|
|
|
<FilePreviewModal
|
|
|
|
|
|
open={!!filePreview}
|
|
|
|
|
|
title="文件预览"
|
|
|
|
|
|
fileName={filePreview?.name}
|
2026-07-07 15:58:05 +08:00
|
|
|
|
url={resolvePreviewUrl(filePreview?.previewUrl)}
|
2026-06-26 17:28:06 +08:00
|
|
|
|
onCancel={() => setFilePreview(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{viewPersonnelId && (
|
|
|
|
|
|
<StaffViewModal
|
|
|
|
|
|
open={!!viewPersonnelId}
|
|
|
|
|
|
currentId={viewPersonnelId}
|
2026-07-08 22:33:22 +08:00
|
|
|
|
|
2026-06-26 17:28:06 +08:00
|
|
|
|
onCancel={() => setViewPersonnelId("")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-29 16:45:58 +08:00
|
|
|
|
</PageLayout>
|
2026-06-26 17:28:06 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default RegisteredOrgDetailPage;
|