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

615 lines
17 KiB
JavaScript
Raw Normal View History

2026-07-08 09:37:21 +08:00
import { PlusOutlined } from "@ant-design/icons";
2026-06-23 18:07:30 +08:00
import { Connect } from "@cqsjjb/jjb-dva-runtime";
2026-07-08 09:37:21 +08:00
import {
Button,
Empty,
Form,
Input,
message,
Modal,
Select,
Space,
Tree,
Table,
TreeSelect,
} from "antd";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
2026-07-07 09:07:19 +08:00
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
2026-06-23 18:07:30 +08:00
import { useEffect, useMemo, useRef, useState } from "react";
2026-06-29 16:45:58 +08:00
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
2026-07-08 09:37:21 +08:00
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import {
NS_ORG_DEPARTMENT,
NS_ORG_POSITION,
NS_STAFF_INFO,
} from "~/enumerate/namespace";
2026-07-07 17:13:33 +08:00
import { asId, sameId } from "~/utils/enterpriseInfo/idUtil";
2026-06-23 18:07:30 +08:00
function findDeptNode(nodes = [], id) {
for (const node of nodes) {
2026-06-25 16:30:37 +08:00
if (sameId(node.id, id)) {
2026-06-23 18:07:30 +08:00
return node;
}
if (node.children?.length) {
const found = findDeptNode(node.children, id);
if (found) {
return found;
}
}
}
return null;
}
function toSelectedDept(node) {
if (!node) {
return null;
}
return {
2026-06-25 16:30:37 +08:00
id: asId(node.id),
2026-06-23 18:07:30 +08:00
deptName: node.deptName || node.title,
2026-07-08 09:37:21 +08:00
managerAccount: node.managerAccount,
managerName: node.managerName,
deptLevelName: node.deptLevelName,
2026-06-23 18:07:30 +08:00
};
}
function DepartmentPositionPage(props) {
2026-07-08 14:44:41 +08:00
const { orgPosition } = props;
const { orgPositionLoading } = orgPosition;
2026-06-23 18:07:30 +08:00
const [selectedDept, setSelectedDept] = useState(null);
const [treeData, setTreeData] = useState([]);
const [deptModalOpen, setDeptModalOpen] = useState(false);
const [positionModalOpen, setPositionModalOpen] = useState(false);
const [currentId, setCurrentId] = useState("");
const [filterKeyword, setFilterKeyword] = useState("");
const [staffOptions, setStaffOptions] = useState([]);
const [positionForm] = Form.useForm();
const [modalForm] = Form.useForm();
2026-07-08 09:37:21 +08:00
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [pageIndex, setPageIndex] = useState(1);
const [pageSize, setPageSize] = useState(10);
2026-06-23 18:07:30 +08:00
useEffect(() => {
(async () => {
try {
2026-07-08 09:37:21 +08:00
const res = await props.staffInfoList?.({
pageIndex: 1,
pageSize: 500,
});
setStaffOptions(
(res?.data || []).map((s) => ({
label: `${s.userName ?? s.staffName}${s.account}`,
value: s.account,
staffName: s.userName ?? s.staffName,
})),
);
} catch (err) {
2026-06-23 18:07:30 +08:00
console.warn("[DepartmentPosition] load staff options failed:", err);
}
})();
}, []);
2026-07-08 09:37:21 +08:00
const fetchPositionData = async (page = 1, size = 10) => {
if (!selectedDept?.id) return;
setLoading(true);
try {
const formData = positionForm.getFieldsValue();
const res = await props.orgPositionList({
2026-06-23 18:07:30 +08:00
...formData,
2026-07-08 09:37:21 +08:00
pageIndex: page,
pageSize: size,
deptId: asId(selectedDept.id),
});
setDataSource(res?.data || []);
setTotal(res?.totalCount || 0);
} catch {
setDataSource([]);
setTotal(0);
} finally {
setLoading(false);
}
};
2026-06-23 18:07:30 +08:00
const loadTree = async () => {
try {
const res = await props.orgDepartmentTree();
2026-07-08 09:37:21 +08:00
const tree = res?.data || [];
2026-06-23 18:07:30 +08:00
setTreeData(tree);
2026-07-08 09:37:21 +08:00
if (tree.length) {
2026-06-23 18:07:30 +08:00
setSelectedDept(toSelectedDept(tree[0]));
}
2026-07-08 09:37:21 +08:00
} catch (err) {
2026-06-23 18:07:30 +08:00
console.warn("[DepartmentPosition] loadTree failed:", err);
setTreeData([]);
}
};
useEffect(() => {
loadTree();
}, []);
useEffect(() => {
if (selectedDept?.id) {
2026-07-08 09:37:21 +08:00
fetchPositionData(1, 10);
2026-06-23 18:07:30 +08:00
}
}, [selectedDept?.id]);
const filteredTree = useMemo(() => {
if (!filterKeyword) {
return treeData;
}
const filterNodes = (nodes) => {
return nodes
.map((node) => {
const children = node.children ? filterNodes(node.children) : [];
const match = (node.deptName || node.title)?.includes(filterKeyword);
if (match || children.length) {
return { ...node, children };
}
return null;
})
.filter(Boolean);
};
return filterNodes(treeData);
}, [treeData, filterKeyword]);
const onDeleteDept = (id) => {
Modal.confirm({
title: "提示",
content: "数据将会删除,您是否确认删除?",
okText: "是",
cancelText: "否",
onOk: async () => {
2026-07-08 09:37:21 +08:00
const res = await props.orgDepartmentRemove({ data: id });
2026-06-23 18:07:30 +08:00
if (res?.success !== false) {
message.success("删除成功");
2026-06-25 16:30:37 +08:00
if (sameId(selectedDept?.id, id)) {
2026-06-23 18:07:30 +08:00
setSelectedDept(null);
}
loadTree();
}
},
});
};
const onDeletePosition = (id) => {
Modal.confirm({
title: "提示",
content: "数据将会删除,您是否确认删除?",
okText: "是",
cancelText: "否",
onOk: async () => {
2026-07-08 12:59:15 +08:00
const res = await props.orgPositionRemove({ data: id });
2026-06-23 18:07:30 +08:00
if (res?.success !== false) {
message.success("删除成功");
2026-07-08 09:37:21 +08:00
fetchPositionData(1, 10);
2026-06-23 18:07:30 +08:00
}
},
});
};
const closeDeptModal = () => {
modalForm.resetFields();
setCurrentId("");
setDeptModalOpen(false);
};
const closePositionModal = () => {
modalForm.resetFields();
setCurrentId("");
setPositionModalOpen(false);
};
const openDeptModal = (record) => {
2026-06-25 16:30:37 +08:00
setCurrentId(asId(record?.id) || "");
2026-06-23 18:07:30 +08:00
setDeptModalOpen(true);
};
const openPositionModal = (record) => {
if (!selectedDept?.id) {
message.warning("请先选择部门");
return;
}
2026-06-25 16:30:37 +08:00
setCurrentId(asId(record?.id) || "");
2026-06-23 18:07:30 +08:00
modalForm.resetFields();
if (record) {
modalForm.setFieldsValue({
...record,
2026-06-25 16:30:37 +08:00
deptId: asId(record.deptId || selectedDept.id),
2026-06-23 18:07:30 +08:00
deptName: record.deptName || selectedDept.deptName,
});
2026-07-08 09:37:21 +08:00
} else {
modalForm.setFieldsValue({
deptId: asId(selectedDept.id),
deptName: selectedDept.deptName,
});
2026-06-23 18:07:30 +08:00
}
setPositionModalOpen(true);
};
const submitPosition = async (values) => {
const request = currentId ? props.orgPositionEdit : props.orgPositionAdd;
const payload = { ...values };
if (currentId) {
payload.id = currentId;
}
2026-06-25 16:30:37 +08:00
payload.deptId = asId(values.deptId || selectedDept?.id);
2026-06-23 18:07:30 +08:00
const res = await request(payload);
if (res?.success !== false) {
message.success(currentId ? "编辑成功" : "添加成功");
closePositionModal();
2026-07-08 09:37:21 +08:00
fetchPositionData(pageIndex, pageSize);
} else {
2026-06-23 18:07:30 +08:00
message.error(res?.message || "操作失败");
}
};
return (
2026-06-29 16:45:58 +08:00
<PageLayout title="部门岗位管理">
2026-06-23 18:07:30 +08:00
<div style={{ display: "flex", gap: 16, minHeight: 480 }}>
2026-07-08 09:37:21 +08:00
<div
style={{
width: 260,
borderRight: "1px solid #f0f0f0",
paddingRight: 16,
flexShrink: 0,
}}
>
<Space
direction="vertical"
style={{ width: "100%", marginBottom: 12 }}
>
<Button
type="primary"
icon={<PlusOutlined />}
block
onClick={() => openDeptModal()}
>
2026-06-23 18:07:30 +08:00
新增部门
</Button>
<Input.Search
placeholder="输入关键字进行过滤"
allowClear
onChange={(e) => setFilterKeyword(e.target.value)}
/>
</Space>
<Tree
2026-06-25 16:30:37 +08:00
selectedKeys={selectedDept?.id ? [asId(selectedDept.id)] : []}
2026-06-23 18:07:30 +08:00
treeData={filteredTree}
fieldNames={{ title: "deptName", key: "id", children: "children" }}
onSelect={(_, { node }) => {
setSelectedDept(toSelectedDept(node));
}}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
{selectedDept?.id ? (
<>
2026-07-08 09:37:21 +08:00
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
}}
>
2026-06-23 18:07:30 +08:00
<span>
当前部门
<strong>{selectedDept.deptName}</strong>
</span>
<Space>
2026-07-08 09:37:21 +08:00
<Button onClick={() => openDeptModal(selectedDept)}>
编辑部门
</Button>
<Button danger onClick={() => onDeleteDept(selectedDept.id)}>
删除部门
</Button>
2026-06-23 18:07:30 +08:00
</Space>
</div>
2026-07-08 09:37:21 +08:00
<SearchForm
2026-06-23 18:07:30 +08:00
form={positionForm}
2026-07-08 09:37:21 +08:00
loading={loading}
formLine={[
<Form.Item key="positionName" name="positionName">
<Input allowClear placeholder="请输入岗位名称" />
</Form.Item>,
]}
onFinish={() => fetchPositionData(1, 10)}
2026-07-08 14:44:41 +08:00
onReset={() => fetchPositionData(1, 10)}
2026-07-08 09:37:21 +08:00
style={{ marginBottom: 16 }}
2026-06-23 18:07:30 +08:00
/>
<Table
2026-07-08 09:37:21 +08:00
footer={() => (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => openPositionModal()}
>
2026-06-23 18:07:30 +08:00
新增岗位
</Button>
)}
columns={[
{ title: "名称", dataIndex: "positionName" },
{ title: "职责", dataIndex: "dutyDesc" },
{ title: "备注", dataIndex: "remark" },
{
title: "操作",
width: 160,
render: (_, record) => (
2026-07-07 09:07:19 +08:00
<TableAction>
2026-07-08 09:37:21 +08:00
<Button
type="link"
onClick={() => openPositionModal(record)}
>
编辑
</Button>
<Button
danger
type="link"
onClick={() => onDeletePosition(record.id)}
>
删除
</Button>
2026-07-07 09:07:19 +08:00
</TableAction>
2026-06-23 18:07:30 +08:00
),
},
]}
2026-07-08 09:37:21 +08:00
dataSource={dataSource}
loading={loading}
pagination={{
current: pageIndex,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
}}
scroll={{ y: props.scrollY }}
onChange={(pag) => {
setPageIndex(pag.current);
setPageSize(pag.pageSize);
fetchPositionData(pag.current, pag.pageSize);
}}
2026-06-23 18:07:30 +08:00
/>
</>
) : (
<Empty description="请在左侧选择部门,查看该部门下的岗位" />
)}
</div>
</div>
{deptModalOpen && (
<DeptFormModal
open={deptModalOpen}
currentId={currentId}
2026-07-08 09:37:21 +08:00
selectedDeptId={selectedDept?.id}
2026-06-23 18:07:30 +08:00
staffOptions={staffOptions}
treeData={treeData}
requestAdd={props.orgDepartmentAdd}
requestEdit={props.orgDepartmentEdit}
requestDetails={props.orgDepartmentGet}
onCancel={closeDeptModal}
onSuccess={loadTree}
/>
)}
{positionModalOpen && (
<Modal
open={positionModalOpen}
2026-07-06 18:06:49 +08:00
destroyOnHidden
2026-06-23 18:07:30 +08:00
title={currentId ? "编辑岗位" : "添加岗位"}
width={560}
2026-07-08 12:59:15 +08:00
confirmLoading={orgPositionLoading}
2026-06-23 18:07:30 +08:00
onCancel={closePositionModal}
onOk={modalForm.submit}
>
<Form form={modalForm} layout="vertical" onFinish={submitPosition}>
<Form.Item name="deptId" hidden>
<Input />
</Form.Item>
<Form.Item name="deptName" label="所属部门">
<Input disabled />
</Form.Item>
<Form.Item
name="positionName"
label="岗位名称"
rules={[{ required: true, message: "请输入岗位名称" }]}
>
<Input placeholder="请输入岗位名称" />
</Form.Item>
<Form.Item
name="dutyDesc"
label="岗位职责"
rules={[{ required: true, message: "请输入岗位职责" }]}
>
2026-07-08 09:37:21 +08:00
<Input.TextArea
rows={3}
placeholder="请输入岗位职责"
showCount
maxLength={500}
/>
2026-06-23 18:07:30 +08:00
</Form.Item>
<Form.Item name="remark" label="备注">
2026-07-08 09:37:21 +08:00
<Input.TextArea
rows={2}
placeholder="请输入备注"
showCount
maxLength={500}
/>
2026-06-23 18:07:30 +08:00
</Form.Item>
</Form>
</Modal>
)}
2026-06-29 16:45:58 +08:00
</PageLayout>
2026-06-23 18:07:30 +08:00
);
}
2026-07-08 09:37:21 +08:00
function resolveManagerAccount(dept, options) {
if (dept?.managerAccount) {
return dept.managerAccount;
2026-06-23 18:07:30 +08:00
}
2026-07-08 09:37:21 +08:00
if (dept?.managerName) {
const matched = options.find((item) => item.staffName === dept.managerName);
2026-06-23 18:07:30 +08:00
return matched?.value;
}
return undefined;
}
function DeptFormModal({
open,
currentId,
staffOptions,
treeData,
requestAdd,
requestEdit,
requestDetails,
onCancel,
onSuccess,
2026-07-08 09:37:21 +08:00
selectedDeptId,
2026-06-23 18:07:30 +08:00
}) {
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
const staffOptionsRef = useRef(staffOptions);
const treeDataRef = useRef(treeData);
const requestDetailsRef = useRef(requestDetails);
staffOptionsRef.current = staffOptions;
treeDataRef.current = treeData;
requestDetailsRef.current = requestDetails;
useEffect(() => {
if (!open) {
return;
}
form.resetFields();
if (!currentId) {
return;
}
let cancelled = false;
setDetailLoading(true);
const loadDetail = async () => {
let dept = null;
const getDetails = requestDetailsRef.current;
if (typeof getDetails === "function") {
2026-07-08 09:37:21 +08:00
try {
const res = await getDetails({ id: currentId });
dept = res?.data || null;
} catch {
dept = null;
}
2026-06-23 18:07:30 +08:00
}
if (!dept) {
dept = findDeptNode(treeDataRef.current, currentId);
}
if (!cancelled && dept) {
form.setFieldsValue({
...dept,
2026-07-08 09:37:21 +08:00
managerAccount: resolveManagerAccount(dept, staffOptionsRef.current),
managerName: dept.managerName,
2026-06-23 18:07:30 +08:00
});
}
};
loadDetail().finally(() => {
if (!cancelled) {
setDetailLoading(false);
}
});
return () => {
cancelled = true;
};
}, [open, currentId]);
2026-07-08 09:37:21 +08:00
const onManagerChange = (account) => {
2026-06-23 18:07:30 +08:00
const staff = staffOptions.find((s) => s.value === account);
2026-07-08 09:37:21 +08:00
form.setFieldValue("managerName", staff?.staffName || "");
2026-06-23 18:07:30 +08:00
};
const handleSubmit = async (values) => {
try {
setSubmitting(true);
const request = currentId ? requestEdit : requestAdd;
const payload = { ...values };
if (currentId) {
payload.id = currentId;
}
const res = await request(payload);
if (res?.success !== false) {
message.success(currentId ? "编辑成功" : "添加成功");
form.resetFields();
onCancel();
await onSuccess();
2026-07-08 09:37:21 +08:00
} else {
2026-06-23 18:07:30 +08:00
message.error(res?.message || "操作失败");
}
2026-07-08 09:37:21 +08:00
} finally {
2026-06-23 18:07:30 +08:00
setSubmitting(false);
}
};
2026-07-08 14:44:41 +08:00
2026-06-23 18:07:30 +08:00
return (
<Modal
open={open}
2026-07-06 18:06:49 +08:00
destroyOnHidden
2026-06-23 18:07:30 +08:00
title={currentId ? "编辑部门" : "添加部门"}
width={560}
loading={detailLoading}
confirmLoading={submitting}
onCancel={onCancel}
onOk={form.submit}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
2026-07-08 14:44:41 +08:00
{!currentId && (
<Form.Item
name="parentId"
label="上级部门"
initialValue={selectedDeptId}
>
<TreeSelect
treeData={treeData}
allowClear
fieldNames={{
label: "deptName",
value: "id",
children: "children",
}}
showSearch
placeholder="请选择上级部门"
/>
</Form.Item>
)}
2026-06-23 18:07:30 +08:00
<Form.Item
name="deptName"
label="部门名称"
rules={[{ required: true, message: "请输入部门名称" }]}
>
<Input placeholder="请输入部门名称" />
</Form.Item>
2026-07-08 09:37:21 +08:00
<Form.Item name="managerAccount" label="负责人">
2026-06-23 18:07:30 +08:00
<Select
allowClear
showSearch
placeholder="请选择负责人"
options={staffOptions}
optionFilterProp="label"
2026-07-08 09:37:21 +08:00
onChange={onManagerChange}
2026-06-23 18:07:30 +08:00
/>
</Form.Item>
2026-07-08 09:37:21 +08:00
<Form.Item name="managerName" hidden>
2026-06-23 18:07:30 +08:00
<Input />
</Form.Item>
2026-07-08 09:37:21 +08:00
<Form.Item name="deptLevelName" label="部门级别">
2026-06-23 18:07:30 +08:00
<Input placeholder="请输入部门级别" />
</Form.Item>
</Form>
</Modal>
);
}
2026-07-08 09:37:21 +08:00
export default Connect(
[NS_ORG_DEPARTMENT, NS_ORG_POSITION, NS_STAFF_INFO],
true,
)(AntdTableFuncControl(DepartmentPositionPage));