safety-eval-service-frontend/src/pages/Container/EnterpriseInfo/OrgInfo/index.js

506 lines
18 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 { Connect } from "@cqsjjb/jjb-dva-runtime";
import {
Button,
Checkbox,
Col,
Form,
Input,
InputNumber,
message,
Row,
Space,
} from "antd";
import { useEffect, useState } from "react";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
// 2026-08-08 变更:按原型 V1.6 institution.html 的 #mod-org-info 模块 1:1 还原机构基础信息表单。
// 复用既有「安全生产监管行业类别」枚举作为「拟申请的法定安全评价业务范围」候选项,
// 二者取值集合完全一致(煤矿/金属非金属矿/陆地油气/油气管道/石油化工医药/烟花爆竹/金属冶炼),无需新增枚举。
import { QUALIFICATION_INDUSTRY_OPTIONS } from "~/enumerate/enterpriseOptions";
import { NS_ORG_INFO } from "~/enumerate/namespace";
import {
creditCodeRule,
nonNegativeIntegerRule,
normalizeUrl,
phoneRule,
positiveNumberRule,
urlRule,
} from "~/utils/validators";
/**
* 2026-08-08 联系人及电话合并/拆分工具(交互约定见下方说明):
* 录入:单输入框,值格式「联系人/电话」,用 / 分隔。
* 提交前:按首个 / 拆为 contactName/前)+ contactPhone/后)两字段,传给后端。
* 回显:后端 contactName/contactPhone 用 / 合并回单输入框。
* 后端落库contact_name varchar(50)、contact_phone varchar(20);原 contact_name_phone 列保留不删、不再写入。
* 复用要点:其他页面如需同样交互,保持「单输入框 + / 分隔 + 提交拆分存两列 + 回显合并」即可。
* 注意:分隔符为单斜杠「/」(无空格),与录入示例"张三/13800138000"一致。
*/
const CONTACT_SEP = "/";
const mergeContactNamePhone = (name, phone) => {
if (!name && !phone) return undefined;
return [name, phone].filter(Boolean).join(CONTACT_SEP);
};
const splitContactNamePhone = (value) => {
if (!value) return { contactName: undefined, contactPhone: undefined };
const idx = String(value).indexOf(CONTACT_SEP);
if (idx >= 0) {
return {
// 按首个 / 拆分;/后全部作为电话(剔除首尾空白),多余字符交给校验/提交时处理
contactName: String(value).slice(0, idx).trim(),
contactPhone: String(value).slice(idx + 1).trim(),
};
}
// 无分隔符:整体作为联系人姓名,电话置空(由校验拦截)
return { contactName: String(value).trim(), contactPhone: undefined };
};
function OrgInfoPage(props) {
const [form] = Form.useForm();
const { orgInfoLoading } = props.orgInfo;
const [editing, setEditing] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [detail, setDetail] = useState({});
/** 是否已存在机构数据(有 id 视为已入库,只能修改) */
const [hasExistingData, setHasExistingData] = useState(false);
const loadDetail = async () => {
try {
const res = await props.orgInfoGet();
if (res?.data?.id) {
sessionStorage.setItem("orgInfo", JSON.stringify(res.data));
setDetail(res.data);
form.setFieldsValue({
...res.data,
// 2026-08-08 联系人及电话:后端 contactName/contactPhone 两列合并为单字段展示
contactNamePhone: mergeContactNamePhone(
res.data.contactName,
res.data.contactPhone,
),
});
setHasExistingData(true);
setEditing(false);
} else {
setDetail({});
setHasExistingData(false);
setEditing(true);
}
} catch (err) {
console.warn("[OrgInfo] loadDetail failed:", err);
setDetail({});
setHasExistingData(false);
setEditing(true);
}
};
useEffect(() => {
loadDetail();
}, []);
const handleCancelEdit = () => {
setEditing(false);
};
const handleSave = async (submitType) => {
try {
const formValues = await form.validateFields();
// 2026-08-08 联系人及电话:单字段拆分为后端 contactName / contactPhone 两列提交
const split = splitContactNamePhone(formValues.contactNamePhone);
// 剔除多余字符:姓名/电话做 trim 与空白清理(校验已保证格式,此处仅兜底)
const contactName = split.contactName
? split.contactName.replace(/\s+/g, "")
: undefined;
const contactPhone = split.contactPhone
? split.contactPhone.replace(/\s+/g, "")
: undefined;
const values = {
...formValues,
contactName,
contactPhone,
// 移除仅用于展示的合并字段,避免提交冗余
contactNamePhone: undefined,
infoDisclosureUrl: formValues.infoDisclosureUrl
? normalizeUrl(formValues.infoDisclosureUrl)
: undefined,
authStatusCode: submitType === "draft" ? 0 : 1,
authStatusName: submitType === "draft" ? "草稿" : "已提交",
};
setSubmitting(true);
let request;
if (hasExistingData) {
request = props.orgInfoModify;
values.id = detail.id;
} else {
request =
submitType === "draft" ? props.orgInfoDraft : props.orgInfoSave;
}
const res = await request(values);
if (res?.success !== false) {
message.success(
hasExistingData
? "修改成功"
: submitType === "draft"
? "暂存成功"
: "提交成功",
);
setEditing(false);
loadDetail();
}
} finally {
setSubmitting(false);
}
};
return (
<PageLayout
title='基础信息管理'
extra={
hasExistingData &&
!editing && (
<Button
type="primary"
loading={orgInfoLoading}
onClick={() => setEditing(true)}
>
修改
</Button>
)
}
>
<Form form={form} layout="vertical" disabled={!editing} scrollToFirstError>
<Row gutter={16}>
{/* 2026-08-08 字段顺序严格对齐原型 institution.html 434-450 行 */}
<Col span={12}>
<Form.Item
name="unitName"
label="单位名称"
rules={[{ required: true, message: "请输入单位名称" }]}
>
<Input placeholder="请输入单位名称" allowClear maxLength={200} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="creditCode"
label="统一社会信用代码"
rules={[creditCodeRule(true)]}
>
<Input
placeholder="请输入统一社会信用代码"
allowClear
maxLength={18}
/>
</Form.Item>
</Col>
{/* 原型注册地址占整行full */}
<Col span={24}>
<Form.Item
name="registerAddress"
label="注册地址"
rules={[{ required: true, message: "请输入注册地址" }]}
>
<Input placeholder="请输入注册地址" maxLength={500} />
</Form.Item>
</Col>
{/* 原型办公地址占整行fullTextArea rows=3 */}
<Col span={24}>
<Form.Item
name="businessAddress"
label="办公地址"
rules={[{ required: true, message: "请输入办公地址" }]}
>
<Input.TextArea
rows={3}
placeholder="请输入办公地址"
maxLength={500}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="infoDisclosureUrl"
label="信息公开网址"
rules={[urlRule("信息公开网址", true)]}
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
>
<Input
placeholder="请输入信息公开网址"
allowClear
maxLength={500}
/>
</Form.Item>
</Col>
{/* 原型:资质证书编号非必填,带"非必填,初次申请无需填写"提示 */}
<Col span={12}>
<Form.Item
name="qualificationCertNo"
label="资质证书编号"
extra="非必填,初次申请无需填写"
>
<Input
placeholder="初次申请无需填写"
allowClear
maxLength={100}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="legalRepresentative"
label="法定代表人"
rules={[{ required: true, message: "请输入法定代表人" }]}
>
<Input placeholder="请输入法定代表人" allowClear maxLength={50} />
</Form.Item>
</Col>
{/* 2026-08-08 标签对齐原型「法定代表人电话」 */}
<Col span={12}>
<Form.Item
name="legalRepresentativePhone"
label="法定代表人电话"
rules={[phoneRule("法定代表人电话", true)]}
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
>
<Input
placeholder="请输入法定代表人电话"
allowClear
maxLength={20}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="fax" label="传真" rules={[{ required: true, message: "请输入传真" }]}>
<Input placeholder="请输入传真" allowClear maxLength={20} />
</Form.Item>
</Col>
{/*
2026-08-08 联系人及电话(交互约定):
单输入框录入,格式「联系人/电话」(用 / 分隔),如:张三/13800138000
提交前按首个 / 拆为 contactName/前)+ contactPhone/后)存两列;
回显时后端 contactName/contactPhone 用 / 合并回填。
校验:拆分后 contactName 非空且≤50、contactPhone 非空且匹配手机/座机格式且≤20。
*/}
<Col span={12}>
<Form.Item
name="contactNamePhone"
label="联系人及电话"
validateFirst
rules={[
{ required: true, message: "请输入联系人及电话" },
{
validator: (_, value) => {
const { contactName, contactPhone } =
splitContactNamePhone(value);
if (!contactName) {
return Promise.reject(new Error("联系人不能为空"));
}
if (contactName.length > 50) {
return Promise.reject(new Error("联系人不能超过50字"));
}
if (!contactPhone) {
return Promise.reject(
new Error("请使用/分隔联系人和电话,并填写电话"),
);
}
// 复用既有 PHONE_PATTERN手机11位 / 固话10~12位去空白后
if (
!/^(1[3-9]\d{9}|\d{10,12})$/.test(
String(contactPhone).replace(/\s+/g, ""),
)
) {
return Promise.reject(
new Error("电话格式不正确手机11位或固话区号+号码)"),
);
}
if (contactPhone.length > 20) {
return Promise.reject(new Error("电话不能超过20字"));
}
return Promise.resolve();
},
},
]}
extra="请使用/分隔联系人和电话,如:张三/13800138000"
>
<Input
placeholder="如:张三/13800138000"
allowClear
maxLength={73}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="fixedAssetsTotal"
label="固定资产总值(万元)"
rules={[
{ required: true, message: "请输入固定资产总值" },
{
validator: (_, value) => {
if (value === undefined || value === null || value === "") {
return Promise.resolve();
}
if (Number(value) < 0) {
return Promise.reject(
new Error("固定资产总值应为非负数字"),
);
}
return Promise.resolve();
},
},
]}
>
<InputNumber
style={{ width: "100%" }}
min={0}
precision={4}
placeholder="请输入固定资产总值"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="workplaceArea"
label="工作场所建筑面积(㎡)"
rules={[positiveNumberRule("工作场所建筑面积", true)]}
>
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={2}
placeholder="请输入工作场所建筑面积"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="archiveRoomArea"
label="档案室面积(㎡)"
rules={[positiveNumberRule("档案室面积", true)]}
>
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={2}
placeholder="请输入档案室面积"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="fulltimeEvaluatorCount"
label="专职安全评价师数量"
rules={[nonNegativeIntegerRule("专职安全评价师数量", true)]}
>
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={0}
placeholder="请输入专职安全评价师数量"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="registeredEngineerCount"
label="注册安全工程师数量"
rules={[nonNegativeIntegerRule("注册安全工程师数量", true)]}
>
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={0}
placeholder="请输入注册安全工程师数量"
/>
</Form.Item>
</Col>
{/*
2026-08-08 业务范围:
原型为 checkbox 网格2列、灰底 #f8fafc、边框容器此处用 Checkbox.Group + Row/Col 还原视觉。
*/}
<Col span={24}>
<Form.Item
name="safetyIndustryCategoryCode"
label="拟申请的法定安全评价业务范围"
rules={[
{
required: true,
message: "请选择拟申请的法定安全评价业务范围",
},
]}
>
<Checkbox.Group
style={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: "0.65rem 1rem",
padding: "1rem",
border: "1px solid #e2e8f0",
borderRadius: 8,
background: "#f8fafc",
}}
>
{QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => (
<Checkbox key={opt.value} value={opt.value}>
{opt.label}
</Checkbox>
))}
</Checkbox.Group>
</Form.Item>
</Col>
{/* 原型单位基本情况介绍可附页TextArea rows=7 */}
<Col span={24}>
<Form.Item
name="orgIntro"
label="单位基本情况介绍(可附页)"
rules={[{ required: true, message: "请输入单位基本情况介绍" }]}
>
<Input.TextArea
rows={7}
placeholder="请简要介绍单位基本情况,如成立时间、人员构成、技术力量、业绩等"
maxLength={2000}
showCount
/>
</Form.Item>
</Col>
</Row>
{editing && (
<div style={{ marginTop: 24, textAlign: "center" }}>
<Space>
<Button onClick={handleCancelEdit}>取消</Button>
{!hasExistingData && (
<Button loading={submitting} onClick={() => handleSave("draft")}>
暂存
</Button>
)}
<Button
type="primary"
loading={submitting}
onClick={() => handleSave("submit")}
>
提交
</Button>
</Space>
</div>
)}
</Form>
</PageLayout>
);
}
export default Connect([NS_ORG_INFO], true)(OrgInfoPage);