import { Avatar, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Table, Tag, TreeSelect, Typography, Upload, message,App } from "antd"; import type { DefaultOptionType } from "antd/es/select"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ApartmentOutlined, DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SearchOutlined, ShopOutlined, UploadOutlined, UserOutlined } from "@ant-design/icons"; import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants, listUserRoles, listUsers, saveUserRoles, updateUser, uploadPlatformAsset } from "@/api"; import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types"; import "./index.less"; const { Title, Text } = Typography; type OrgTreeNode = { value: number; title: string; children: OrgTreeNode[] }; type Membership = { tenantId?: number; orgId?: number; orgName?: string }; function buildOrgTree(list: SysOrg[]): OrgTreeNode[] { const map = new Map(); const roots: OrgTreeNode[] = []; list.forEach((item) => { map.set(item.id, { value: item.id, title: item.orgName, children: [] }); }); map.forEach((node, id) => { const item = list.find((org) => org.id === id); if (item?.parentId && map.has(item.parentId)) { map.get(item.parentId)!.children.push(node); } else { roots.push(node); } }); return roots; } function MembershipOrgSelect({ fieldProps, name, tenantId }: { fieldProps: any; name: number; tenantId?: number }) { const { t } = useTranslation(); const [orgs, setOrgs] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!tenantId) { setOrgs([]); return; } setLoading(true); listOrgs(tenantId) .then((data) => setOrgs(buildOrgTree(data || []))) .finally(() => setLoading(false)); }, [tenantId]); return ( ); } export default function Users() { const { message } = App.useApp(); const { t } = useTranslation(); const { can } = usePermission(); const { items: statusDict } = useDict("sys_common_status"); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); const [data, setData] = useState([]); const [roles, setRoles] = useState([]); const [tenants, setTenants] = useState([]); const [orgs, setOrgs] = useState([]); const [current, setCurrent] = useState(1); const [pageSize, setPageSize] = useState(10); const [searchText, setSearchText] = useState(""); const handleSearch = () => { setCurrent(1); }; const handleResetSearch = () => { setSearchText(""); setFilterTenantId(undefined); setCurrent(1); }; const [filterTenantId, setFilterTenantId] = useState(undefined); const [drawerOpen, setDrawerOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const isPlatformMode = useMemo(() => { const profileStr = sessionStorage.getItem("userProfile"); if (!profileStr) return false; const profile = JSON.parse(profileStr); return !!profile.isPlatformAdmin; }, []); const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []); const selectedTenantId = Form.useWatch("tenantId", form); const memberships = (Form.useWatch("memberships", form) || []) as Membership[]; const tenantMap = useMemo(() => { const map: Record = {}; tenants.forEach((tenant) => { map[tenant.id] = tenant.tenantName; }); return map; }, [tenants]); const roleOptions = useMemo(() => { if (!isPlatformMode) { return roles.map((role) => ({ label: role.roleName, value: role.roleId })); } const selectedTenantIds = new Set(memberships.map((membership) => membership?.tenantId).filter(Boolean)); return roles .filter((role: any) => role.tenantId != null && selectedTenantIds.has(role.tenantId)) .map((role: any) => { const tenantId = role.tenantId ?? 0; const tenantName = tenantMap[tenantId] || `Tenant:${tenantId}`; return { label: (
{role.roleName} [{tenantName}]
), value: role.roleId, searchText: `${role.roleName} ${tenantName}` }; }); }, [isPlatformMode, roles, memberships, tenantMap]); const loadBaseData = async () => { try { const promises: Promise[] = [listRoles()]; if (isPlatformMode) { promises.push(listTenants({ current: 1, size: 1000 })); } const [rolesList, tenantsResponse] = await Promise.all(promises); setRoles(rolesList || []); if (isPlatformMode && tenantsResponse) { setTenants(tenantsResponse.records || []); } } catch { } }; const loadUsersData = async () => { setLoading(true); try { const usersList = await listUsers({ tenantId: filterTenantId }); setData(usersList || []); } finally { setLoading(false); } }; useEffect(() => { loadBaseData(); }, []); useEffect(() => { loadUsersData(); }, [filterTenantId]); useEffect(() => { const fetchOrgs = async () => { const targetId = isPlatformMode ? selectedTenantId : activeTenantId; if (targetId) { const list = await listOrgs(targetId); setOrgs(list || []); } else { setOrgs([]); } }; fetchOrgs(); }, [selectedTenantId, isPlatformMode, activeTenantId]); const orgTreeData = useMemo(() => buildOrgTree(orgs), [orgs]); const filteredData = useMemo(() => { if (!searchText) return data; const lower = searchText.toLowerCase(); return data.filter( (user) => user.username.toLowerCase().includes(lower) || user.displayName.toLowerCase().includes(lower) || (user.email && user.email.toLowerCase().includes(lower)) ); }, [data, searchText]); const openCreate = () => { setEditing(null); form.resetFields(); form.setFieldsValue({ status: 1, roleIds: [], isPlatformAdmin: false, tenantId: isPlatformMode ? undefined : activeTenantId, memberships: isPlatformMode ? [] : [{ tenantId: activeTenantId }] }); setDrawerOpen(true); }; const openEdit = async (record: SysUser) => { setEditing(record); try { const detail = await getUserDetail(record.userId); const roleIds = await listUserRoles(record.userId); form.setFieldsValue({ ...detail, roleIds: roleIds || [], password: "", tenantId: (detail as any).tenantId || detail.memberships?.[0]?.tenantId, orgId: (detail as any).orgId || detail.memberships?.[0]?.orgId, memberships: detail.memberships || [] }); setDrawerOpen(true); } catch { } }; const handleDelete = async (id: number) => { await deleteUser(id); message.success(t("common.success")); loadUsersData(); }; const handleAvatarUpload = async (file: File) => { try { setAvatarUploading(true); const url = await uploadPlatformAsset(file); form.setFieldValue("avatarUrl", url); message.success(t("common.success")); } finally { setAvatarUploading(false); } return false; }; const submit = async () => { const values = await form.validateFields(); setSaving(true); try { const userPayload: Partial = { username: values.username, displayName: values.displayName, email: values.email, phone: values.phone, avatarUrl: values.avatarUrl, status: values.status, isPlatformAdmin: values.isPlatformAdmin }; if (!isPlatformMode) { userPayload.memberships = [{ tenantId: activeTenantId, orgId: values.orgId } as any]; } else { userPayload.memberships = values.memberships || []; } if (values.password) { userPayload.password = values.password; } let userId = editing?.userId; if (editing) { await updateUser(editing.userId, userPayload); } else { await createUser(userPayload); const updatedList = await listUsers(); const newUser = updatedList.find((user) => user.username === userPayload.username); userId = newUser?.userId; } if (userId) { await saveUserRoles(userId, values.roleIds || []); } message.success(t("common.success")); setDrawerOpen(false); loadUsersData(); } finally { setSaving(false); } }; const columns: any[] = [ { title: t("users.userInfo"), key: "user", render: (_: any, record: SysUser) => ( } />
{record.displayName}
{record.isPlatformAdmin && {t("users.platformAdmin")}}
@{record.username}
) }, ...(isPlatformMode ? [{ title: t("users.tenant"), key: "tenant", render: (_: any, record: SysUser) => { if (record.memberships && record.memberships.length > 0) { return (
{record.memberships.slice(0, 2).map((membership: any) => ( {tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`} ))} {record.memberships.length > 2 && +{record.memberships.length - 2} more}
); } return {t("usersExt.noTenant")}; } }] : []), { title: t("users.orgNode"), key: "org", render: (_: any, record: SysUser) => { if (record.memberships && record.memberships.length > 0) { const orgNames = record.memberships.map((membership: any) => membership.orgName).filter(Boolean); if (orgNames.length > 0) { return (
{orgNames.map((name: string, index: number) => ( {name} ))}
); } } return -; } }, { title: t("users.roles"), key: "roles", render: (_: any, record: SysUser) => ( {record.roles && record.roles.length > 0 ? record.roles.map((role) => {role.roleName}) : {t("usersExt.noRoles")}} ) }, { title: t("common.status"), dataIndex: "status", width: 80, render: (status: number) => { const item = statusDict.find((dictItem) => dictItem.itemValue === String(status)); return {item ? item.itemLabel : status === 1 ? t("usersExt.enabled") : t("usersExt.disabled")}; } }, { title: t("common.action"), key: "action", width: 100, fixed: "right" as const, render: (_: any, record: SysUser) => ( {can("sys:user:update") && {can("sys:user:create") && }
{ setCurrent(page); setPageSize(size); }} /> ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> {isPlatformMode && } {isPlatformMode && ( <> {t("usersExt.membershipsTitle")} {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }) => ( 1 &&