Files
2026-07-13 15:23:29 +08:00

453 lines
14 KiB
TypeScript

import {
Alert,
Button,
Card,
type CardProps,
Col,
message,
Popconfirm,
Row,
Space, Table, type TableProps, Tag,
Tree,
type TreeDataNode, type TreeProps, Typography
} from "antd";
import {BankOutlined, DeleteOutlined, PlusOutlined, TeamOutlined, UserOutlined} from "@ant-design/icons";
import {useEffect, useRef, useState} from "react";
import {queryDept, createDept, updateDept, deleteDept, deptUsers} from "@/api/system/sys_dept.ts";
import type {ISysDept} from "@/domain/iSysDept.ts";
import {isArray, omit} from 'lodash';
import type {FormColumn} from "@/components/XinFormField/FieldRender/typings.ts";
import XinForm, {type XinFormRef} from "@/components/XinForm";
import * as React from "react";
import {useTranslation} from "react-i18next";
import AuthButton from "@/components/AuthButton";
import useAuth from "@/hooks/useAuth.ts";
import type ISysUser from "@/domain/iSysUser.ts";
const deptMap = new Map<string, ISysDept>();
interface TableParams {
page: number;
pageSize: number;
total: number;
}
const Dept = () => {
const {t} = useTranslation();
const {auth} = useAuth();
/** 部门信息表单 */
const formRef = useRef<XinFormRef<ISysDept>>(null);
/** 新增表单 */
const modalFormRef = useRef<XinFormRef<ISysDept>>(null);
/** 当前选中的部门 key */
const [selectKey, setSelectKey] = useState<string>('');
/** 当前(多选框)选择的部门 keys */
const [checkedKeys , setCheckedKeys] = useState<React.Key[]>([]);
/** 标签页 KEY */
const [tabKey, setTabKey] = useState<string>('info');
/** 标签页 Tab */
const tabList: CardProps['tabList'] = [
{
key: 'info',
label: t("system.dept.tab.info")
},
{
key: 'users',
label: t("system.dept.tab.users"),
disabled: !auth("system.dept.users")
},
];
/** 部门数据 */
const [deptData, setDeptData] = useState<TreeDataNode[]>([]);
/** 部门用户列表数据 */
const [users, setUsers] = useState<ISysUser[]>([]);
/** 部门用户列表表格参数 */
const [tableParams, setTableParams] = useState<TableParams>({ page: 1, pageSize: 10, total: 0 });
/** 加载状态 */
const [loading, setLoading] = useState<boolean>(false);
const [tableLoading, setTableLoading] = useState<boolean>(false);
/** 获取部门用户列表 */
const selectUsers = async (id: number, params?: TableParams) => {
try {
setTableLoading(true);
const data = await deptUsers(id, omit(params || tableParams, 'total'));
setUsers(data.data.data!.data);
setTableParams({
...tableParams,
total: data.data.data!.total
})
} finally {
setTableLoading(false);
}
}
/** 刷新部门数据 */
const refreshDept = async () => {
setLoading(true);
/** 转换部门类型 */
const convertDeptToTreeData = (depts: ISysDept[]): TreeDataNode[] => {
if (!depts?.length) return [];
return depts.map(dept => {
deptMap.set(dept.id!.toString(), omit(dept, 'children'));
return {
title: dept.name || t("system.dept.tab.users"),
key: dept.id?.toString() || '',
icon: dept.type === 0 ? <BankOutlined /> : dept.type === 1 ? <TeamOutlined /> : <UserOutlined />,
children: convertDeptToTreeData(dept.children || [])
}
});
};
try {
const res = await queryDept();
if(res.data.data && res.data.data.length > 0) {
deptMap.clear();
setDeptData(convertDeptToTreeData(res.data.data));
if(!selectKey || !deptMap.has(selectKey)) {
setSelectKey(res.data.data[0].id!.toString());
formRef.current?.setFieldsValue(omit(res.data.data[0], 'children'));
if(auth("system.dept.users")) {
await selectUsers(res.data.data[0].id!);
}
}
}
} finally {
setLoading(false);
}
};
/** 新增部门点击事件 */
const addChange = (children: boolean = false) => {
modalFormRef.current?.resetFields();
modalFormRef.current?.setFieldsValue({
parent_id: children ? Number(selectKey) : 0,
sort: 0,
status: 0,
type: 0
})
modalFormRef.current?.open();
}
/** 部门选择事件 */
const onSelect: TreeProps['onSelect'] = (key) => {
if(key && key.length >= 1) {
setSelectKey(key[0].toString());
const deptInfo = deptMap.get(key[0].toString());
if (deptInfo) {
formRef.current?.setFieldsValue(deptInfo);
}
selectUsers(Number(key[0])).then();
}
}
/** 部门多选框选中事件 */
const onCheck: TreeProps['onCheck'] = (checkedKeys) => {
if(isArray(checkedKeys)) {
setCheckedKeys(checkedKeys);
}else {
setCheckedKeys(checkedKeys.checked);
}
}
/** 表单提交 */
const onSubmit = async (data: ISysDept, update: boolean = false) => {
try {
setLoading(true);
if(!update) {
await createDept(data);
message.success(t("system.dept.createSuccess"));
modalFormRef.current?.close();
}else {
await updateDept(Number(selectKey), data);
message.success(t("system.dept.updateSuccess"));
}
await refreshDept();
}finally {
setLoading(false);
}
}
/** 批量删除 */
const onDeleteConfirm = async () => {
try {
setLoading(true);
await deleteDept(checkedKeys);
await refreshDept();
setCheckedKeys([]);
message.success(t("system.dept.deleteSuccess"));
}finally {
setLoading(false);
}
}
/** 表单列数据 */
const columns: FormColumn<ISysDept>[] = [
{
title: t("system.dept.column.name"),
valueType: 'text',
dataIndex: 'name',
rules: [{required: true, message: t("system.dept.column.name.required")}],
},
{
title: t("system.dept.column.code"),
valueType: 'text',
dataIndex: 'code',
rules: [{required: true, message: t("system.dept.column.code.required")}],
},
{
title: t("system.dept.column.type"),
valueType: 'radioButton',
dataIndex: 'type',
fieldProps: {
options: [
{ value: 0, label: t("system.dept.column.type.0") },
{ value: 1, label: t("system.dept.column.type.1") },
{ value: 2, label: t("system.dept.column.type.2") },
],
},
rules: [{required: true, message: t("system.dept.column.type.required")}],
},
{
title: t("system.dept.column.parent"),
valueType: 'treeSelect',
dataIndex: 'parent_id',
fieldProps: {
treeData: [
{
title: t("system.dept.column.parent.0"),
value: 0,
children: deptData
}
],
fieldNames: { label: 'title', value: 'key' },
disabled: true
},
rules: [{required: true, message: t("system.dept.column.parent.required")}],
},
{
title: t("system.dept.column.email"),
valueType: 'text',
dataIndex: 'email',
},
{
title: t("system.dept.column.address"),
valueType: 'text',
dataIndex: 'address',
},
{
title: t("system.dept.column.phone"),
valueType: 'text',
dataIndex: 'phone',
},
{
title: t("system.dept.column.sort"),
valueType: 'digit',
dataIndex: 'sort',
rules: [{required: true, message: t("system.dept.column.sort.required")}],
},
{
title: t("system.dept.column.status"),
valueType: 'radioButton',
dataIndex: 'status',
fieldProps: {
options: [
{ value: 0, label: t("system.dept.column.status.0") },
{ value: 1, label: t("system.dept.column.status.1") },
]
},
rules: [{required: true, message: t("system.dept.column.status.required")}],
},
{
title: t("system.dept.column.remark"),
valueType: 'textarea',
dataIndex: 'remark',
},
];
/** 部门用户列表表格列 */
const usersColumns: TableProps<ISysUser>['columns'] = [
{
title: t("system.dept.users.column.id"),
dataIndex: 'id',
key: 'id',
align: 'center',
},
{
title: t("system.dept.users.column.username"),
dataIndex: 'username',
key: 'username',
align: 'center',
},
{
title: t("system.dept.users.column.nickname"),
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
},
{
title: t("system.dept.users.column.nickname"),
dataIndex: 'email',
key: 'email',
align: 'center',
},
{
title: t("system.dept.users.column.mobile"),
dataIndex: 'mobile',
key: 'mobile',
align: 'center',
},
{
title: t("system.dept.users.column.status"),
dataIndex: 'status',
key: 'status',
align: 'center',
render: (value) => (
<>
{value === 1 && <Tag color={'success'}>{t("system.dept.users.column.status.0")}</Tag>}
{value === 0 && <Tag color={'error'}>{t("system.dept.users.column.status.1")}</Tag>}
</>
)
},
];
/** 初始化 */
useEffect(() => { refreshDept() }, []);
return (
<>
<div className={'mb-5'}>
<Typography.Title level={3}>{t("system.dept.page.title")}</Typography.Title>
<Typography.Text type="secondary">{t("system.dept.page.description")}</Typography.Text>
</div>
<Row gutter={[20, 20]}>
<Col xxl={12} lg={12} xs={24}>
<XinForm<ISysDept>
layoutType={'ModalForm'}
formRef={modalFormRef}
modalProps={{
title: t("system.dept.createModalTitle"),
styles: { body: { paddingTop: 20} },
width: 800
}}
onFinish={async (data: ISysDept) => {
await onSubmit(data, false);
return true;
}}
columns={columns}
layout={'vertical'}
grid
rowProps={{gutter: [30, 0]}}
colProps={{ span: 12 }}
/>
<Card
title={(
<Space>
<AuthButton auth={"system.dept.create"}>
<Button
loading={loading}
children={t("system.dept.createButton")}
icon={<PlusOutlined />}
type={'primary'}
onClick={() => addChange()}
/>
</AuthButton>
<AuthButton auth={"system.dept.create"}>
<Button
loading={loading}
children={t("system.dept.createChildrenButton")}
icon={<PlusOutlined />}
type={'primary'}
onClick={() => addChange(true)}
/>
</AuthButton>
</Space>
)}
variant={'borderless'}
loading={loading}
styles={{ body: { minHeight: '70vh' } }}
>
{checkedKeys.length > 0 && (
<Alert
style={{ marginBottom: 20 }}
description={t("system.dept.checkedMessage", {checked: checkedKeys.length})}
type="info"
action={
<Space>
<Button size="small" type="primary" onClick={()=> setCheckedKeys([])}>
{t("system.dept.unselect")}
</Button>
<AuthButton auth={"system.dept.delete"}>
<Popconfirm
okText={t("system.dept.delete.ok")}
cancelText={t("system.dept.delete.cancel")}
title={t("system.dept.delete.title")}
description={t("system.dept.delete.description")}
onConfirm={() => onDeleteConfirm()}
>
<Button type="primary" icon={<DeleteOutlined />} size={'small'} danger loading={loading}/>
</Popconfirm>
</AuthButton>
</Space>
}
/>
)}
<Tree
checkable
treeData={deptData}
showIcon={true}
checkStrictly={true}
selectedKeys={[selectKey]}
defaultExpandedKeys={[selectKey]}
onSelect={onSelect}
checkedKeys={checkedKeys}
onCheck={onCheck}
/>
</Card>
</Col>
<Col xxl={12} lg={12} xs={24}>
<Card
variant={'borderless'}
tabList={tabList}
tabProps={{ accessKey: tabKey }}
onTabChange={setTabKey}
styles={{ body: { minHeight: '70vh' } }}
>
<XinForm<ISysDept>
formRef={formRef}
onFinish={async (data: ISysDept) => {
await onSubmit(data, true);
return true;
}}
columns={columns}
layout={'horizontal'}
style={{ display: tabKey === 'info' ? "block" : "none" }}
submitter={{
render: (dom) => (
<AuthButton auth={"system.dept.update"}>
{dom['submit']}
</AuthButton>
),
submitText: t("system.dept.saveInfo")
}}
/>
<AuthButton auth={"system.dept.users"}>
<Table<ISysUser>
style={{ display: tabKey === 'users' ? "block" : "none" }}
dataSource={users}
bordered={true}
columns={usersColumns}
loading={tableLoading}
size={'small'}
pagination={{
current: tableParams.page,
pageSize: tableParams.pageSize,
total: tableParams.total,
showSizeChanger: true,
onChange: (page, pageSize) => {
const params = { total: tableParams.total, pageSize, page }
setTableParams(params);
selectUsers(Number(selectKey), params).then();
}
}}
scroll={{x: 600}}
/>
</AuthButton>
</Card>
</Col>
</Row>
</>
);
};
export default Dept;