486 lines
15 KiB
JavaScript
486 lines
15 KiB
JavaScript
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||
import { Button, Empty, Form, Input, message, Modal, Row, Col, Select, Space, Tree } from "antd";
|
||
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||
import { useEffect, useMemo, useRef, useState } from "react";
|
||
import AddIcon from "zy-react-library/components/Icon/AddIcon";
|
||
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
||
import Search from "zy-react-library/components/Search";
|
||
import Table from "zy-react-library/components/Table";
|
||
import useTable from "zy-react-library/hooks/useTable";
|
||
import { NS_ORG_DEPARTMENT, NS_ORG_POSITION, NS_STAFF_INFO } from "~/enumerate/namespace";
|
||
import { asId, sameId } from "~/api/enterpriseInfo/idUtil";
|
||
import { safeListRequest, safeRequest } from "~/utils";
|
||
|
||
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,
|
||
leaderAccount: node.leaderAccount,
|
||
leaderName: node.leaderName,
|
||
deptLevel: node.deptLevel,
|
||
};
|
||
}
|
||
|
||
function DepartmentPositionPage(props) {
|
||
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();
|
||
|
||
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 { tableProps: positionTableProps, getData: getPositionData } = useTable(
|
||
safeListRequest(props.orgPositionList),
|
||
{
|
||
form: positionForm,
|
||
transform: (formData) => ({
|
||
...formData,
|
||
eqDeptId: asId(selectedDept?.id),
|
||
}),
|
||
},
|
||
);
|
||
|
||
const loadTree = async () => {
|
||
try {
|
||
const res = await props.orgDepartmentTree();
|
||
const normalizeTree = (nodes = []) => nodes.map((node) => ({
|
||
...node,
|
||
title: node.title || node.deptName,
|
||
deptName: node.deptName || node.title,
|
||
children: node.children ? normalizeTree(node.children) : [],
|
||
}));
|
||
const tree = normalizeTree(res?.data || []);
|
||
setTreeData(tree);
|
||
if (!selectedDept && tree.length) {
|
||
setSelectedDept(toSelectedDept(tree[0]));
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.warn("[DepartmentPosition] loadTree failed:", err);
|
||
setTreeData([]);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadTree();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (selectedDept?.id) {
|
||
getPositionData();
|
||
}
|
||
}, [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({ 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({ id });
|
||
if (res?.success !== false) {
|
||
message.success("删除成功");
|
||
getPositionData();
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
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();
|
||
getPositionData();
|
||
}
|
||
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={<AddIcon />} 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>
|
||
<Search
|
||
form={positionForm}
|
||
options={[{ name: "likePositionName", label: "岗位名称", placeholder: "请输入岗位名称" }]}
|
||
onFinish={getPositionData}
|
||
/>
|
||
<Table
|
||
toolBarRender={() => (
|
||
<Button type="primary" icon={<AddIcon />} 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>
|
||
),
|
||
},
|
||
]}
|
||
{...positionTableProps}
|
||
/>
|
||
</>
|
||
) : (
|
||
<Empty description="请在左侧选择部门,查看该部门下的岗位" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{deptModalOpen && (
|
||
<DeptFormModal
|
||
open={deptModalOpen}
|
||
currentId={currentId}
|
||
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}
|
||
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 resolveLeaderAccount(dept, options) {
|
||
if (dept?.leaderAccount) {
|
||
return dept.leaderAccount;
|
||
}
|
||
if (dept?.leaderName) {
|
||
const matched = options.find((item) => item.staffName === dept.leaderName);
|
||
return matched?.value;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
function DeptFormModal({
|
||
open,
|
||
currentId,
|
||
staffOptions,
|
||
treeData,
|
||
requestAdd,
|
||
requestEdit,
|
||
requestDetails,
|
||
onCancel,
|
||
onSuccess,
|
||
}) {
|
||
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") {
|
||
const res = await safeRequest(getDetails, { id: currentId });
|
||
dept = res?.data || null;
|
||
}
|
||
if (!dept) {
|
||
dept = findDeptNode(treeDataRef.current, currentId);
|
||
}
|
||
if (!cancelled && dept) {
|
||
form.setFieldsValue({
|
||
...dept,
|
||
leaderAccount: resolveLeaderAccount(dept, staffOptionsRef.current),
|
||
leaderName: dept.leaderName,
|
||
});
|
||
}
|
||
};
|
||
loadDetail().finally(() => {
|
||
if (!cancelled) {
|
||
setDetailLoading(false);
|
||
}
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [open, currentId]);
|
||
|
||
const onLeaderChange = (account) => {
|
||
const staff = staffOptions.find((s) => s.value === account);
|
||
form.setFieldValue("leaderName", 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}>
|
||
<Form.Item
|
||
name="deptName"
|
||
label="部门名称"
|
||
rules={[{ required: true, message: "请输入部门名称" }]}
|
||
>
|
||
<Input placeholder="请输入部门名称" />
|
||
</Form.Item>
|
||
<Form.Item name="leaderAccount" label="负责人">
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
placeholder="请选择负责人"
|
||
options={staffOptions}
|
||
optionFilterProp="label"
|
||
onChange={onLeaderChange}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="leaderName" hidden>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="deptLevel" label="部门级别">
|
||
<Input placeholder="请输入部门级别" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
export default Connect([NS_ORG_DEPARTMENT, NS_ORG_POSITION, NS_STAFF_INFO], true)(DepartmentPositionPage);
|