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

897 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useEffect, useState } from "react";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { message, Empty } from "antd";
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";
import { normalizeUrl } from "~/utils/validators";
import { resolveRegisterQueryParams } from "~/utils/resolveRegisterQueryParams";
import "./index.less";
// ===== 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) 单列适配。
// 业务范围选项(与后端枚举 code 严格匹配OrgBusinessScopeDeviceRefDO 等定义的行业 code
// 2026-08-08 改为 {value,label}value 用后端枚举 code存储/渲染的值label 仅作展示,
// 不再以中文名作为存储值,保证与后端枚举一致。
const BUSINESS_SCOPE_OPTIONS = [
{ 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 业务范围值清洗:兼容历史已存的中文 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;
});
}
// 初始表单(字段名对齐后端 OrgInfo
const INITIAL_FORM = {
unitName: "",
creditCode: "",
attachmentUrls: [], // 营业执照(数组,提交时 JSON.stringify
registerAddress: "",
businessAddress: "",
infoDisclosureUrl: "",
qualificationCertNo: "",
legalRepresentative: "",
legalRepresentativePhone: "",
fax: "",
contactNamePhone: "",
fixedAssetsTotal: "",
workplaceArea: "",
archiveRoomArea: "",
fulltimeEvaluatorCount: "",
registeredEngineerCount: "",
safetyIndustryCategoryCode: [], // 多选,提交时传数组
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 校验前先清洗为 code兼容历史中文 label 混存),避免以 label 长度误判
const normalizedScope = normalizeBusinessScope(formData.safetyIndustryCategoryCode);
if (normalizedScope.length === 0) {
errors.safetyIndustryCategoryCode = "请至少选择一项业务范围";
} else if (normalizedScope.join(",").length > 500)
errors.safetyIndustryCategoryCode = "业务范围总字数不能超过500字"; // varchar(500)
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;
};
const RegisterMore = (props) => {
const { orgInfoSave, register, syncUserToGBS } = props;
const [current, setCurrent] = useState(0);
const [formData, setFormData] = useState(INITIAL_FORM);
const [errors, setErrors] = useState({});
const [uploading, setUploading] = useState(false);
const { registerLoading, syncUserToGBSLoading } = register;
// ===== 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 }));
};
const loadDetail = async () => {
const res = await props.orgInfoGet({
data: resolvedAccount,
});
if (res?.data) {
// 2026-08-08 回显safetyIndustryCategoryCode 拆为数组attachmentUrls JSON.parse
// 同时把后端主键 id 写回 formData供"缓存/提交"更新时精确匹配记录(规避二次映射)。
setFormData({
...INITIAL_FORM,
...res.data,
// 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 回显时清洗历史中文 label 为 code保证 formData 中只存枚举值
safetyIndustryCategoryCode: normalizeBusinessScope(
res.data.safetyIndustryCategoryCode
? String(res.data.safetyIndustryCategoryCode).split(",").filter(Boolean)
: [],
),
attachmentUrls: res.data.attachmentUrls
? (() => {
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("/"),
});
}
};
useEffect(() => {
loadDetail();
}, []);
// ===== 营业执照上传(原生 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) => {
const set = new Set(prev.safetyIndustryCategoryCode);
if (set.has(val)) set.delete(val);
else set.add(val);
return { ...prev, safetyIndustryCategoryCode: Array.from(set) };
});
setErrors((prev) => ({ ...prev, safetyIndustryCategoryCode: undefined }));
};
// 2026-08-08 暂存(draft)与提交(submit)区分流转:
// - 两者都会先校验(必填+常见格式:网址/电话等)并调用 orgInfoSave 落库;
// - draft 仅保存草稿,停留当前页可继续编辑,不调用 syncUserToGBS、不进入下一步
// - submit 才调用 syncUserToGBS 同步并 setCurrent(1) 进入认证审核步骤。
const handleSave = async (type) => {
// ===== 2026-08-08 提交前拦截registerUserId 必须存在且为数字(变更原因) =====
// 变更理由registerUserId 缺失/非法时直接拦截,禁止把错误 id 提交后端落库,
// 与后端 account 存在性校验形成前后端双保险。
if (!resolvedRegisterUserId || !/^\d+$/.test(String(resolvedRegisterUserId).trim())) {
message.error("注册用户标识缺失或非法,请从注册页重新进入");
return;
}
if (!hasRegisterContext) {
message.error("缺少注册信息,请从注册页重新进入");
return;
}
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;
const values = {
...formRest,
// 2026-08-08 后端 registerUserId 为 Long 类型19位超过 Number.MAX_SAFE_INTEGER
// 用 Number() 转换会丢失末位精度(如 2085936982110887938 → 2085936982110888000导致落库 id 与真实账户脱节。
// 故统一以字符串传递,由后端 Long 反序列化正确解析(与 syncUserToGBS 传参保持一致)。
registerUserId: String(resolvedRegisterUserId),
contactName,
contactPhone,
// 2026-08-08 提交前清洗历史中文 label 为 code以数组形式提交
safetyIndustryCategoryCode: normalizeBusinessScope(formData.safetyIndustryCategoryCode),
attachmentUrls: formData.attachmentUrls.length
? JSON.stringify(formData.attachmentUrls)
: null,
infoDisclosureUrl: formData.infoDisclosureUrl
? normalizeUrl(formData.infoDisclosureUrl)
: undefined,
fixedAssetsTotal: Number(formData.fixedAssetsTotal),
workplaceArea: Number(formData.workplaceArea),
archiveRoomArea: Number(formData.archiveRoomArea),
fulltimeEvaluatorCount: Number(formData.fulltimeEvaluatorCount),
registeredEngineerCount: Number(formData.registeredEngineerCount),
authStatusCode: type === "draft" ? 0 : 1,
authStatusName: type === "draft" ? "草稿" : "已提交",
};
const res1 = await orgInfoSave(values);
if (!res1.success) {
message.error(res1.message || "保存失败");
return;
}
if (type === "submit") {
// 提交:同步用户到 GBS 并进入下一步骤
const res2 = await syncUserToGBS({
account: resolvedAccount,
registerUserId: resolvedRegisterUserId,
});
if (res2.success) {
message.success("提交成功,进入认证审核");
setCurrent(1);
}
} else {
// 暂存:仅保存草稿,停留在当前页,保留表单可继续编辑,不跳转、不同步 GBS
message.success("已暂存草稿,可继续编辑");
}
};
const handleBack = () => {
if (window.history.length > 1) window.history.back();
};
const handleLogin = () => {
message.success("即将跳转到登录页面,请稍候...");
setTimeout(() => {
window.location.href =
"https://gbs-gateway.qhdsafety.com/login/client/ZQAQPJ";
}, 2000);
};
useEffect(() => {
if (current === 1) {
window.setTimeout(() => {
setCurrent(2);
}, 3000);
}
if (current === 2) {
handleLogin();
}
}, [current]);
const stepTitles = ["填写信息", "认证审核中", "认证通过"];
return (
<div className="register-more">
{/* hero 头图区 */}
<div className="register-more-hero">
<h1>认证信息</h1>
</div>
<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>
{current === 0 && (
<div className="register-more-card">
<h2>申请单位基本情况</h2>
{!hasRegisterContext && (
<Empty
description="缺少注册信息,请从注册页重新进入"
style={{ marginBottom: 24 }}
/>
)}
<div
className={`register-more-grid ${
!hasRegisterContext ? "is-disabled" : ""
}`}
>
{/* 单位名称 */}
<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}
/>
<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}
disabled={!hasRegisterContext}
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
error={errors.safetyIndustryCategoryCode}
>
<div className="register-more-scope">
{BUSINESS_SCOPE_OPTIONS.map((opt) => (
<label key={opt.value}>
<input
type="checkbox"
checked={formData.safetyIndustryCategoryCode.includes(opt.value)}
disabled={!hasRegisterContext}
onChange={() => toggleScope(opt.value)}
/>
{opt.label}
</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>
</div>
)}
{current === 1 && (
<div className="register-more-card">
<div className="register-more-state">
<div
className="audit-look"
style={{ backgroundImage: `url(${auditBkImage})` }}
>
<img src={auditLookImage} className="audit-look-img" alt="" />
</div>
<div>认证审核中请耐心等待···</div>
</div>
</div>
)}
{current === 2 && (
<div className="register-more-card">
<div className="register-more-state">
<img src={successIcon} alt="" />
<div>审核通过</div>
</div>
</div>
)}
</div>
</div>
);
};
// 字段行组件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>
);
export default Connect([NS_REGISTER], true)(RegisterMore);