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

615 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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