546 lines
19 KiB
JavaScript
546 lines
19 KiB
JavaScript
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 新增:业务范围在后端为 varchar(500) 的「逗号分隔字符串」(apply_business_scope),
|
||
* 而原型要求多选交互,故在页面出入口做数组/字符串互转。
|
||
* 不做字段名映射,仅做同名字段的值形态转换,保持前后端字段名一致。
|
||
*/
|
||
const splitBusinessScope = (value) => {
|
||
if (Array.isArray(value)) return value;
|
||
if (!value) return undefined;
|
||
return String(value)
|
||
.split(",")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
};
|
||
|
||
const joinBusinessScope = (value) =>
|
||
Array.isArray(value) ? value.join(",") : value || undefined;
|
||
|
||
/**
|
||
* 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) {
|
||
setDetail(res.data);
|
||
form.setFieldsValue({
|
||
...res.data,
|
||
// 2026-08-08 业务范围回填为数组以适配多选控件
|
||
applyBusinessScope: splitBusinessScope(res.data.applyBusinessScope),
|
||
// 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,
|
||
// 2026-08-08 多选业务范围按后端 varchar(500) 约定以逗号拼接提交
|
||
applyBusinessScope: joinBusinessScope(formValues.applyBusinessScope),
|
||
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={
|
||
<div>
|
||
{/* 2026-08-08 标题对齐原型 institution.html #mod-org-info(副标题按需求移除) */}
|
||
<span>基础信息管理</span>
|
||
</div>
|
||
}
|
||
extra={
|
||
hasExistingData &&
|
||
!editing && (
|
||
<Button
|
||
type="primary"
|
||
loading={orgInfoLoading}
|
||
onClick={() => setEditing(true)}
|
||
>
|
||
修改
|
||
</Button>
|
||
)
|
||
}
|
||
>
|
||
{/*
|
||
2026-08-08 布局对齐原型 institution.html:
|
||
原型 .form-grid 为两列网格、label 在控件上方(纵向),gap 1rem。
|
||
此处使用 antd Form layout="vertical" + Row/Col 两列还原,整行字段 span=24。
|
||
2026-08-08 按原型 .info-card 增加带边框容器并整体居中(border-radius 8px、padding 1.25rem、maxWidth 960px 居中)。
|
||
*/}
|
||
<div
|
||
style={{
|
||
width: "94%",
|
||
maxWidth: "100%",
|
||
margin: "0 auto",
|
||
border: "1px solid #e2e8f0",
|
||
borderRadius: 8,
|
||
padding: "1.25rem",
|
||
background: "#fff",
|
||
}}
|
||
>
|
||
<Form form={form} layout="vertical" disabled={!editing}>
|
||
<Row gutter={[16, 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>
|
||
{/* 原型:办公地址占整行(full),TextArea 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="applyBusinessScope"
|
||
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>
|
||
</div>
|
||
</PageLayout>
|
||
);
|
||
}
|
||
|
||
export default Connect([NS_ORG_INFO], true)(OrgInfoPage);
|