代码提交

dev-1.3
zhanglei 2026-08-10 10:18:38 +08:00
parent c31b85c4c1
commit de5a1a4dc6
4 changed files with 300 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

@ -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,166 +238,187 @@ function PersonnelInfoPage(props) {
</Space> </Space>
} }
> >
<SearchForm <div className="personnel-info-layout">
style={{ marginBottom: 24 }} <div className="personnel-info-tree">
form={searchForm} <div className="personnel-info-tree-title">组织机构</div>
loading={loading} <Tree
formLine={[ key={treeData.length ? "dept-tree-loaded" : "dept-tree-empty"}
<Form.Item selectedKeys={[asId(selectedDeptId) || ALL_DEPT_KEY]}
key="userName" treeData={displayTree}
name="userName" defaultExpandAll
rules={[{ max: 50, message: "用户名称不能超过50个字符" }]} fieldNames={{ title: "deptName", key: "id", children: "children" }}
> titleRender={(node) => {
<ControlWrapper.Input if (node.id === ALL_DEPT_KEY) {
label="用户名称" return `全部人员 (${totalPersonCount}人)`;
placeholder="请输入用户名称" }
allowClear const count = deptCountMap[asId(node.id)] ?? 0;
maxLength={50} return `${node.deptName || ""} (${count}人)`;
/> }}
</Form.Item>, onSelect={(keys, { node }) => {
<Form.Item key="deptId" name="deptId"> if (!keys?.length) {
<ControlWrapper.TreeSelect return;
placeholder="请选择部门" }
treeData={orgDepartmentTreeData} applyDeptFilter(node.id);
allowClear }}
fieldNames={{ />
label: "deptName", </div>
value: "id", <div className="personnel-info-main">
key: "id", <SearchForm
}} style={{ marginBottom: 24 }}
treeDefaultExpandAll form={searchForm}
showSearch loading={loading}
></ControlWrapper.TreeSelect> formLine={[
</Form.Item>, <Form.Item
<Form.Item key="postId" name="postId"> key="userName"
<ControlWrapper.Select name="userName"
label="岗位" rules={[{ max: 50, message: "用户名称不能超过50个字符" }]}
placeholder="请选择岗位" >
allowClear <ControlWrapper.Input
style={{ width: "100%" }} label="用户名称"
> placeholder="请输入用户名称"
{positionOptions.map((p) => ( allowClear
<Select.Option key={p.value} value={p.value}> maxLength={50}
{p.label} />
</Select.Option> </Form.Item>,
))} <Form.Item key="postId" name="postId">
</ControlWrapper.Select> <ControlWrapper.Select
</Form.Item>, label="岗位"
]} placeholder="请选择岗位"
onReset={(value) => { allowClear
router.query = { ...value, current: 1, size: 10 }; style={{ width: "100%" }}
handleSearch(); >
}} {positionOptions.map((p) => (
onFinish={(value) => { <Select.Option key={p.value} value={p.value}>
router.query = { ...value, current: 1, size: 10 }; {p.label}
handleSearch(); </Select.Option>
}} ))}
/> </ControlWrapper.Select>
<Table </Form.Item>,
rowKey="id" ]}
columns={[ onReset={(value) => {
{ title: "姓名", dataIndex: "userName", ellipsis: true, width: 150 }, const deptId =
{ title: "账号", dataIndex: "account" }, selectedDeptId && selectedDeptId !== ALL_DEPT_KEY
{ title: "部门", dataIndex: "deptName" }, ? asId(selectedDeptId)
{ title: "岗位", dataIndex: "postName" }, : undefined;
router.query = { ...value, deptId, current: 1, size: 10 };
handleSearch();
}}
onFinish={(value) => {
const deptId =
selectedDeptId && selectedDeptId !== ALL_DEPT_KEY
? asId(selectedDeptId)
: undefined;
router.query = { ...value, deptId, current: 1, size: 10 };
handleSearch();
}}
/>
<Table
rowKey="id"
columns={[
{ title: "姓名", dataIndex: "userName", ellipsis: true, width: 150 },
{ title: "账号", dataIndex: "account" },
{ title: "部门", dataIndex: "deptName" },
{ title: "岗位", dataIndex: "postName" },
{ {
title: "专业能力", title: "专业能力",
dataIndex: "capabilityAssessment", dataIndex: "capabilityAssessment",
render: (list) => { render: (list) => {
if (!Array.isArray(list) || !list.length) return "-"; if (!Array.isArray(list) || !list.length) return "-";
return list return list
.map((item) => CAPABILITY_MAP[item.professionalCapabilityCode]) .map((item) => CAPABILITY_MAP[item.professionalCapabilityCode])
.join("、"); .join("、");
}, },
},
{
title: "证照名称",
dataIndex: "personnelCertFilingPageCOList",
width: 280,
render: (_, record) => {
const arr = [];
if(record.registeredSafetyEngineerCert){
arr.push(<Tag color="blue">{TITLE_LEVEL_MAP[record.registeredSafetyEngineerCert.certLevel]}注册安全工程师</Tag>);
}
if(record.evaluatorCert){
arr.push(<Tag color="green">{evalCertLevelMap[record.evaluatorCert.certLevel]}注册安全评价师</Tag>);
}
return <Space size={4}>{arr}</Space>;
}, },
}, {
{ title: "证照名称",
title: "操作", dataIndex: "personnelCertFilingPageCOList",
width: 200, width: 280,
fixed: "right", render: (_, record) => {
render: (_, record) => ( const arr = [];
<TableAction> if(record.registeredSafetyEngineerCert){
<Button arr.push(<Tag color="blue">{TITLE_LEVEL_MAP[record.registeredSafetyEngineerCert.certLevel]}注册安全工程师</Tag>);
type="link" }
size="small" if(record.evaluatorCert){
onClick={() => { arr.push(<Tag color="green">{evalCertLevelMap[record.evaluatorCert.certLevel]}注册安全评价师</Tag>);
setCurrentId(record.id); }
setViewModalOpen(true); return <Space size={4}>{arr}</Space>;
}} },
> },
查看 {
</Button> title: "操作",
<Button width: 200,
type="link" fixed: "right",
size="small" render: (_, record) => (
onClick={() => { <TableAction>
setCurrentId(record.id); <Button
setResumeModalOpen(true); type="link"
}} size="small"
> onClick={() => {
查看简历 setCurrentId(record.id);
</Button> setViewModalOpen(true);
<Button }}
type="link" >
size="small" 查看
onClick={() => { </Button>
setCurrentId(record.id); <Button
setFormModalOpen(true); type="link"
}} size="small"
> onClick={() => {
编辑 setCurrentId(record.id);
</Button> setResumeModalOpen(true);
}}
>
查看简历
</Button>
<Button
type="link"
size="small"
onClick={() => {
setCurrentId(record.id);
setFormModalOpen(true);
}}
>
编辑
</Button>
<Button <Button
type="link" type="link"
size="small" size="small"
onClick={() => onResetPassword(record.id)} onClick={() => onResetPassword(record.id)}
> >
重置密码 重置密码
</Button> </Button>
<Button <Button
danger danger
type="link" type="link"
size="small" size="small"
onClick={() => onDelete(record.id)} onClick={() => onDelete(record.id)}
> >
删除 删除
</Button> </Button>
</TableAction> </TableAction>
), ),
}, },
]} ]}
dataSource={dataSource} dataSource={dataSource}
scroll={{ y: props.scrollY, x: 1300 }} scroll={{ y: props.scrollY, x: 1300 }}
loading={loading} loading={loading}
pagination={{ pagination={{
total, total,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (t) => `${t}`, showTotal: (t) => `${t}`,
current: Number(router.query.current) || 1, current: Number(router.query.current) || 1,
pageSize: Number(router.query.size) || 10, pageSize: Number(router.query.size) || 10,
onChange: (page, pageSize) => { onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, size: pageSize }; router.query = { ...router.query, current: page, size: pageSize };
handleSearch(); handleSearch();
}, },
}} }}
/> />
</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;