imeeting/frontend/src/pages/Orgs.tsx

340 lines
9.9 KiB
TypeScript
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 {
Button,
Card,
Drawer,
Form,
Input,
message,
Popconfirm,
Space,
Table,
Tag,
Typography,
InputNumber,
Row,
Col,
Select,
Empty
} from "antd";
import { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { createOrg, deleteOrg, listOrgs, updateOrg, listTenants } from "../api";
import { usePermission } from "../hooks/usePermission";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ApartmentOutlined,
ReloadOutlined,
ShopOutlined
} from "@ant-design/icons";
import type { SysOrg, SysTenant, OrgNode } from "../types";
const { Title, Text } = Typography;
function buildOrgTree(list: SysOrg[]): OrgNode[] {
const map = new Map<number, OrgNode>();
const roots: OrgNode[] = [];
list.forEach((item) => {
map.set(item.id, { ...item, children: [] });
});
map.forEach((node) => {
if (node.parentId && map.has(node.parentId)) {
map.get(node.parentId)!.children.push(node);
} else {
roots.push(node);
}
});
const sortTree = (nodes: OrgNode[]) => {
nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
nodes.forEach(n => n.children && sortTree(n.children));
};
sortTree(roots);
return roots;
}
export default function Orgs() {
const { t } = useTranslation();
const { can } = usePermission();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysOrg[]>([]);
const [tenants, setTenants] = useState<SysTenant[]>([]);
const [selectedTenantId, setSelectedTenantId] = useState<number | undefined>(undefined);
// Platform admin check
const isPlatformMode = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile");
if (profileStr) {
const profile = JSON.parse(profileStr);
return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0";
}
return false;
}, []);
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
const [drawerOpen, setDrawerOpen] = useState(false);
const [editing, setEditing] = useState<SysOrg | null>(null);
const [form] = Form.useForm();
const loadTenants = async () => {
try {
const resp = await listTenants({ current: 1, size: 100 });
const list = resp.records || [];
setTenants(list);
if (!isPlatformMode) {
setSelectedTenantId(activeTenantId);
} else if (list.length > 0 && selectedTenantId === undefined) {
setSelectedTenantId(list[0].id);
}
} catch (e) {
message.error(t('common.error'));
}
};
const loadOrgs = async () => {
if (selectedTenantId === undefined) return;
setLoading(true);
try {
const list = await listOrgs(selectedTenantId);
setData(list || []);
} catch (e) {
message.error(t('common.error'));
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTenants();
}, []);
useEffect(() => {
loadOrgs();
}, [selectedTenantId]);
const treeData = useMemo(() => buildOrgTree(data), [data]);
const parentOptions = useMemo(() => {
return data.map(o => ({ label: o.orgName, value: o.id }));
}, [data]);
const openCreate = (parentId?: number) => {
setEditing(null);
form.resetFields();
form.setFieldsValue({
tenantId: selectedTenantId,
parentId: parentId,
status: 1,
sortOrder: 0
});
setDrawerOpen(true);
};
const openEdit = (record: SysOrg) => {
setEditing(record);
form.setFieldsValue(record);
setDrawerOpen(true);
};
const handleDelete = async (id: number) => {
try {
await deleteOrg(id);
message.success(t('common.success'));
loadOrgs();
} catch (e: any) {
message.error(e.message || t('common.error'));
}
};
const submit = async () => {
try {
const values = await form.validateFields();
setSaving(true);
if (editing) {
await updateOrg(editing.id, values);
message.success(t('common.success'));
} else {
await createOrg(values);
message.success(t('common.success'));
}
setDrawerOpen(false);
loadOrgs();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
} finally {
setSaving(false);
}
};
const columns = [
{
title: t('orgs.orgName'),
dataIndex: "orgName",
key: "orgName",
render: (text: string) => <Text strong>{text}</Text>
},
{
title: t('orgs.orgCode'),
dataIndex: "orgCode",
key: "orgCode",
width: 150,
render: (text: string) => <Tag className="tabular-nums">{text || "-"}</Tag>
},
{
title: t('orgs.sort'),
dataIndex: "sortOrder",
width: 100,
className: "tabular-nums"
},
{
title: t('common.status'),
dataIndex: "status",
width: 100,
render: (s: number) => <Tag color={s === 1 ? "green" : "red"}>{s === 1 ? "启用" : "禁用"}</Tag>
},
{
title: t('common.action'),
key: "action",
width: 180,
render: (_: any, record: SysOrg) => (
<Space>
{can("sys:org:create") && (
<Button type="link" size="small" onClick={() => openCreate(record.id)}>{t('orgs.addSub')}</Button>
)}
{can("sys:org:update") && (
<Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t('common.edit')} />
)}
{can("sys:org:delete") && (
<Popconfirm title={`确定删除 "${record.orgName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
<Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
</Popconfirm>
)}
</Space>
)
}
];
return (
<div className="p-6">
<div className="mb-6 flex justify-between items-end">
<div>
<Title level={4} className="mb-1">{t('orgs.title')}</Title>
<Text type="secondary">{t('orgs.subtitle')}</Text>
</div>
{can("sys:org:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
{t('orgs.createRoot')}
</Button>
)}
</div>
{isPlatformMode && (
<Card className="shadow-sm mb-4">
<Space>
<Text strong>{t('users.tenant')}</Text>
<Select
style={{ width: 220 }}
placeholder={t('orgs.selectTenant')}
value={selectedTenantId}
onChange={setSelectedTenantId}
options={tenants.map(t => ({ label: t.tenantName, value: t.id }))}
suffixIcon={<ShopOutlined aria-hidden="true" />}
/>
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>{t('common.refresh')}</Button>
</Space>
</Card>
)}
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
{selectedTenantId !== undefined ? (
<Table
rowKey="id"
columns={columns}
dataSource={treeData}
loading={loading}
pagination={false}
size="middle"
expandable={{ defaultExpandAllRows: true }}
/>
) : (
<div className="py-20 flex justify-center">
<Empty description={t('orgs.selectTenant')} />
</div>
)}
</Card>
<Drawer
title={
<Space>
<ApartmentOutlined aria-hidden="true" />
<span>{editing ? t('orgs.drawerTitleEdit') : t('orgs.drawerTitleCreate')}</span>
</Space>
}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={420}
destroyOnClose
footer={
<div className="flex justify-end gap-2 p-2">
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
<Button type="primary" loading={saving} onClick={submit}>{t('common.confirm')}</Button>
</div>
}
>
<Form form={form} layout="vertical">
<Form.Item
label={t('users.tenant')}
name="tenantId"
rules={[{ required: true }]}
hidden={!isPlatformMode}
>
<Select disabled options={tenants.map(t => ({ label: t.tenantName, value: t.id }))} />
</Form.Item>
{!isPlatformMode && (
<Form.Item label={t('users.tenant')}>
<Input value={tenants.find(t => t.id === activeTenantId)?.tenantName || "当前租户"} disabled />
</Form.Item>
)}
<Form.Item label={t('orgs.parentOrg')} name="parentId">
<Select
placeholder={t('orgs.rootOrg')}
allowClear
showSearch
optionFilterProp="label"
options={parentOptions}
/>
</Form.Item>
<Form.Item label={t('orgs.orgName')} name="orgName" rules={[{ required: true, message: t('orgs.orgName') }]}>
<Input placeholder="例如:技术部、财务处…" />
</Form.Item>
<Form.Item label={t('orgs.orgCode')} name="orgCode">
<Input placeholder="例如DEPT_TECH" className="tabular-nums" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t('dicts.sort')} name="sortOrder" initialValue={0}>
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} />
</Form.Item>
</Col>
</Row>
</Form>
</Drawer>
</div>
);
}