diff --git a/src/pages/Container/EnterpriseInfo/OrgInfo/formOptions.js b/src/pages/Container/EnterpriseInfo/OrgInfo/formOptions.js index 77e1843..62d070a 100644 --- a/src/pages/Container/EnterpriseInfo/OrgInfo/formOptions.js +++ b/src/pages/Container/EnterpriseInfo/OrgInfo/formOptions.js @@ -10,6 +10,7 @@ import { } from "~/enumerate/enterpriseOptions"; import { formSelectField } from "~/utils/enterpriseForm"; import { + coordinatePairRule, creditCodeRule, latitudeRule, longitudeRule, @@ -61,7 +62,11 @@ export function buildOrgInfoFormOptions() { render: FORM_ITEM_RENDER_ENUM.INPUT_NUMBER, useConstraints: false, componentProps: { ...numberFieldProps, min: -180, max: 180, precision: 6 }, - rules: [longitudeRule(false)], + dependencies: ["latitude"], + rules: [ + longitudeRule(false), + coordinatePairRule("latitude", "经度", "纬度"), + ], }, { name: "latitude", @@ -69,7 +74,11 @@ export function buildOrgInfoFormOptions() { render: FORM_ITEM_RENDER_ENUM.INPUT_NUMBER, useConstraints: false, componentProps: { ...numberFieldProps, min: -90, max: 90, precision: 6 }, - rules: [latitudeRule(false)], + dependencies: ["longitude"], + rules: [ + latitudeRule(false), + coordinatePairRule("longitude", "纬度", "经度"), + ], }, { name: "registerAddress", diff --git a/src/pages/Container/EnterpriseInfo/OrgInfo/index.js b/src/pages/Container/EnterpriseInfo/OrgInfo/index.js index c970826..e16b250 100644 --- a/src/pages/Container/EnterpriseInfo/OrgInfo/index.js +++ b/src/pages/Container/EnterpriseInfo/OrgInfo/index.js @@ -27,10 +27,12 @@ import { import { NS_ORG_INFO } from "~/enumerate/namespace"; import { mockUploadFileList } from "~/utils/mockUpload"; import { + coordinatePairRule, creditCodeRule, latitudeRule, longitudeRule, nonNegativeIntegerRule, + normalizeUrl, phoneRule, positiveNumberRule, urlRule, @@ -89,6 +91,9 @@ function OrgInfoPage(props) { ? dayjs(formValues.productionDate).format("YYYY-MM-DD") : undefined, attachmentUrls: formValues.attachmentUrls? JSON.stringify(formValues.attachmentUrls) :null, + infoDisclosureUrl: formValues.infoDisclosureUrl + ? normalizeUrl(formValues.infoDisclosureUrl) + : undefined, authStatusCode: submitType === "draft" ? 0 : 1, authStatusName: submitType === "draft" ? "草稿" : "已提交", }; @@ -205,7 +210,11 @@ function OrgInfoPage(props) { (typeof value === "string" ? value.trim() : value)} > @@ -294,6 +308,7 @@ function OrgInfoPage(props) { name="principalPhone" label="主要负责人联系电话" rules={[phoneRule("主要负责人联系电话", true)]} + normalize={(value) => (typeof value === "string" ? value.trim() : value)} > @@ -308,6 +323,7 @@ function OrgInfoPage(props) { name="safetyDeptManagerPhone" label="安全管理部门负责人联系电话" rules={[phoneRule("安全管理部门负责人联系电话", false)]} + normalize={(value) => (typeof value === "string" ? value.trim() : value)} > (typeof value === "string" ? value.trim() : value)} > @@ -347,6 +364,7 @@ function OrgInfoPage(props) { name="infoDisclosureUrl" label="信息公开网址" rules={[urlRule("信息公开网址", false)]} + normalize={(value) => (typeof value === "string" ? value.trim() : value)} > diff --git a/src/utils/validators.js b/src/utils/validators.js index 78351f4..fa90501 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -7,8 +7,8 @@ export const ID_CARD_PATTERN = /^\d{17}[\dX]$/i; /** 手机号 */ export const MOBILE_PHONE_PATTERN = /^1[3-9]\d{9}$/; -/** 固话或手机 */ -export const PHONE_PATTERN = /^(\d{3,4}-?\d{7,8}|1[3-9]\d{9})$/; +/** 固话或手机(去格式符后:手机 11 位 / 固话 10~12 位) */ +export const PHONE_PATTERN = /^(1[3-9]\d{9}|\d{10,12})$/; /** 经度 */ export const LONGITUDE_PATTERN = /^-?((1[0-7]\d)|(\d{1,2}))(\.\d+)?$|^180(\.0+)?$/; @@ -16,21 +16,48 @@ export const LONGITUDE_PATTERN = /^-?((1[0-7]\d)|(\d{1,2}))(\.\d+)?$|^180(\.0+)? /** 纬度 */ export const LATITUDE_PATTERN = /^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$/; -/** URL */ -export const URL_PATTERN = /^(https?:\/\/)[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=.]*$/; +/** URL(需含协议或合法域名) */ +export const URL_PATTERN = /^(https?:\/\/)([\w-]+\.)+[\w-]+([\w\-._~:/?#[\]@!$&'()*+,;=.]*)?$/i; + +const FORMAT_VALIDATE_TRIGGER = ["onBlur", "onChange"]; + +function isEmptyValue(value) { + return value === undefined || value === null || value === ""; +} + +/** 电话去空格、括号、横线,并剥离 +86 前缀 */ +export function normalizePhone(value) { + return String(value).replace(/[\s()-]/g, "").replace(/^(\+?86)/, ""); +} + +/** URL 补全协议后校验 */ +export function normalizeUrl(value) { + const trimmed = String(value).trim(); + if (!trimmed) { + return ""; + } + return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; +} + +function hasTooManyDecimals(value, maxDecimals = 6) { + const str = String(value); + const dotIndex = str.indexOf("."); + if (dotIndex === -1) { + return false; + } + return str.slice(dotIndex + 1).length > maxDecimals; +} export function creditCodeRule(required = true) { return { required, - message: required ? "请输入统一社会信用代码" : undefined, - pattern: CREDIT_CODE_PATTERN, - ...(required ? {} : { validateTrigger: "onBlur" }), + validateTrigger: FORMAT_VALIDATE_TRIGGER, validator: (_, value) => { if (!value) { return required ? Promise.reject(new Error("请输入统一社会信用代码")) : Promise.resolve(); } - if (!CREDIT_CODE_PATTERN.test(value.trim())) { - return Promise.reject(new Error("统一社会信用代码格式不正确")); + if (!CREDIT_CODE_PATTERN.test(String(value).trim().toUpperCase())) { + return Promise.reject(new Error("统一社会信用代码格式不正确(18位)")); } return Promise.resolve(); }, @@ -56,13 +83,15 @@ export function idCardRule(required = true) { export function phoneRule(label = "联系电话", required = true) { return { required, + validateTrigger: FORMAT_VALIDATE_TRIGGER, + whitespace: true, validator: (_, value) => { - if (!value) { + if (!value || !String(value).trim()) { return required ? Promise.reject(new Error(`请输入${label}`)) : Promise.resolve(); } - const str = String(value).replace(/\s+/g, ""); - if (!PHONE_PATTERN.test(str)) { - return Promise.reject(new Error(`${label}格式不正确`)); + const normalized = normalizePhone(value); + if (!PHONE_PATTERN.test(normalized)) { + return Promise.reject(new Error(`${label}格式不正确(手机11位或固话区号+号码)`)); } return Promise.resolve(); }, @@ -88,14 +117,18 @@ export function mobileRule(label = "账号", required = true) { export function longitudeRule(required = false) { return { required, + validateTrigger: FORMAT_VALIDATE_TRIGGER, validator: (_, value) => { - if (value === undefined || value === null || value === "") { + if (isEmptyValue(value)) { return required ? Promise.reject(new Error("请输入经度")) : Promise.resolve(); } const num = Number(value); if (Number.isNaN(num) || num < -180 || num > 180) { return Promise.reject(new Error("经度格式不正确(-180 ~ 180)")); } + if (hasTooManyDecimals(value)) { + return Promise.reject(new Error("经度最多保留6位小数")); + } return Promise.resolve(); }, }; @@ -104,28 +137,60 @@ export function longitudeRule(required = false) { export function latitudeRule(required = false) { return { required, + validateTrigger: FORMAT_VALIDATE_TRIGGER, validator: (_, value) => { - if (value === undefined || value === null || value === "") { + if (isEmptyValue(value)) { return required ? Promise.reject(new Error("请输入纬度")) : Promise.resolve(); } const num = Number(value); if (Number.isNaN(num) || num < -90 || num > 90) { return Promise.reject(new Error("纬度格式不正确(-90 ~ 90)")); } + if (hasTooManyDecimals(value)) { + return Promise.reject(new Error("纬度最多保留6位小数")); + } return Promise.resolve(); }, }; } +/** 经纬度成对填写:填了其中一个则另一个必填 */ +export function coordinatePairRule(peerField, selfLabel, peerLabel) { + return ({ getFieldValue }) => ({ + validateTrigger: FORMAT_VALIDATE_TRIGGER, + validator: (_, value) => { + const peerValue = getFieldValue(peerField); + const hasSelf = !isEmptyValue(value); + const hasPeer = !isEmptyValue(peerValue); + if (hasSelf && !hasPeer) { + return Promise.reject(new Error(`请同时填写${peerLabel}`)); + } + if (!hasSelf && hasPeer) { + return Promise.reject(new Error(`请填写${selfLabel}`)); + } + return Promise.resolve(); + }, + }); +} + export function urlRule(label = "网址", required = false) { return { required, + validateTrigger: FORMAT_VALIDATE_TRIGGER, + whitespace: true, validator: (_, value) => { - if (!value) { + if (!value || !String(value).trim()) { return required ? Promise.reject(new Error(`请输入${label}`)) : Promise.resolve(); } - if (!URL_PATTERN.test(String(value).trim())) { - return Promise.reject(new Error(`${label}格式不正确`)); + const normalized = normalizeUrl(value); + if (!URL_PATTERN.test(normalized)) { + return Promise.reject(new Error(`${label}格式不正确(如 https://example.com)`)); + } + try { + // eslint-disable-next-line no-new + new URL(normalized); + } catch { + return Promise.reject(new Error(`${label}格式不正确(如 https://example.com)`)); } return Promise.resolve(); },