From 026d86b082eb50fe0a767b74752efdb63a1129a6 Mon Sep 17 00:00:00 2001 From: huwei <3313749341@qq.com> Date: Sat, 8 Aug 2026 14:18:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E4=BF=A1=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ectoryPolylineTrailLinkMaterialProperty.js | 62 +- .../WallPolylineTrailLinkMaterialProperty.js | 86 +- src/pages/Container/Register/index.js | 6 + src/pages/Container/RegisterMore/index.js | 1426 +++++++++-------- src/pages/Container/RegisterMore/index.less | 410 ++++- src/utils/resolveRegisterQueryParams.js | 30 + 6 files changed, 1302 insertions(+), 718 deletions(-) diff --git a/src/pages/Container/Map/js/TrajectoryPolylineTrailLinkMaterialProperty.js b/src/pages/Container/Map/js/TrajectoryPolylineTrailLinkMaterialProperty.js index c7591db..8ac0ced 100644 --- a/src/pages/Container/Map/js/TrajectoryPolylineTrailLinkMaterialProperty.js +++ b/src/pages/Container/Map/js/TrajectoryPolylineTrailLinkMaterialProperty.js @@ -1,6 +1,15 @@ -const Cesium = window.Cesium; +// 2026-08-08 修复:原实现在模块顶层直接 `const Cesium = window.Cesium` 并立即访问 +// `Cesium.createPropertyDescriptor` / `Cesium.Material` 执行材质注册。当 Cesium(CDN 全局脚本) +// 尚未就绪(如离线/CDN 不可达/加载顺序差异)时,模块求值阶段即抛 +// "Cannot read properties of undefined (reading 'createPropertyDescriptor')", +// 而该模块被自动路由同步加载,会连带阻塞整个应用启动(DvaRoot 渲染失败)。 +// 变更逻辑:① 统一改用 `window.Cesium` 取最新全局值,避免加载时缓存为 undefined; +// ② 把依赖 Cesium 的材质注册逻辑延迟到 Cesium 就绪后执行(立即或 window load 事件), +// 注册失败仅跳过、不抛错,保证即使 Cesium 暂未加载也不会拖垮应用启动。 function TrajectoryPolylineTrailLinkMaterialProperty(viewer) { + // 构造时再取全局 Cesium,确保 Cesium 已就绪 + const Cesium = window.Cesium; this.viewer = viewer; this._definitionChanged = new Cesium.Event(); this._uTime = 0; @@ -17,7 +26,9 @@ Object.defineProperties(TrajectoryPolylineTrailLinkMaterialProperty.prototype, { return this._definitionChanged; }, }, - uTime: Cesium.createPropertyDescriptor("uTime"), + uTime: window.Cesium + ? window.Cesium.createPropertyDescriptor("uTime") + : { get() { return this._uTime; }, set(v) { this._uTime = v; } }, }); TrajectoryPolylineTrailLinkMaterialProperty.prototype.getType = function () { @@ -28,6 +39,7 @@ TrajectoryPolylineTrailLinkMaterialProperty.prototype.getValue = function ( time, result, ) { + const Cesium = window.Cesium; if (!Cesium.defined(result)) { result = {}; } @@ -52,8 +64,15 @@ TrajectoryPolylineTrailLinkMaterialProperty.prototype.equals = function ( ); }; -Cesium.Material.CustomMaterialType = "CustomMaterial"; -Cesium.Material.CustomMaterialSource = ` +// 注册到 Cesium.Material(依赖 Cesium 全局脚本已就绪),失败仅跳过不阻断应用 +function registerTrajectoryMaterial() { + try { + const Cesium = window.Cesium; + if (!Cesium || !Cesium.Material) { + return; + } + Cesium.Material.CustomMaterialType = "CustomMaterial"; + Cesium.Material.CustomMaterialSource = ` czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); @@ -66,16 +85,29 @@ czm_material czm_getMaterial(czm_materialInput materialInput) material.emission = material.diffuse * 0.5; return material; }`; + Cesium.Material._materialCache.addMaterial(Cesium.Material.CustomMaterialType, { + fabric: { + type: Cesium.Material.CustomMaterialType, + uniforms: { + uTime: 0, + }, + source: Cesium.Material.CustomMaterialSource, + }, + }); + Cesium.TrajectoryPolylineTrailLinkMaterialProperty + = TrajectoryPolylineTrailLinkMaterialProperty; + } catch (e) { + // Cesium 未就绪或注册异常:仅跳过,不阻断应用启动(Map 真实访问时 Cesium 已存在) + // eslint-disable-next-line no-console + console.warn("[TrajectoryPolylineTrailLinkMaterialProperty] 材质注册跳过:", e); + } +} -Cesium.Material._materialCache.addMaterial(Cesium.Material.CustomMaterialType, { - fabric: { - type: Cesium.Material.CustomMaterialType, - uniforms: { - uTime: 0, - }, - source: Cesium.Material.CustomMaterialSource, - }, -}); +// 模块顶层仅定义类;注册延迟到 Cesium 就绪(立即或 window load)后执行,避免阻断应用启动 +if (window.Cesium && window.Cesium.Material) { + registerTrajectoryMaterial(); +} else { + window.addEventListener("load", registerTrajectoryMaterial, { once: true }); +} -Cesium.TrajectoryPolylineTrailLinkMaterialProperty - = TrajectoryPolylineTrailLinkMaterialProperty; +export default TrajectoryPolylineTrailLinkMaterialProperty; diff --git a/src/pages/Container/Map/js/WallPolylineTrailLinkMaterialProperty.js b/src/pages/Container/Map/js/WallPolylineTrailLinkMaterialProperty.js index 2cc2440..31489a5 100644 --- a/src/pages/Container/Map/js/WallPolylineTrailLinkMaterialProperty.js +++ b/src/pages/Container/Map/js/WallPolylineTrailLinkMaterialProperty.js @@ -1,14 +1,22 @@ import PolylineTrailLinkImage from "~/assets/images/map_bi/wall_img.png"; -const Cesium = window.Cesium; +// 2026-08-08 修复:原实现在模块顶层 `const Cesium = window.Cesium` 并立即访问 +// `Cesium.Color` / `Cesium.Material` 执行材质注册。当 Cesium(CDN 全局脚本)尚未就绪时, +// 模块求值阶段即抛错,而该模块被自动路由同步加载,会连带阻塞整个应用启动。 +// 变更逻辑:① 统一改用 `window.Cesium` 取最新全局值;② 材质注册延迟到 Cesium 就绪后执行, +// 失败仅跳过、不抛错,保证应用可正常启动(Map 真实访问时 Cesium 已存在)。 function WallPolylineTrailLinkMaterialProperty( viewer, options = { - color: Cesium.Color.fromBytes(201, 118, 243).withAlpha(0.5), + // 构造时再取全局 Cesium,避免加载时缓存为 undefined + color: window.Cesium + ? window.Cesium.Color.fromBytes(201, 118, 243).withAlpha(0.5) + : undefined, duration: 2000, }, ) { + const Cesium = window.Cesium; this.viewer = viewer; this._definitionChanged = new Cesium.Event(); this._color = undefined; @@ -29,15 +37,20 @@ Object.defineProperties(WallPolylineTrailLinkMaterialProperty.prototype, { return this._definitionChanged; }, }, - color: Cesium.createPropertyDescriptor("color"), + color: window.Cesium + ? window.Cesium.createPropertyDescriptor("color") + : { get() { return this._color; }, set(v) { this._color = v; } }, }); + WallPolylineTrailLinkMaterialProperty.prototype.getType = function () { return "PolylineTrailLink"; }; + WallPolylineTrailLinkMaterialProperty.prototype.getValue = function ( time, result, ) { + const Cesium = window.Cesium; if (!Cesium.defined(result)) { result = {}; } @@ -56,18 +69,28 @@ WallPolylineTrailLinkMaterialProperty.prototype.getValue = function ( this.viewer.scene.requestRender(); return result; }; + WallPolylineTrailLinkMaterialProperty.prototype.equals = function (other) { + const Cesium = window.Cesium; return ( this === other || (other instanceof WallPolylineTrailLinkMaterialProperty && Cesium.Property.equals(this._color, other._color)) ); }; -Cesium.WallPolylineTrailLinkMaterialProperty - = WallPolylineTrailLinkMaterialProperty; -Cesium.Material.PolylineTrailLinkType = "PolylineTrailLink"; -Cesium.Material.PolylineTrailLinkImage = PolylineTrailLinkImage; -Cesium.Material.PolylineTrailLinkSource = `czm_material czm_getMaterial(czm_materialInput + +// 注册到 Cesium.Material(依赖 Cesium 全局脚本已就绪),失败仅跳过不阻断应用 +function registerWallMaterial() { + try { + const Cesium = window.Cesium; + if (!Cesium || !Cesium.Material) { + return; + } + Cesium.WallPolylineTrailLinkMaterialProperty + = WallPolylineTrailLinkMaterialProperty; + Cesium.Material.PolylineTrailLinkType = "PolylineTrailLink"; + Cesium.Material.PolylineTrailLinkImage = PolylineTrailLinkImage; + Cesium.Material.PolylineTrailLinkSource = `czm_material czm_getMaterial(czm_materialInput materialInput)\n\ {\n\ czm_material material = @@ -83,20 +106,35 @@ Cesium.Material.PolylineTrailLinkSource = `czm_material czm_getMaterial(czm_mate material.emission = fragColor.rgb;\n\ return material;\n\ }`; -Cesium.Material._materialCache.addMaterial( - Cesium.Material.PolylineTrailLinkType, - { - fabric: { - type: Cesium.Material.PolylineTrailLinkType, - uniforms: { - color: new Cesium.Color(1.0, 1.0, 1.0, 1), - image: Cesium.Material.PolylineTrailLinkImage, - time: 0, + Cesium.Material._materialCache.addMaterial( + Cesium.Material.PolylineTrailLinkType, + { + fabric: { + type: Cesium.Material.PolylineTrailLinkType, + uniforms: { + color: new Cesium.Color(1.0, 1.0, 1.0, 1), + image: Cesium.Material.PolylineTrailLinkImage, + time: 0, + }, + source: Cesium.Material.PolylineTrailLinkSource, + }, + translucent() { + return true; + }, }, - source: Cesium.Material.PolylineTrailLinkSource, - }, - translucent() { - return true; - }, - }, -); + ); + } catch (e) { + // Cesium 未就绪或注册异常:仅跳过,不阻断应用启动 + // eslint-disable-next-line no-console + console.warn("[WallPolylineTrailLinkMaterialProperty] 材质注册跳过:", e); + } +} + +// 模块顶层仅定义类;注册延迟到 Cesium 就绪(立即或 window load)后执行,避免阻断应用启动 +if (window.Cesium && window.Cesium.Material) { + registerWallMaterial(); +} else { + window.addEventListener("load", registerWallMaterial, { once: true }); +} + +export default WallPolylineTrailLinkMaterialProperty; diff --git a/src/pages/Container/Register/index.js b/src/pages/Container/Register/index.js index 6cad679..be9b329 100644 --- a/src/pages/Container/Register/index.js +++ b/src/pages/Container/Register/index.js @@ -13,6 +13,7 @@ import loginbg from "~/enumerate/img/loginbg.jpg"; import logo from "~/enumerate/img/logo.png"; import leftIcon from "~/enumerate/img/left-img.jpg"; import "./index.less"; +import { cacheRegisterQueryParams } from "~/utils/resolveRegisterQueryParams"; function Register(props) { const { sendMessageAction, registerAction, register } = props; @@ -74,6 +75,11 @@ function Register(props) { } message.success("操作成功"); form.resetFields(); + // 2026-08-08 跳转填报页同时写入会话缓存,供填报页缺失路由参数时兜底读取(防止瞎写 registerUserId) + cacheRegisterQueryParams({ + account: values.account, + registerUserId: res.data.id, + }); props.history.push( `RegisterMore?account=${encodeURIComponent(values.account)}®isterUserId=${encodeURIComponent(res.data.id)}`, ); diff --git a/src/pages/Container/RegisterMore/index.js b/src/pages/Container/RegisterMore/index.js index 516d1ef..94cf93b 100644 --- a/src/pages/Container/RegisterMore/index.js +++ b/src/pages/Container/RegisterMore/index.js @@ -1,88 +1,257 @@ import React, { useEffect, useState } from "react"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; -import dayjs from "dayjs"; -import { - Button, - Empty, - Form, - Input, - message, - Steps, - Flex, - Row, - Col, - Select, - InputNumber, - DatePicker, - Space, - Radio, -} from "antd"; +import { message, Empty } from "antd"; import { NS_REGISTER } from "~/enumerate/namespace"; -import AttachmentUpload from "~/components/AttachmentUpload"; -import { tools } from "@cqsjjb/jjb-common-lib"; - -import headerImage from "~/enumerate/img/header.png"; import auditLookImage from "~/enumerate/img/auditLook.png"; import auditBkImage from "~/enumerate/img/audit.png"; import successIcon from "~/enumerate/img/successIcon.png"; -import { - creditCodeRule, - nonNegativeIntegerRule, - phoneRule, - positiveNumberRule, - urlRule, - normalizeUrl, -} from "~/utils/validators"; -import { - CHONGQING_DISTRICTS, - NATIONAL_COUNTIES, - ECONOMY_INDUSTRY_OPTIONS, - ENTERPRISE_SCALE_OPTIONS, - ENTERPRISE_STATUS_OPTIONS, - QUALIFICATION_INDUSTRY_OPTIONS, - REGISTERED_ORG_FILING_TYPE_SEARCH_OPTIONS, - REGISTERED_ORG_FILING_RECORD_STATUS_OPTIONS, -} from "~/enumerate/enterpriseOptions"; -import BaiduMapPicker from "~/components/BaiduMapPicker"; +import { normalizeUrl } from "~/utils/validators"; +import { resolveRegisterQueryParams } from "~/utils/resolveRegisterQueryParams"; import "./index.less"; -const { router } = tools; +// ===== 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) 单列适配。 +// 业务范围选项(原型固定 7 项,多选) +const BUSINESS_SCOPE_OPTIONS = [ + "煤炭开采业", + "金属、非金属矿及其他矿采选业", + "石油和天然气开采业", + "石油加工业,化学原料、化学品及医药制造业", + "烟花爆竹制造业", + "民用爆破器材制造业", + "金属冶炼业", +]; + +// 初始表单(字段名对齐后端 OrgInfo) +const INITIAL_FORM = { + unitName: "", + creditCode: "", + attachmentUrls: [], // 营业执照(数组,提交时 JSON.stringify) + registerAddress: "", + businessAddress: "", + infoDisclosureUrl: "", + qualificationCertNo: "", + legalRepresentative: "", + legalRepresentativePhone: "", + fax: "", + contactNamePhone: "", + fixedAssetsTotal: "", + workplaceArea: "", + archiveRoomArea: "", + fulltimeEvaluatorCount: "", + registeredEngineerCount: "", + applyBusinessScope: [], // 多选,提交时 join(",") + 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 上限 + if (required("applyBusinessScope", "请至少选择一项业务范围")) { + } else if (formData.applyBusinessScope.join(",").length > 500) + errors.applyBusinessScope = "业务范围总字数不能超过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 [mapPickerVisible, setMapPickerVisible] = useState(false); - const [form] = Form.useForm(); + const [formData, setFormData] = useState(INITIAL_FORM); + const [errors, setErrors] = useState({}); + const [uploading, setUploading] = useState(false); const { registerLoading, syncUserToGBSLoading } = register; - const isChongqing = Form.useWatch("isChongqing", form); - const districtOptions = - isChongqing === 2 ? NATIONAL_COUNTIES : CHONGQING_DISTRICTS; - const hasRegisterContext = Boolean( - router.query?.account && router.query?.registerUserId, - ); + // ===== 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: router.query?.account, + data: resolvedAccount, }); if (res?.data) { - const districtCode = res.data.districtCode; - const isChongqing = districtCode - ? CHONGQING_DISTRICTS.some( - (d) => d.value === districtCode || d.label === districtCode, - ) - ? 1 - : 2 - : 1; - form.setFieldsValue({ + // 2026-08-08 回显:applyBusinessScope 拆为数组,attachmentUrls JSON.parse + // 同时把后端主键 id 写回 formData,供"缓存/提交"更新时精确匹配记录(规避二次映射)。 + setFormData({ + ...INITIAL_FORM, ...res.data, - isChongqing, - productionDate: res.data.productionDate - ? dayjs(res.data.productionDate) - : undefined, + // 2026-08-08 主键 id 以字符串保留:id 为后端 Long(19位),超过 JS Number.MAX_SAFE_INTEGER, + // 用 Number() 转换会丢失末位精度(如 ...87938 → ...88000),故保持字符串,由后端 Long 反序列化解析。 + id: res.data.id != null ? String(res.data.id) : undefined, // 写回主键,使保存走更新分支更精确(后端仍按 registerUserId 兜底) + applyBusinessScope: res.data.applyBusinessScope + ? String(res.data.applyBusinessScope).split(",").filter(Boolean) + : [], attachmentUrls: res.data.attachmentUrls - ? JSON.parse(res.data.attachmentUrls) - : null, + ? (() => { + 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("/"), }); } }; @@ -91,44 +260,139 @@ const RegisterMore = (props) => { 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.applyBusinessScope); + if (set.has(val)) set.delete(val); + else set.add(val); + return { ...prev, applyBusinessScope: Array.from(set) }; + }); + setErrors((prev) => ({ ...prev, applyBusinessScope: 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 formValues = await form.validateFields(); + 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 = { - ...formValues, - registerUserId: router.query?.registerUserId, - productionDate: formValues.productionDate - ? dayjs(formValues.productionDate).format("YYYY-MM-DD") - : undefined, - attachmentUrls: formValues.attachmentUrls - ? JSON.stringify(formValues.attachmentUrls) + ...formRest, + // 2026-08-08 后端 registerUserId 为 Long 类型(19位,超过 Number.MAX_SAFE_INTEGER), + // 用 Number() 转换会丢失末位精度(如 2085936982110887938 → 2085936982110888000)导致落库 id 与真实账户脱节。 + // 故统一以字符串传递,由后端 Long 反序列化正确解析(与 syncUserToGBS 传参保持一致)。 + registerUserId: String(resolvedRegisterUserId), + contactName, + contactPhone, + applyBusinessScope: formData.applyBusinessScope.join(","), + attachmentUrls: formData.attachmentUrls.length + ? JSON.stringify(formData.attachmentUrls) : null, - infoDisclosureUrl: formValues.infoDisclosureUrl - ? normalizeUrl(formValues.infoDisclosureUrl) + 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) { + if (!res1.success) { + message.error(res1.message || "保存失败"); + return; + } + if (type === "submit") { + // 提交:同步用户到 GBS 并进入下一步骤 const res2 = await syncUserToGBS({ - account: router.query?.account, - registerUserId: router.query?.registerUserId, + account: resolvedAccount, + registerUserId: resolvedRegisterUserId, }); if (res2.success) { - message.success("保存成功"); + message.success("提交成功,进入认证审核"); setCurrent(1); } + } else { + // 暂存:仅保存草稿,停留在当前页,保留表单可继续编辑,不跳转、不同步 GBS + message.success("已暂存草稿,可继续编辑"); } }; - const handleMapConfirm = ({ lng, lat }) => { - form.setFieldsValue({ longitude: lng, latitude: lat }); - setMapPickerVisible(false); + const handleBack = () => { + if (window.history.length > 1) window.history.back(); }; const handleLogin = () => { @@ -150,606 +414,435 @@ const RegisterMore = (props) => { } }, [current]); + const stepTitles = ["填写信息", "认证审核中", "认证通过"]; + return (