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

506 lines
18 KiB
JavaScript
Raw Normal View History

2026-06-23 18:07:30 +08:00
import { Connect } from "@cqsjjb/jjb-dva-runtime";
2026-06-29 16:45:58 +08:00
import {
Button,
2026-08-08 14:42:25 +08:00
Checkbox,
2026-06-29 16:45:58 +08:00
Col,
Form,
Input,
InputNumber,
message,
Row,
Space,
} from "antd";
2026-06-23 18:07:30 +08:00
import { useEffect, useState } from "react";
2026-06-29 16:45:58 +08:00
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
2026-08-08 14:42:25 +08:00
// 2026-08-08 变更:按原型 V1.6 institution.html 的 #mod-org-info 模块 1:1 还原机构基础信息表单。
// 复用既有「安全生产监管行业类别」枚举作为「拟申请的法定安全评价业务范围」候选项,
// 二者取值集合完全一致(煤矿/金属非金属矿/陆地油气/油气管道/石油化工医药/烟花爆竹/金属冶炼),无需新增枚举。
import { QUALIFICATION_INDUSTRY_OPTIONS } from "~/enumerate/enterpriseOptions";
2026-06-23 18:07:30 +08:00
import { NS_ORG_INFO } from "~/enumerate/namespace";
2026-07-09 13:15:13 +08:00
2026-06-29 16:45:58 +08:00
import {
creditCodeRule,
nonNegativeIntegerRule,
2026-07-09 09:43:00 +08:00
normalizeUrl,
2026-06-29 16:45:58 +08:00
phoneRule,
positiveNumberRule,
urlRule,
} from "~/utils/validators";
2026-06-24 11:58:57 +08:00
2026-08-08 14:42:25 +08:00
/**
* 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 };
};
2026-06-23 18:07:30 +08:00
function OrgInfoPage(props) {
const [form] = Form.useForm();
2026-06-29 16:45:58 +08:00
const { orgInfoLoading } = props.orgInfo;
2026-06-23 18:07:30 +08:00
const [editing, setEditing] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [detail, setDetail] = useState({});
2026-06-24 11:58:57 +08:00
/** 是否已存在机构数据(有 id 视为已入库,只能修改) */
const [hasExistingData, setHasExistingData] = useState(false);
2026-06-23 18:07:30 +08:00
const loadDetail = async () => {
try {
2026-06-29 16:45:58 +08:00
const res = await props.orgInfoGet();
2026-06-24 11:58:57 +08:00
if (res?.data?.id) {
2026-08-11 11:20:20 +08:00
sessionStorage.setItem("orgInfo", JSON.stringify(res.data));
2026-06-23 18:07:30 +08:00
setDetail(res.data);
2026-06-29 16:45:58 +08:00
form.setFieldsValue({
...res.data,
2026-08-08 14:42:25 +08:00
// 2026-08-08 联系人及电话:后端 contactName/contactPhone 两列合并为单字段展示
contactNamePhone: mergeContactNamePhone(
res.data.contactName,
res.data.contactPhone,
),
2026-06-29 16:45:58 +08:00
});
2026-06-24 11:58:57 +08:00
setHasExistingData(true);
2026-06-23 18:07:30 +08:00
setEditing(false);
2026-06-29 16:45:58 +08:00
} else {
2026-06-24 11:58:57 +08:00
setDetail({});
setHasExistingData(false);
2026-06-23 18:07:30 +08:00
setEditing(true);
}
2026-06-29 16:45:58 +08:00
} catch (err) {
2026-06-23 18:07:30 +08:00
console.warn("[OrgInfo] loadDetail failed:", err);
2026-06-24 11:58:57 +08:00
setDetail({});
setHasExistingData(false);
2026-06-23 18:07:30 +08:00
setEditing(true);
}
};
useEffect(() => {
loadDetail();
}, []);
2026-06-24 11:58:57 +08:00
const handleCancelEdit = () => {
2026-07-09 13:15:13 +08:00
setEditing(false);
2026-06-24 11:58:57 +08:00
};
2026-06-23 18:07:30 +08:00
const handleSave = async (submitType) => {
try {
2026-06-29 16:45:58 +08:00
const formValues = await form.validateFields();
2026-08-08 14:42:25 +08:00
// 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;
2026-06-29 16:45:58 +08:00
const values = {
...formValues,
2026-08-08 14:42:25 +08:00
contactName,
contactPhone,
// 移除仅用于展示的合并字段,避免提交冗余
contactNamePhone: undefined,
2026-07-09 09:43:00 +08:00
infoDisclosureUrl: formValues.infoDisclosureUrl
? normalizeUrl(formValues.infoDisclosureUrl)
: undefined,
2026-07-06 16:22:51 +08:00
authStatusCode: submitType === "draft" ? 0 : 1,
authStatusName: submitType === "draft" ? "草稿" : "已提交",
2026-06-29 16:45:58 +08:00
};
2026-06-23 18:07:30 +08:00
setSubmitting(true);
2026-06-24 11:58:57 +08:00
let request;
if (hasExistingData) {
2026-07-06 16:22:51 +08:00
request = props.orgInfoModify;
2026-06-24 11:58:57 +08:00
values.id = detail.id;
2026-06-29 16:45:58 +08:00
} else {
request =
submitType === "draft" ? props.orgInfoDraft : props.orgInfoSave;
2026-06-24 11:58:57 +08:00
}
2026-06-23 18:07:30 +08:00
const res = await request(values);
if (res?.success !== false) {
2026-06-24 11:58:57 +08:00
message.success(
hasExistingData
? "修改成功"
2026-06-29 16:45:58 +08:00
: submitType === "draft"
? "暂存成功"
: "提交成功",
2026-06-24 11:58:57 +08:00
);
2026-06-23 18:07:30 +08:00
setEditing(false);
loadDetail();
}
2026-07-09 13:15:13 +08:00
} finally {
2026-06-23 18:07:30 +08:00
setSubmitting(false);
}
};
return (
2026-06-29 16:45:58 +08:00
<PageLayout
2026-08-11 11:20:20 +08:00
title='基础信息管理'
2026-06-29 16:45:58 +08:00
extra={
hasExistingData &&
!editing && (
2026-06-30 17:43:05 +08:00
<Button
type="primary"
loading={orgInfoLoading}
onClick={() => setEditing(true)}
>
2026-06-23 18:07:30 +08:00
修改
</Button>
2026-06-29 16:45:58 +08:00
)
}
>
2026-08-11 11:20:20 +08:00
2026-08-11 13:46:02 +08:00
<Form form={form} layout="vertical" disabled={!editing} scrollToFirstError>
2026-08-11 11:20:20 +08:00
<Row gutter={16}>
2026-08-08 14:42:25 +08:00
{/* 2026-08-08 字段顺序严格对齐原型 institution.html 434-450 行 */}
2026-06-29 16:45:58 +08:00
<Col span={12}>
<Form.Item
2026-07-06 16:22:51 +08:00
name="unitName"
2026-08-08 14:42:25 +08:00
label="单位名称"
rules={[{ required: true, message: "请输入单位名称" }]}
2026-06-29 16:45:58 +08:00
>
2026-08-08 14:42:25 +08:00
<Input placeholder="请输入单位名称" allowClear maxLength={200} />
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="creditCode"
label="统一社会信用代码"
rules={[creditCodeRule(true)]}
>
2026-07-14 10:09:08 +08:00
<Input
placeholder="请输入统一社会信用代码"
allowClear
maxLength={18}
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/* 原型注册地址占整行full */}
<Col span={24}>
2026-06-29 16:45:58 +08:00
<Form.Item
name="registerAddress"
label="注册地址"
rules={[{ required: true, message: "请输入注册地址" }]}
>
2026-08-08 14:42:25 +08:00
<Input placeholder="请输入注册地址" maxLength={500} />
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/* 原型办公地址占整行fullTextArea rows=3 */}
<Col span={24}>
2026-06-29 16:45:58 +08:00
<Form.Item
name="businessAddress"
2026-08-08 14:42:25 +08:00
label="办公地址"
rules={[{ required: true, message: "请输入办公地址" }]}
2026-06-29 16:45:58 +08:00
>
2026-07-14 10:09:08 +08:00
<Input.TextArea
rows={3}
2026-08-08 14:42:25 +08:00
placeholder="请输入办公地址"
maxLength={500}
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
2026-08-08 14:42:25 +08:00
name="infoDisclosureUrl"
label="信息公开网址"
rules={[urlRule("信息公开网址", true)]}
2026-07-09 13:15:13 +08:00
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
2026-06-29 16:45:58 +08:00
>
2026-07-14 10:09:08 +08:00
<Input
2026-08-08 14:42:25 +08:00
placeholder="请输入信息公开网址"
2026-07-14 10:09:08 +08:00
allowClear
2026-08-08 14:42:25 +08:00
maxLength={500}
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/* 原型:资质证书编号非必填,带"非必填,初次申请无需填写"提示 */}
2026-06-29 16:45:58 +08:00
<Col span={12}>
<Form.Item
2026-08-08 14:42:25 +08:00
name="qualificationCertNo"
label="资质证书编号"
extra="非必填,初次申请无需填写"
2026-06-29 16:45:58 +08:00
>
2026-07-14 10:09:08 +08:00
<Input
2026-08-08 14:42:25 +08:00
placeholder="初次申请无需填写"
2026-07-14 10:09:08 +08:00
allowClear
2026-08-08 14:42:25 +08:00
maxLength={100}
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
2026-08-08 14:42:25 +08:00
name="legalRepresentative"
label="法定代表人"
rules={[{ required: true, message: "请输入法定代表人" }]}
2026-06-29 16:45:58 +08:00
>
2026-08-08 14:42:25 +08:00
<Input placeholder="请输入法定代表人" allowClear maxLength={50} />
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/* 2026-08-08 标签对齐原型「法定代表人电话」 */}
2026-06-29 16:45:58 +08:00
<Col span={12}>
<Form.Item
2026-08-08 14:42:25 +08:00
name="legalRepresentativePhone"
label="法定代表人电话"
rules={[phoneRule("法定代表人电话", true)]}
2026-07-09 13:15:13 +08:00
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
2026-06-29 16:45:58 +08:00
>
2026-07-14 10:09:08 +08:00
<Input
2026-08-08 14:42:25 +08:00
placeholder="请输入法定代表人电话"
2026-07-14 10:09:08 +08:00
allowClear
2026-08-08 14:42:25 +08:00
maxLength={20}
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
2026-08-08 14:42:25 +08:00
<Form.Item name="fax" label="传真" rules={[{ required: true, message: "请输入传真" }]}>
<Input placeholder="请输入传真" allowClear maxLength={20} />
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/*
2026-08-08 联系人及电话交互约定
单输入框录入格式联系人/电话 / 分隔张三/13800138000
提交前按首个 / 拆为 contactName/+ contactPhone/存两列
回显时后端 contactName/contactPhone / 合并回填
校验拆分后 contactName 非空且50contactPhone 非空且匹配手机/座机格式且20
*/}
2026-06-29 16:45:58 +08:00
<Col span={12}>
2026-08-08 14:42:25 +08:00
<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"
>
2026-07-14 10:09:08 +08:00
<Input
2026-08-08 14:42:25 +08:00
placeholder="如:张三/13800138000"
2026-07-14 10:09:08 +08:00
allowClear
2026-08-08 14:42:25 +08:00
maxLength={73}
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
2026-08-08 14:42:25 +08:00
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();
},
},
]}
2026-06-29 16:45:58 +08:00
>
2026-08-08 14:42:25 +08:00
<InputNumber
style={{ width: "100%" }}
min={0}
precision={4}
placeholder="请输入固定资产总值"
2026-07-14 10:09:08 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="workplaceArea"
2026-08-08 14:42:25 +08:00
label="工作场所建筑面积(㎡)"
rules={[positiveNumberRule("工作场所建筑面积", true)]}
2026-06-29 16:45:58 +08:00
>
2026-06-30 17:43:05 +08:00
<InputNumber
style={{ width: "100%" }}
min={0}
2026-07-14 10:09:08 +08:00
max={99999}
2026-06-30 17:43:05 +08:00
precision={2}
placeholder="请输入工作场所建筑面积"
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="archiveRoomArea"
2026-08-08 14:42:25 +08:00
label="档案室面积(㎡)"
rules={[positiveNumberRule("档案室面积", true)]}
2026-06-29 16:45:58 +08:00
>
2026-06-30 17:43:05 +08:00
<InputNumber
style={{ width: "100%" }}
min={0}
2026-07-14 10:09:08 +08:00
max={99999}
2026-06-30 17:43:05 +08:00
precision={2}
placeholder="请输入档案室面积"
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
2026-07-06 16:22:51 +08:00
name="fulltimeEvaluatorCount"
2026-06-29 16:45:58 +08:00
label="专职安全评价师数量"
2026-08-08 14:42:25 +08:00
rules={[nonNegativeIntegerRule("专职安全评价师数量", true)]}
2026-06-29 16:45:58 +08:00
>
2026-06-30 17:43:05 +08:00
<InputNumber
style={{ width: "100%" }}
min={0}
2026-07-14 10:09:08 +08:00
max={99999}
2026-06-30 17:43:05 +08:00
precision={0}
placeholder="请输入专职安全评价师数量"
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
2026-07-06 16:22:51 +08:00
name="registeredEngineerCount"
2026-06-29 16:45:58 +08:00
label="注册安全工程师数量"
2026-08-08 14:42:25 +08:00
rules={[nonNegativeIntegerRule("注册安全工程师数量", true)]}
2026-06-29 16:45:58 +08:00
>
2026-06-30 17:43:05 +08:00
<InputNumber
style={{ width: "100%" }}
min={0}
2026-07-14 10:09:08 +08:00
max={99999}
2026-06-30 17:43:05 +08:00
precision={0}
placeholder="请输入注册安全工程师数量"
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/*
2026-08-08 业务范围
原型为 checkbox 网格2灰底 #f8fafc边框容器此处用 Checkbox.Group + Row/Col 还原视觉
*/}
<Col span={24}>
<Form.Item
2026-08-11 11:20:20 +08:00
name="safetyIndustryCategoryCode"
2026-08-08 14:42:25 +08:00
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",
2026-07-09 13:15:13 +08:00
}}
2026-08-08 14:42:25 +08:00
>
{QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => (
<Checkbox key={opt.value} value={opt.value}>
{opt.label}
</Checkbox>
))}
</Checkbox.Group>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
{/* 原型单位基本情况介绍可附页TextArea rows=7 */}
<Col span={24}>
<Form.Item
name="orgIntro"
label="单位基本情况介绍(可附页)"
rules={[{ required: true, message: "请输入单位基本情况介绍" }]}
>
<Input.TextArea
rows={7}
placeholder="请简要介绍单位基本情况,如成立时间、人员构成、技术力量、业绩等"
maxLength={2000}
showCount
2026-06-30 17:43:05 +08:00
/>
2026-06-29 16:45:58 +08:00
</Form.Item>
</Col>
2026-08-08 14:42:25 +08:00
</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>
2026-08-11 11:20:20 +08:00
2026-06-29 16:45:58 +08:00
</PageLayout>
2026-06-23 18:07:30 +08:00
);
}
export default Connect([NS_ORG_INFO], true)(OrgInfoPage);