tangjie 2026-08-10 10:53:54 +08:00
commit 8f73ce1086
5 changed files with 351 additions and 170 deletions

View File

@ -10,10 +10,10 @@ module.exports = {
javaGitBranch: "dev", javaGitBranch: "dev",
// 本地联调 safetyEval-servicecontext-path: /safetyEval默认端口 8095 // 本地联调 safetyEval-servicecontext-path: /safetyEval默认端口 8095
// 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095 // 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095
// API_HOST: "https://gbs-gateway.qhdsafety.com", API_HOST: "http://localhost:80",
// API_HOST: "http://192.168.0.134", // API_HOST: "http://192.168.0.134",
API_HOST: "http://192.168.0.150", //太浅 // API_HOST: "http://192.168.0.150", //太浅
// API_HOST: "http://192.168.0.152", // API_HOST: "http://192.168.0.152",
//API_HOST: "http://192.168.0.103", //huwei //API_HOST: "http://192.168.0.103", //huwei
}, },

View File

@ -8,6 +8,12 @@ export const staffInfoList = declareRequest(
"staffInfoList: [] | res.data || [] & staffInfoTotal: 0 | res.total || 0", "staffInfoList: [] | res.data || [] & staffInfoTotal: 0 | res.total || 0",
); );
export const staffInfoDeptPersonCount = declareRequest(
"staffInfoLoading",
"Get > /safetyEval/org-personnel/deptPersonCount",
"staffInfoDeptPersonCount: [] | res.data || []",
);
export const staffInfoGet = declareRequest( export const staffInfoGet = declareRequest(
"staffInfoLoading", "staffInfoLoading",
"Get > /safetyEval/org-personnel/get", "Get > /safetyEval/org-personnel/get",

View File

@ -41,6 +41,17 @@ function findDeptNode(nodes = [], id) {
return null; return null;
} }
function collectSelfAndDescendantIds(node) {
if (!node) {
return [];
}
const ids = [asId(node.id)].filter(Boolean);
(node.children || []).forEach((child) => {
ids.push(...collectSelfAndDescendantIds(child));
});
return ids;
}
function toSelectedDept(node) { function toSelectedDept(node) {
if (!node) { if (!node) {
return null; return null;
@ -180,6 +191,30 @@ function DepartmentPositionPage(props) {
okText: "是", okText: "是",
cancelText: "否", cancelText: "否",
onOk: async () => { onOk: async () => {
try {
const node = findDeptNode(treeData, id);
const deptIds = collectSelfAndDescendantIds(node);
const countRes = await props.staffInfoDeptPersonCount?.();
const countMap = {};
(countRes?.data || []).forEach((item) => {
const deptId = asId(item.deptId);
if (deptId) {
countMap[deptId] = Number(item.personCount) || 0;
}
});
const personCount = deptIds.reduce(
(sum, deptId) => sum + (countMap[deptId] || 0),
0,
);
if (personCount > 0) {
message.warning("该部门(含子部门)下存在人员,无法删除");
return;
}
} catch (err) {
console.warn("[DepartmentPosition] check dept personnel failed:", err);
message.warning("校验部门人员失败,请稍后重试");
return;
}
const res = await props.orgDepartmentRemove({ data: id }); const res = await props.orgDepartmentRemove({ data: id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");
@ -199,6 +234,22 @@ function DepartmentPositionPage(props) {
okText: "是", okText: "是",
cancelText: "否", cancelText: "否",
onOk: async () => { onOk: async () => {
try {
const res = await props.staffInfoList?.({
postId: asId(id),
current: 1,
size: 1,
});
const total = Number(res?.total ?? res?.totalCount ?? 0);
if (total > 0) {
message.warning("该岗位下存在人员,无法删除");
return;
}
} catch (err) {
console.warn("[DepartmentPosition] check position personnel failed:", err);
message.warning("校验岗位人员失败,请稍后重试");
return;
}
const res = await props.orgPositionRemove({ data: id }); const res = await props.orgPositionRemove({ data: id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");

View File

@ -12,13 +12,14 @@ import {
Select, Select,
Space, Space,
Table, Table,
Tree,
TreeSelect, TreeSelect,
Upload, Upload,
Tag, Tag,
} from "antd"; } from "antd";
import { InboxOutlined } from "@ant-design/icons"; import { InboxOutlined } from "@ant-design/icons";
import { Get } from "@cqsjjb/jjb-common-lib/http"; import { Get } from "@cqsjjb/jjb-common-lib/http";
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
@ -49,11 +50,13 @@ import { idCardRule, mobileRule } from "~/utils/validators";
import AttachmentUpload from "~/components/AttachmentUpload"; import AttachmentUpload from "~/components/AttachmentUpload";
import StaffViewModal from "~/components/StaffViewModal"; import StaffViewModal from "~/components/StaffViewModal";
import StaffResumeModal from "~/components/StaffResumeModal"; import StaffResumeModal from "~/components/StaffResumeModal";
import { asId } from "~/utils/enterpriseInfo/idUtil";
import "./index.less"; import "./index.less";
const { router } = tools; const { router } = tools;
const API_HOST = window.process?.env?.app?.API_HOST || ""; const API_HOST = window.process?.env?.app?.API_HOST || "";
const evalCertLevelMap = CERT_LEVEL_LABEL_BY_TYPE.evaluator; const evalCertLevelMap = CERT_LEVEL_LABEL_BY_TYPE.evaluator;
const ALL_DEPT_KEY = "ALL";
function PersonnelInfoPage(props) { function PersonnelInfoPage(props) {
const [formModalOpen, setFormModalOpen] = useState(false); const [formModalOpen, setFormModalOpen] = useState(false);
@ -65,6 +68,11 @@ function PersonnelInfoPage(props) {
const [searchForm] = Form.useForm(); const [searchForm] = Form.useForm();
const [deptOptions, setDeptOptions] = useState([]); const [deptOptions, setDeptOptions] = useState([]);
const [positionOptions, setPositionOptions] = useState([]); const [positionOptions, setPositionOptions] = useState([]);
const [treeData, setTreeData] = useState([]);
const [deptCountMap, setDeptCountMap] = useState({});
const [selectedDeptId, setSelectedDeptId] = useState(
asId(router.query.deptId) || ALL_DEPT_KEY,
);
const { staffInfo, orgDepartmentTree, orgDepartment } = props; const { staffInfo, orgDepartmentTree, orgDepartment } = props;
const { orgDepartmentTreeData } = orgDepartment; const { orgDepartmentTreeData } = orgDepartment;
const { const {
@ -73,8 +81,36 @@ function PersonnelInfoPage(props) {
staffInfoLoading: loading, staffInfoLoading: loading,
} = staffInfo || {}; } = staffInfo || {};
const loadDeptPersonCount = async () => {
try {
const res = await props.staffInfoDeptPersonCount?.();
const map = {};
(res?.data || []).forEach((item) => {
const id = asId(item.deptId);
if (id) {
map[id] = Number(item.personCount) || 0;
}
});
setDeptCountMap(map);
} catch (err) {
console.warn("[PersonnelInfo] load deptPersonCount failed:", err);
setDeptCountMap({});
}
};
const loadTree = async () => {
try {
const res = await orgDepartmentTree();
setTreeData(res?.data || []);
} catch (err) {
console.warn("[PersonnelInfo] loadTree failed:", err);
setTreeData([]);
}
};
useEffect(() => { useEffect(() => {
orgDepartmentTree(); loadTree();
loadDeptPersonCount();
let cancelled = false; let cancelled = false;
(async () => { (async () => {
try { try {
@ -100,20 +136,62 @@ function PersonnelInfoPage(props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
searchForm.setFieldsValue(router.query); const { deptId: _ignored, ...rest } = router.query || {};
searchForm.setFieldsValue(rest);
handleSearch(); handleSearch();
}, []); }, []);
const handleSearch = () => { const handleSearch = () => {
props.staffInfoList({ ...router.query }); const deptId =
selectedDeptId && selectedDeptId !== ALL_DEPT_KEY
? asId(selectedDeptId)
: undefined;
props.staffInfoList({
...router.query,
deptId,
});
}; };
const goCertificate = (id, staffName) => { const refreshListAndCount = () => {
props.history.push( handleSearch();
`Certificate?staffId=${id}&staffName=${encodeURIComponent(staffName || "")}`, loadDeptPersonCount();
);
}; };
const applyDeptFilter = (deptId) => {
const nextId = deptId || ALL_DEPT_KEY;
setSelectedDeptId(nextId);
const queryDeptId = nextId === ALL_DEPT_KEY ? undefined : asId(nextId);
router.query = {
...router.query,
deptId: queryDeptId,
current: 1,
};
props.staffInfoList({
...router.query,
deptId: queryDeptId,
});
};
const displayTree = useMemo(
() => [
{
id: ALL_DEPT_KEY,
deptName: "全部人员",
children: treeData,
},
],
[treeData],
);
const totalPersonCount = useMemo(
() =>
Object.values(deptCountMap).reduce(
(sum, n) => sum + (Number(n) || 0),
0,
),
[deptCountMap],
);
const onDelete = (id) => { const onDelete = (id) => {
Modal.confirm({ Modal.confirm({
title: "提示", title: "提示",
@ -124,7 +202,7 @@ function PersonnelInfoPage(props) {
const res = await props.staffInfoRemove({ id }); const res = await props.staffInfoRemove({ id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");
handleSearch(); refreshListAndCount();
} }
}, },
}); });
@ -160,6 +238,31 @@ function PersonnelInfoPage(props) {
</Space> </Space>
} }
> >
<div className="personnel-info-layout">
<div className="personnel-info-tree">
<div className="personnel-info-tree-title">组织机构</div>
<Tree
key={treeData.length ? "dept-tree-loaded" : "dept-tree-empty"}
selectedKeys={[asId(selectedDeptId) || ALL_DEPT_KEY]}
treeData={displayTree}
defaultExpandAll
fieldNames={{ title: "deptName", key: "id", children: "children" }}
titleRender={(node) => {
if (node.id === ALL_DEPT_KEY) {
return `全部人员 (${totalPersonCount}人)`;
}
const count = deptCountMap[asId(node.id)] ?? 0;
return `${node.deptName || ""} (${count}人)`;
}}
onSelect={(keys, { node }) => {
if (!keys?.length) {
return;
}
applyDeptFilter(node.id);
}}
/>
</div>
<div className="personnel-info-main">
<SearchForm <SearchForm
style={{ marginBottom: 24 }} style={{ marginBottom: 24 }}
form={searchForm} form={searchForm}
@ -177,20 +280,6 @@ function PersonnelInfoPage(props) {
maxLength={50} maxLength={50}
/> />
</Form.Item>, </Form.Item>,
<Form.Item key="deptId" name="deptId">
<ControlWrapper.TreeSelect
placeholder="请选择部门"
treeData={orgDepartmentTreeData}
allowClear
fieldNames={{
label: "deptName",
value: "id",
key: "id",
}}
treeDefaultExpandAll
showSearch
></ControlWrapper.TreeSelect>
</Form.Item>,
<Form.Item key="postId" name="postId"> <Form.Item key="postId" name="postId">
<ControlWrapper.Select <ControlWrapper.Select
label="岗位" label="岗位"
@ -207,11 +296,19 @@ function PersonnelInfoPage(props) {
</Form.Item>, </Form.Item>,
]} ]}
onReset={(value) => { onReset={(value) => {
router.query = { ...value, current: 1, size: 10 }; const deptId =
selectedDeptId && selectedDeptId !== ALL_DEPT_KEY
? asId(selectedDeptId)
: undefined;
router.query = { ...value, deptId, current: 1, size: 10 };
handleSearch(); handleSearch();
}} }}
onFinish={(value) => { onFinish={(value) => {
router.query = { ...value, current: 1, size: 10 }; const deptId =
selectedDeptId && selectedDeptId !== ALL_DEPT_KEY
? asId(selectedDeptId)
: undefined;
router.query = { ...value, deptId, current: 1, size: 10 };
handleSearch(); handleSearch();
}} }}
/> />
@ -320,6 +417,8 @@ function PersonnelInfoPage(props) {
}, },
}} }}
/> />
</div>
</div>
{formModalOpen && ( {formModalOpen && (
<StaffFormModal <StaffFormModal
@ -337,7 +436,7 @@ function PersonnelInfoPage(props) {
setFormModalOpen(false); setFormModalOpen(false);
setCurrentId(""); setCurrentId("");
}} }}
onSuccess={handleSearch} onSuccess={refreshListAndCount}
/> />
)} )}
{viewModalOpen && ( {viewModalOpen && (
@ -364,7 +463,7 @@ function PersonnelInfoPage(props) {
<StaffImportModal <StaffImportModal
open={importModalOpen} open={importModalOpen}
onCancel={() => setImportModalOpen(false)} onCancel={() => setImportModalOpen(false)}
onSuccess={handleSearch} onSuccess={refreshListAndCount}
/> />
)} )}
{resetPasswordModalOpen && ( {resetPasswordModalOpen && (

View File

@ -1,3 +1,28 @@
.personnel-info-layout {
display: flex;
gap: 16px;
min-height: 480px;
}
.personnel-info-tree {
width: 260px;
flex-shrink: 0;
border-right: 1px solid #f0f0f0;
padding-right: 16px;
overflow: auto;
&-title {
font-weight: 500;
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
}
}
.personnel-info-main {
flex: 1;
min-width: 0;
}
.register-engineer-section { .register-engineer-section {
border: 1px dashed #d9d9d9; border: 1px dashed #d9d9d9;
border-radius: 8px; border-radius: 8px;