safety-eval-website/src/pages/Container/RegisterMore/index.js

897 lines
38 KiB
JavaScript
Raw Normal View History

2026-07-09 16:54:59 +08:00
import React, { useEffect, useState } from "react";
2026-07-09 10:05:52 +08:00
import { Connect } from "@cqsjjb/jjb-dva-runtime";
2026-08-08 14:18:54 +08:00
import { message, Empty } from "antd";
2026-07-09 10:05:52 +08:00
import { NS_REGISTER } from "~/enumerate/namespace";
import auditLookImage from "~/enumerate/img/auditLook.png";
import auditBkImage from "~/enumerate/img/audit.png";
import successIcon from "~/enumerate/img/successIcon.png";
2026-08-08 14:18:54 +08:00
import { normalizeUrl } from "~/utils/validators";
import { resolveRegisterQueryParams } from "~/utils/resolveRegisterQueryParams";
2026-07-09 10:05:52 +08:00
import "./index.less";
2026-08-08 14:18:54 +08:00
// ===== 2026-08-08 依据原型 V1.6 register.html「认证信息填报页」1:1 还原 =====
// 变更原因:原型为原生 HTML/CSS 页面,需按原型的样式、布局、边框、字体、颜色、按钮、响应式做 1:1 还原。
// 变更逻辑:
// 1) 前端改用原生受控表单formData state + 原生 input/textarea/checkbox + 自定义校验),
// 字段名直接对齐后端 OrgInfo 字段,规避二次映射;
// 2) 布局采用原型 .grid 两列 + .wide 全宽字段,营业执照/拟申请业务范围/单位基本情况介绍单独成行居左;
// 3) 所有字段均加红星必填标注(含资质证书编号);
// 4) 边框容器 .card 居中、内容居左;保留 Steps 三步流程与 orgInfoSave/syncUserToGBS 提交逻辑。
// 变更理由:最小侵入,样式与原型一一对应;响应式 @media(max-width:760px) 单列适配。
2026-08-08 14:41:32 +08:00
// 业务范围选项(与后端枚举 code 严格匹配OrgBusinessScopeDeviceRefDO 等定义的行业 code
// 2026-08-08 改为 {value,label}value 用后端枚举 code存储/渲染的值label 仅作展示,
// 不再以中文名作为存储值,保证与后端枚举一致。
2026-08-08 14:18:54 +08:00
const BUSINESS_SCOPE_OPTIONS = [
2026-08-08 14:41:32 +08:00
{ value: "COAL_MINING", label: "煤炭开采业" },
{ value: "METAL_NONMETAL_MINING", label: "金属、非金属矿及其他矿采选业" },
{ value: "ONSHORE_OIL_GAS", label: "陆地石油和天然气开采业" },
{ value: "ONSHORE_OIL_GAS_PIPELINE", label: "陆上油气管道运输业" },
{
value: "PETROCHEMICAL_CHEMICAL_PHARMACEUTICAL",
label: "石油加工业,化学原料、化学品及医药制造业",
},
{ value: "FIREWORKS_FIRECRACKERS", label: "烟花爆竹制造业" },
{ value: "METAL_SMELTING", label: "金属冶炼" },
2026-08-08 14:18:54 +08:00
];
2026-08-08 14:41:32 +08:00
// 2026-08-08 业务范围值清洗:兼容历史已存的中文 label提交/回显时统一转换为后端枚举 code。
// 原因:早期数据 apply_business_scope 存储的是中文名,现改为存 code若历史中文与 code 混存,
// 提交前必须清洗为 code避免请求体中出现 label 导致后端只应接收枚举值。
function normalizeBusinessScope(arr) {
if (!Array.isArray(arr)) return [];
const labelToValue = Object.fromEntries(
BUSINESS_SCOPE_OPTIONS.map((opt) => [opt.label, opt.value]),
);
return arr
.filter((v) => v != null && String(v).trim() !== "")
.map((v) => {
const trimmed = String(v).trim();
return labelToValue[trimmed] || trimmed;
});
}
2026-08-08 14:18:54 +08:00
// 初始表单(字段名对齐后端 OrgInfo
const INITIAL_FORM = {
unitName: "",
creditCode: "",
attachmentUrls: [], // 营业执照(数组,提交时 JSON.stringify
registerAddress: "",
businessAddress: "",
infoDisclosureUrl: "",
qualificationCertNo: "",
legalRepresentative: "",
legalRepresentativePhone: "",
fax: "",
contactNamePhone: "",
fixedAssetsTotal: "",
workplaceArea: "",
archiveRoomArea: "",
fulltimeEvaluatorCount: "",
registeredEngineerCount: "",
2026-08-13 12:54:26 +08:00
safetyIndustryCategoryCode: [], // 多选,提交时传数组
2026-08-08 14:18:54 +08:00
orgIntro: "",
id: undefined,
};
// ===== 自定义校验(不依赖 antd rules贴合原型原生表单=====
// 2026-08-08 校验规则与后端库表字段类型/长度对齐(见 OrgInfoAddCmd 的 Bean Validation 注解):
// 字符串字段长度取库 varchar 长度decimal 字段整数/小数位取库精度int 字段取 32 位有符号上限。
const PHONE_RE = /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8})$/;
// 2026-08-08 联系人及电话为「联系人姓名 + 电话」组合字段,要求串中包含合法手机/座机号
const HAS_PHONE_RE = /(1[3-9]\d{9}|0\d{2,3}-?\d{7,8})/;
// 2026-08-08 严格 URL 校验:必须为 http(s):// 开头且含「域名.后缀」结构(如 https://www.example.com
// 拦截纯数字/无域名等非法输入(如 "123" 经 normalizeUrl 补全后为 "https://123",无 "." 域名,不匹配)。
const URL_RE =
/^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+([\/?#][\w\-._~:/?#[\]@!$&'()*+,;=.\-%]*)?$/;
const CREDIT_RE = /^[0-9A-Z]{18}$/;
const NUMBER_RE = /^\d+(\.\d+)?$/;
const INT_RE = /^\d+$/;
// 2026-08-08 联系人与电话采用「单输入框 + "/" 分隔」录入模式:用户输入 "联系人/电话"。
// 提交时按首个 "/" 拆分为 contactName / contactPhone 两个后端字段(与 OrgInfoDO 拆分字段对齐)。
// 兼容无 "/" 的情形:整段视为联系人、电话置空,由校验拦截提示。
function splitContact(input) {
if (!input) return { name: "", phone: "" };
const idx = input.indexOf("/");
if (idx === -1) return { name: input.trim(), phone: "" };
return {
name: input.slice(0, idx).trim(),
phone: input.slice(idx + 1).trim(),
};
}
// 各数字字段的取值范围上限(与库 decimal/int 精度对齐,详见 OrgInfoAddCmd 注解)
const MAX_AREA_INT = 9999999999; // decimal(12,2) 整数最多10位
const MAX_AREA_FRAC = 2; // decimal(12,2) 小数最多2位
const MAX_FIXED_INT = 99999999999999; // decimal(16,4) 整数最多12位
const MAX_FIXED_FRAC = 4; // decimal(16,4) 小数最多4位
const MAX_INT32 = 2147483647; // int 32位有符号上限
// 小数位数校验:拆分整数/小数部分分别与库精度比对
const exceedsScale = (val, maxFrac) => {
const s = String(val);
const dot = s.indexOf(".");
if (dot === -1) return false;
return s.length - dot - 1 > maxFrac;
};
const validate = (formData, registerUserId) => {
const errors = {};
const required = (f, msg) => {
const v = formData[f];
if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) {
errors[f] = msg;
return true;
}
return false;
};
const len = (f, max, msg) => {
if (String(formData[f]).length > max) errors[f] = msg;
};
if (required("unitName", "请输入单位名称")) {
} else len("unitName", 200, "单位名称不能超过200字");
if (required("creditCode", "请输入统一社会信用代码")) {
} else if (!CREDIT_RE.test(formData.creditCode))
errors.creditCode = "统一社会信用代码为18位字母或数字";
if (required("attachmentUrls", "请上传营业执照")) {
}
if (required("registerAddress", "请输入注册地址")) {
} else len("registerAddress", 500, "注册地址不能超过500字"); // varchar(500)
if (required("businessAddress", "请输入办公地址")) {
} else len("businessAddress", 500, "办公地址不能超过500字"); // varchar(500)
if (required("infoDisclosureUrl", "请输入信息公开网址")) {
} else if (!URL_RE.test(normalizeUrl(formData.infoDisclosureUrl)))
errors.infoDisclosureUrl = "请输入合法网址(如 https://www.example.com";
else len("infoDisclosureUrl", 500, "网址不能超过500字"); // varchar(500)
if (required("qualificationCertNo", "请输入资质证书编号")) {
} else len("qualificationCertNo", 100, "资质证书编号不能超过100字"); // varchar(100)
if (required("legalRepresentative", "请输入法定代表人")) {
} else len("legalRepresentative", 50, "法定代表人不能超过50字"); // varchar(50)
if (required("legalRepresentativePhone", "请输入法定代表人电话")) {
} else if (!PHONE_RE.test(formData.legalRepresentativePhone))
errors.legalRepresentativePhone = "请输入正确的电话(手机或座机)";
else len("legalRepresentativePhone", 20, "电话不能超过20字"); // varchar(20)
if (required("fax", "请输入传真")) {
} else if (!PHONE_RE.test(formData.fax)) errors.fax = "请输入正确的传真(手机或座机)";
else len("fax", 20, "传真不能超过20字"); // varchar(20)
// 2026-08-08 联系人与电话:单输入框 "联系人/电话",提交时拆分;此处按 "/" 分割后分别校验。
// contactName varchar(50)contactPhone varchar(20)(与 OrgInfoDO / OrgInfoAddCmd 一致)。
if (required("contactNamePhone", "请输入联系人及电话(用/分隔联系人和电话)")) {
} else {
const { name, phone } = splitContact(formData.contactNamePhone);
if (!name) errors.contactNamePhone = "请输入联系人(/前)";
else if (name.length > 50) errors.contactNamePhone = "联系人不能超过50字"; // varchar(50)
else if (!phone) errors.contactNamePhone = "请输入联系电话(/后)";
else if (!HAS_PHONE_RE.test(phone))
errors.contactNamePhone = "请输入正确的联系电话(手机或座机)";
else if (phone.length > 20) errors.contactNamePhone = "联系电话不能超过20字"; // varchar(20)
}
// 2026-08-08 数值范围/精度校验:与后端库表 decimal 精度对齐,避免提交后数据库写入时
// 因整数位或小数位超出 decimal 精度而抛 "Out of range" 异常。
if (required("fixedAssetsTotal", "请输入固定资产总值")) {
} else if (!NUMBER_RE.test(formData.fixedAssetsTotal) || Number(formData.fixedAssetsTotal) < 0)
errors.fixedAssetsTotal = "请输入非负数字";
else if (Number(formData.fixedAssetsTotal) > MAX_FIXED_INT)
errors.fixedAssetsTotal = "数值过大整数部分不能超过12位"; // decimal(16,4)
else if (exceedsScale(formData.fixedAssetsTotal, MAX_FIXED_FRAC))
errors.fixedAssetsTotal = "小数位最多4位"; // decimal(16,4)
if (required("workplaceArea", "请输入工作场所建筑面积")) {
} else if (!NUMBER_RE.test(formData.workplaceArea) || Number(formData.workplaceArea) < 0)
errors.workplaceArea = "请输入非负数字";
else if (Number(formData.workplaceArea) > MAX_AREA_INT)
errors.workplaceArea = "数值过大整数部分不能超过10位"; // decimal(12,2)
else if (exceedsScale(formData.workplaceArea, MAX_AREA_FRAC))
errors.workplaceArea = "小数位最多2位"; // decimal(12,2)
if (required("archiveRoomArea", "请输入档案室面积")) {
} else if (!NUMBER_RE.test(formData.archiveRoomArea) || Number(formData.archiveRoomArea) < 0)
errors.archiveRoomArea = "请输入非负数字";
else if (Number(formData.archiveRoomArea) > MAX_AREA_INT)
errors.archiveRoomArea = "数值过大整数部分不能超过10位"; // decimal(12,2)
else if (exceedsScale(formData.archiveRoomArea, MAX_AREA_FRAC))
errors.archiveRoomArea = "小数位最多2位"; // decimal(12,2)
if (required("fulltimeEvaluatorCount", "请输入专职安全评价师数量")) {
} else if (!INT_RE.test(formData.fulltimeEvaluatorCount))
errors.fulltimeEvaluatorCount = "请输入非负整数";
else if (Number(formData.fulltimeEvaluatorCount) > MAX_INT32)
errors.fulltimeEvaluatorCount = "数值过大"; // int 上限
if (required("registeredEngineerCount", "请输入注册安全工程师数量")) {
} else if (!INT_RE.test(formData.registeredEngineerCount))
errors.registeredEngineerCount = "请输入非负整数";
else if (Number(formData.registeredEngineerCount) > MAX_INT32)
errors.registeredEngineerCount = "数值过大"; // int 上限
2026-08-08 14:41:32 +08:00
// 2026-08-08 校验前先清洗为 code兼容历史中文 label 混存),避免以 label 长度误判
2026-08-13 12:54:26 +08:00
const normalizedScope = normalizeBusinessScope(formData.safetyIndustryCategoryCode);
2026-08-08 14:41:32 +08:00
if (normalizedScope.length === 0) {
2026-08-13 12:54:26 +08:00
errors.safetyIndustryCategoryCode = "请至少选择一项业务范围";
2026-08-08 14:41:32 +08:00
} else if (normalizedScope.join(",").length > 500)
2026-08-13 12:54:26 +08:00
errors.safetyIndustryCategoryCode = "业务范围总字数不能超过500字"; // varchar(500)
2026-08-08 14:18:54 +08:00
if (required("orgIntro", "请输入单位基本情况介绍")) {
} else if (formData.orgIntro.length > 2000)
errors.orgIntro = "单位基本情况介绍不能超过2000字";
// 2026-08-08 注册用户标识校验:来自统一解析(路由参数或会话缓存),后端 registerUserId 为 Long
// 此处校验其存在且为数字字符串,避免非数字值(如测试占位符)提交后端时
// 触发 JSON 反序列化异常Jackson 无法将非数字字符串解析为 Long
if (!registerUserId || !/^\d+$/.test(String(registerUserId).trim()))
errors.registerUserId = "注册用户标识缺失或非法";
return errors;
};
2026-07-09 16:54:59 +08:00
2026-07-09 10:05:52 +08:00
const RegisterMore = (props) => {
const { orgInfoSave, register, syncUserToGBS } = props;
const [current, setCurrent] = useState(0);
2026-08-08 14:18:54 +08:00
const [formData, setFormData] = useState(INITIAL_FORM);
const [errors, setErrors] = useState({});
const [uploading, setUploading] = useState(false);
2026-07-09 10:05:52 +08:00
const { registerLoading, syncUserToGBSLoading } = register;
2026-08-08 14:18:54 +08:00
// ===== 2026-08-08 注册用户标识统一解析(变更原因) =====
// 变更逻辑registerUserId 仅从"路由参数"或"会话缓存"获取resolveRegisterQueryParams
// 禁止直接取 router.query 散落值,避免手改 URL 瞎写非法 id 落库。
// 变更理由:此前出现过 create_id 与真实账户脱节(前端传了错误的 registerUserId
// 现统一来源 + 保存前校验(前端拦截 + 后端 account 存在性校验)双保险。
const resolvedParams = resolveRegisterQueryParams();
const resolvedAccount = resolvedParams.account;
const resolvedRegisterUserId = resolvedParams.registerUserId;
const hasRegisterContext = Boolean(resolvedAccount && resolvedRegisterUserId);
const setField = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setErrors((prev) => ({ ...prev, [field]: undefined }));
};
2026-07-09 16:54:59 +08:00
const loadDetail = async () => {
const res = await props.orgInfoGet({
2026-08-08 14:18:54 +08:00
data: resolvedAccount,
2026-07-09 16:54:59 +08:00
});
if (res?.data) {
2026-08-13 12:54:26 +08:00
// 2026-08-08 回显safetyIndustryCategoryCode 拆为数组attachmentUrls JSON.parse
2026-08-08 14:18:54 +08:00
// 同时把后端主键 id 写回 formData供"缓存/提交"更新时精确匹配记录(规避二次映射)。
setFormData({
...INITIAL_FORM,
2026-07-09 16:54:59 +08:00
...res.data,
2026-08-08 14:18:54 +08:00
// 2026-08-08 主键 id 以字符串保留id 为后端 Long19位超过 JS Number.MAX_SAFE_INTEGER
// 用 Number() 转换会丢失末位精度(如 ...87938 → ...88000),故保持字符串,由后端 Long 反序列化解析。
id: res.data.id != null ? String(res.data.id) : undefined, // 写回主键,使保存走更新分支更精确(后端仍按 registerUserId 兜底)
2026-08-08 14:41:32 +08:00
// 2026-08-08 回显时清洗历史中文 label 为 code保证 formData 中只存枚举值
2026-08-13 12:54:26 +08:00
safetyIndustryCategoryCode: normalizeBusinessScope(
res.data.safetyIndustryCategoryCode
? String(res.data.safetyIndustryCategoryCode).split(",").filter(Boolean)
2026-08-08 14:41:32 +08:00
: [],
),
2026-07-09 16:54:59 +08:00
attachmentUrls: res.data.attachmentUrls
2026-08-08 14:18:54 +08:00
? (() => {
try {
const p = JSON.parse(res.data.attachmentUrls);
return Array.isArray(p) ? p : [];
} catch {
return [];
}
})()
: [],
fixedAssetsTotal: res.data.fixedAssetsTotal ?? "",
workplaceArea: res.data.workplaceArea ?? "",
archiveRoomArea: res.data.archiveRoomArea ?? "",
fulltimeEvaluatorCount: res.data.fulltimeEvaluatorCount ?? "",
registeredEngineerCount: res.data.registeredEngineerCount ?? "",
// 2026-08-08 后端拆分字段 contactName/contactPhone 合并回单输入框("/" 分隔)回显
contactNamePhone: [res.data.contactName, res.data.contactPhone]
.filter(Boolean)
.join("/"),
2026-07-09 16:54:59 +08:00
});
}
};
useEffect(() => {
loadDetail();
}, []);
2026-07-09 10:05:52 +08:00
2026-08-08 14:18:54 +08:00
// ===== 营业执照上传(原生 input对接后端 /safetyEval/images/upload=====
const handleFileUpload = async (e) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
const accept = [".pdf", ".jpg", ".jpeg", ".png"];
const ext = "." + file.name.split(".").pop().toLowerCase();
if (!accept.includes(ext)) {
message.error("仅支持 pdf/jpg/jpeg/png 格式文件");
return;
}
if (file.size / 1024 / 1024 > 10) {
message.error("文件大小不能超过 10MB");
return;
}
setUploading(true);
try {
const fd = new FormData();
fd.append("file", file);
const host = window.process?.env?.app?.API_HOST || "";
const resp = await fetch(`${host}/safetyEval/images/upload`, {
method: "POST",
body: fd,
});
const json = await resp.json();
const url = json?.data?.url;
if (!url) throw new Error("上传失败");
setFormData((prev) => ({
...prev,
attachmentUrls: [
...prev.attachmentUrls,
{ url, name: file.name, uid: `${Date.now()}`, status: "done" },
],
}));
setErrors((prev) => ({ ...prev, attachmentUrls: undefined }));
} catch {
message.error("上传失败,请重试");
} finally {
setUploading(false);
}
};
const removeAttachment = (uid) => {
setFormData((prev) => ({
...prev,
attachmentUrls: prev.attachmentUrls.filter((f) => f.uid !== uid),
}));
};
const toggleScope = (val) => {
setFormData((prev) => {
2026-08-13 12:54:26 +08:00
const set = new Set(prev.safetyIndustryCategoryCode);
2026-08-08 14:18:54 +08:00
if (set.has(val)) set.delete(val);
else set.add(val);
2026-08-13 12:54:26 +08:00
return { ...prev, safetyIndustryCategoryCode: Array.from(set) };
2026-08-08 14:18:54 +08:00
});
2026-08-13 12:54:26 +08:00
setErrors((prev) => ({ ...prev, safetyIndustryCategoryCode: undefined }));
2026-08-08 14:18:54 +08:00
};
// 2026-08-08 暂存(draft)与提交(submit)区分流转:
// - 两者都会先校验(必填+常见格式:网址/电话等)并调用 orgInfoSave 落库;
// - draft 仅保存草稿,停留当前页可继续编辑,不调用 syncUserToGBS、不进入下一步
// - submit 才调用 syncUserToGBS 同步并 setCurrent(1) 进入认证审核步骤。
2026-07-09 10:05:52 +08:00
const handleSave = async (type) => {
2026-08-08 14:18:54 +08:00
// ===== 2026-08-08 提交前拦截registerUserId 必须存在且为数字(变更原因) =====
// 变更理由registerUserId 缺失/非法时直接拦截,禁止把错误 id 提交后端落库,
// 与后端 account 存在性校验形成前后端双保险。
if (!resolvedRegisterUserId || !/^\d+$/.test(String(resolvedRegisterUserId).trim())) {
message.error("注册用户标识缺失或非法,请从注册页重新进入");
return;
}
2026-07-09 10:05:52 +08:00
if (!hasRegisterContext) {
message.error("缺少注册信息,请从注册页重新进入");
return;
}
2026-08-08 14:18:54 +08:00
const errs = validate(formData, resolvedRegisterUserId);
setErrors(errs);
if (Object.keys(errs).length > 0) {
message.error("请完善表单必填项或检查格式(网址/电话等)");
return;
}
// 2026-08-08 数字字段转 number业务范围 join(","),附件 JSON.stringify
// 联系人与电话:单输入框 contactNamePhone 按 "/" 拆分为 contactName + contactPhone 两个后端字段。
const { name: contactName, phone: contactPhone } = splitContact(
formData.contactNamePhone,
);
const { contactNamePhone, ...formRest } = formData;
2026-07-09 10:05:52 +08:00
const values = {
2026-08-08 14:18:54 +08:00
...formRest,
// 2026-08-08 后端 registerUserId 为 Long 类型19位超过 Number.MAX_SAFE_INTEGER
// 用 Number() 转换会丢失末位精度(如 2085936982110887938 → 2085936982110888000导致落库 id 与真实账户脱节。
// 故统一以字符串传递,由后端 Long 反序列化正确解析(与 syncUserToGBS 传参保持一致)。
registerUserId: String(resolvedRegisterUserId),
contactName,
contactPhone,
2026-08-13 12:54:26 +08:00
// 2026-08-08 提交前清洗历史中文 label 为 code以数组形式提交
safetyIndustryCategoryCode: normalizeBusinessScope(formData.safetyIndustryCategoryCode),
2026-08-08 14:18:54 +08:00
attachmentUrls: formData.attachmentUrls.length
? JSON.stringify(formData.attachmentUrls)
2026-07-09 10:05:52 +08:00
: null,
2026-08-08 14:18:54 +08:00
infoDisclosureUrl: formData.infoDisclosureUrl
? normalizeUrl(formData.infoDisclosureUrl)
2026-07-09 16:54:59 +08:00
: undefined,
2026-08-08 14:18:54 +08:00
fixedAssetsTotal: Number(formData.fixedAssetsTotal),
workplaceArea: Number(formData.workplaceArea),
archiveRoomArea: Number(formData.archiveRoomArea),
fulltimeEvaluatorCount: Number(formData.fulltimeEvaluatorCount),
registeredEngineerCount: Number(formData.registeredEngineerCount),
2026-07-09 10:05:52 +08:00
authStatusCode: type === "draft" ? 0 : 1,
authStatusName: type === "draft" ? "草稿" : "已提交",
};
2026-07-09 16:54:59 +08:00
const res1 = await orgInfoSave(values);
2026-08-08 14:18:54 +08:00
if (!res1.success) {
message.error(res1.message || "保存失败");
return;
}
if (type === "submit") {
// 提交:同步用户到 GBS 并进入下一步骤
2026-07-09 16:54:59 +08:00
const res2 = await syncUserToGBS({
2026-08-08 14:18:54 +08:00
account: resolvedAccount,
registerUserId: resolvedRegisterUserId,
2026-07-09 16:54:59 +08:00
});
if (res2.success) {
2026-08-08 14:18:54 +08:00
message.success("提交成功,进入认证审核");
2026-07-09 16:54:59 +08:00
setCurrent(1);
}
2026-08-08 14:18:54 +08:00
} else {
// 暂存:仅保存草稿,停留在当前页,保留表单可继续编辑,不跳转、不同步 GBS
message.success("已暂存草稿,可继续编辑");
2026-07-09 16:54:59 +08:00
}
};
2026-08-08 14:18:54 +08:00
const handleBack = () => {
if (window.history.length > 1) window.history.back();
2026-07-14 10:08:50 +08:00
};
2026-07-09 16:54:59 +08:00
const handleLogin = () => {
message.success("即将跳转到登录页面,请稍候...");
setTimeout(() => {
window.location.href =
"https://gbs-gateway.qhdsafety.com/login/client/ZQAQPJ";
}, 2000);
2026-07-09 10:05:52 +08:00
};
useEffect(() => {
if (current === 1) {
window.setTimeout(() => {
setCurrent(2);
}, 3000);
}
2026-07-09 16:54:59 +08:00
if (current === 2) {
handleLogin();
}
2026-07-09 10:05:52 +08:00
}, [current]);
2026-08-08 14:18:54 +08:00
const stepTitles = ["填写信息", "认证审核中", "认证通过"];
2026-07-09 10:05:52 +08:00
return (
<div className="register-more">
2026-08-08 14:18:54 +08:00
{/* hero 头图区 */}
<div className="register-more-hero">
<h1>认证信息</h1>
2026-07-09 10:05:52 +08:00
</div>
2026-08-08 14:18:54 +08:00
<div className="register-more-wrap">
{/* 步骤条 */}
<div className="register-more-steps">
{stepTitles.map((t, i) => (
<div
key={t}
className={`register-more-step ${i <= current ? "active" : ""}`}
>
<i>{i + 1}</i>
<span>{t}</span>
</div>
))}
</div>
2026-07-09 10:05:52 +08:00
{current === 0 && (
2026-08-08 14:18:54 +08:00
<div className="register-more-card">
<h2>申请单位基本情况</h2>
2026-07-09 10:05:52 +08:00
{!hasRegisterContext && (
<Empty
description="缺少注册信息,请从注册页重新进入"
style={{ marginBottom: 24 }}
/>
)}
2026-08-08 14:18:54 +08:00
<div
className={`register-more-grid ${
!hasRegisterContext ? "is-disabled" : ""
}`}
2026-07-09 16:54:59 +08:00
>
2026-08-08 14:18:54 +08:00
{/* 单位名称 */}
<Field
label="单位名称"
required
error={errors.unitName}
>
<input
type="text"
maxLength={200}
placeholder="请输入单位名称"
value={formData.unitName}
disabled={!hasRegisterContext}
onChange={(e) => setField("unitName", e.target.value)}
/>
</Field>
{/* 统一社会信用代码 */}
<Field
label="统一社会信用代码"
required
error={errors.creditCode}
>
<input
type="text"
maxLength={18}
placeholder="请输入统一社会信用代码"
value={formData.creditCode}
disabled={!hasRegisterContext}
onChange={(e) => setField("creditCode", e.target.value)}
/>
</Field>
{/* 营业执照(两列,与单位名称/信用代码同列;控件内联显示红星与提示) */}
<Field
label="营业执照"
required
error={errors.attachmentUrls}
>
<div className="register-more-license">
<label className="register-more-file-btn">
<input
type="file"
accept=".pdf,.jpg,.jpeg,.png"
disabled={!hasRegisterContext || uploading}
onChange={handleFileUpload}
2026-07-09 10:05:52 +08:00
/>
2026-08-08 14:18:54 +08:00
<span className="register-more-file-text">
{uploading ? "上传中..." : "选择文件"}
</span>
</label>
<span className="register-more-optional-note">
请上传营业执照扫描件或清晰照片支持 PDFJPGPNG
</span>
<div className="register-more-license-list">
{formData.attachmentUrls.map((f) => (
<span className="license-item" key={f.uid}>
<a href={f.url} target="_blank" rel="noreferrer">
{f.name || "营业执照"}
</a>
<button
type="button"
className="license-remove"
disabled={!hasRegisterContext}
onClick={() => removeAttachment(f.uid)}
>
×
</button>
</span>
))}
</div>
</div>
</Field>
{/* 注册地址(全宽单独成行) */}
<Field
label="注册地址"
required
wide
alignStart
error={errors.registerAddress}
>
<textarea
rows={3}
maxLength={500}
placeholder="请输入注册地址"
value={formData.registerAddress}
disabled={!hasRegisterContext}
onChange={(e) => setField("registerAddress", e.target.value)}
/>
</Field>
{/* 办公地址(全宽单独成行) */}
<Field
label="办公地址"
required
wide
alignStart
error={errors.businessAddress}
>
<textarea
rows={3}
maxLength={500}
placeholder="请输入办公地址"
value={formData.businessAddress}
disabled={!hasRegisterContext}
onChange={(e) => setField("businessAddress", e.target.value)}
/>
</Field>
{/* 信息公开网址 */}
<Field
label="信息公开网址"
required
error={errors.infoDisclosureUrl}
>
<input
type="url"
maxLength={500}
placeholder="请输入信息公开网址"
value={formData.infoDisclosureUrl}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("infoDisclosureUrl", e.target.value)
}
/>
</Field>
{/* 资质证书编号 */}
<Field
label="资质证书编号"
required
error={errors.qualificationCertNo}
>
<input
type="text"
maxLength={100}
placeholder="请输入资质证书编号"
value={formData.qualificationCertNo}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("qualificationCertNo", e.target.value)
}
/>
</Field>
{/* 法定代表人 */}
<Field
label="法定代表人"
required
error={errors.legalRepresentative}
>
<input
type="text"
maxLength={50}
placeholder="请输入法定代表人"
value={formData.legalRepresentative}
2026-07-09 10:05:52 +08:00
disabled={!hasRegisterContext}
2026-08-08 14:18:54 +08:00
onChange={(e) =>
setField("legalRepresentative", e.target.value)
}
/>
</Field>
{/* 法定代表人电话 */}
<Field
label="法定代表人电话"
required
error={errors.legalRepresentativePhone}
>
<input
type="tel"
maxLength={20}
placeholder="请输入法定代表人电话"
value={formData.legalRepresentativePhone}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("legalRepresentativePhone", e.target.value)
}
/>
</Field>
{/* 传真 */}
<Field label="传真" required error={errors.fax}>
<input
type="tel"
maxLength={20}
placeholder="请输入传真"
value={formData.fax}
disabled={!hasRegisterContext}
onChange={(e) => setField("fax", e.target.value)}
/>
</Field>
{/* 联系人及电话(单输入框 + "/" 分隔,提交时拆分存储) */}
<Field label="联系人及电话" required error={errors.contactNamePhone}>
<input
type="text"
maxLength={100}
placeholder="请输入联系人及电话"
value={formData.contactNamePhone}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("contactNamePhone", e.target.value)
}
/>
{/* 2026-08-08 输入框下方 12px 灰字提示:单输入框用 / 分隔联系人和电话 */}
<span className="register-more-optional-note">
请使用/分隔联系人和电话张三/13800138000
</span>
</Field>
{/* 固定资产总值(万元) */}
<Field
label="固定资产总值(万元)"
required
error={errors.fixedAssetsTotal}
>
<input
type="number"
min={0}
max={99999999999999}
step={0.01}
placeholder="请输入固定资产总值"
value={formData.fixedAssetsTotal}
disabled={!hasRegisterContext}
onChange={(e) => setField("fixedAssetsTotal", e.target.value)}
/>
</Field>
{/* 工作场所建筑面积(㎡) */}
<Field
label="工作场所建筑面积(㎡)"
required
error={errors.workplaceArea}
>
<input
type="number"
min={0}
max={9999999999}
step={0.01}
placeholder="请输入工作场所建筑面积"
value={formData.workplaceArea}
disabled={!hasRegisterContext}
onChange={(e) => setField("workplaceArea", e.target.value)}
/>
</Field>
{/* 档案室面积(㎡) */}
<Field
label="档案室面积(㎡)"
required
error={errors.archiveRoomArea}
>
<input
type="number"
min={0}
max={9999999999}
step={0.01}
placeholder="请输入档案室面积"
value={formData.archiveRoomArea}
disabled={!hasRegisterContext}
onChange={(e) => setField("archiveRoomArea", e.target.value)}
/>
</Field>
{/* 专职安全评价师数量 */}
<Field
label="专职安全评价师数量"
required
error={errors.fulltimeEvaluatorCount}
>
<input
type="number"
min={0}
step={1}
placeholder="请输入专职安全评价师数量"
value={formData.fulltimeEvaluatorCount}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("fulltimeEvaluatorCount", e.target.value)
}
/>
</Field>
{/* 注册安全工程师数量 */}
<Field
label="注册安全工程师数量"
required
error={errors.registeredEngineerCount}
>
<input
type="number"
min={0}
step={1}
placeholder="请输入注册安全工程师数量"
value={formData.registeredEngineerCount}
disabled={!hasRegisterContext}
onChange={(e) =>
setField("registeredEngineerCount", e.target.value)
}
/>
</Field>
{/* 拟申请的法定安全评价业务范围(全宽单独成行,多选) */}
<Field
label="拟申请的法定安全评价业务范围"
required
wide
alignStart
2026-08-13 12:54:26 +08:00
error={errors.safetyIndustryCategoryCode}
2026-08-08 14:18:54 +08:00
>
<div className="register-more-scope">
{BUSINESS_SCOPE_OPTIONS.map((opt) => (
2026-08-08 14:41:32 +08:00
<label key={opt.value}>
2026-08-08 14:18:54 +08:00
<input
type="checkbox"
2026-08-13 12:54:26 +08:00
checked={formData.safetyIndustryCategoryCode.includes(opt.value)}
2026-08-08 14:18:54 +08:00
disabled={!hasRegisterContext}
2026-08-08 14:41:32 +08:00
onChange={() => toggleScope(opt.value)}
2026-08-08 14:18:54 +08:00
/>
2026-08-08 14:41:32 +08:00
{opt.label}
2026-08-08 14:18:54 +08:00
</label>
))}
</div>
</Field>
{/* 单位基本情况介绍(全宽单独成行,长文本) */}
<Field
label="单位基本情况介绍"
required
wide
alignStart
error={errors.orgIntro}
>
<textarea
className="register-more-large-textarea"
rows={6}
maxLength={2000}
placeholder="请输入单位基本情况介绍"
value={formData.orgIntro}
disabled={!hasRegisterContext}
onChange={(e) => setField("orgIntro", e.target.value)}
/>
</Field>
</div>
{/* 底部提示 */}
<div className="register-more-tip">
请按机构资质申请材料如实填写 * 字段为必填项
</div>
{/* 操作按钮 */}
<div className="register-more-actions">
<button
type="button"
className="register-more-btn secondary"
onClick={handleBack}
disabled={!hasRegisterContext}
>
返回
</button>
<button
type="button"
className="register-more-btn secondary"
onClick={() => handleSave("draft")}
disabled={!hasRegisterContext || registerLoading || syncUserToGBSLoading}
>
暂存
</button>
<button
type="button"
className="register-more-btn primary"
onClick={() => handleSave("submit")}
disabled={!hasRegisterContext || registerLoading || syncUserToGBSLoading}
>
提交认证
</button>
</div>
2026-07-09 10:05:52 +08:00
</div>
)}
2026-08-08 14:18:54 +08:00
2026-07-09 10:05:52 +08:00
{current === 1 && (
2026-08-08 14:18:54 +08:00
<div className="register-more-card">
<div className="register-more-state">
2026-07-09 10:05:52 +08:00
<div
className="audit-look"
2026-08-08 14:18:54 +08:00
style={{ backgroundImage: `url(${auditBkImage})` }}
2026-07-09 10:05:52 +08:00
>
2026-08-08 14:18:54 +08:00
<img src={auditLookImage} className="audit-look-img" alt="" />
2026-07-09 10:05:52 +08:00
</div>
<div>认证审核中请耐心等待···</div>
2026-08-08 14:18:54 +08:00
</div>
2026-07-09 10:05:52 +08:00
</div>
)}
{current === 2 && (
2026-08-08 14:18:54 +08:00
<div className="register-more-card">
<div className="register-more-state">
<img src={successIcon} alt="" />
2026-07-09 10:05:52 +08:00
<div>审核通过</div>
2026-08-08 14:18:54 +08:00
</div>
2026-07-09 10:05:52 +08:00
</div>
)}
</div>
</div>
);
};
2026-08-08 14:18:54 +08:00
// 字段行组件label + 控件 + 错误提示,按原型 .field 布局
const Field = ({ label, required, wide, alignStart, error, children }) => (
<div
className={`register-more-field ${required ? "required" : ""} ${
wide ? "register-more-wide" : ""
} ${alignStart ? "align-start" : ""}`}
>
<label>{label}</label>
<div className="register-more-control">
{children}
{error && <span className="register-more-error">{error}</span>}
</div>
</div>
);
2026-07-09 10:05:52 +08:00
export default Connect([NS_REGISTER], true)(RegisterMore);