Files
labelmain/app/management/team/organization/page.tsx
2026-02-27 10:21:43 +08:00

268 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { getUserGroupTree } from "@/components/label/api/user"
import { Organization } from "@/components/label/api/user/typing"
import useAuth from "@/components/label/hooks/useAuth"
import { useAllUserStore } from "@/components/label/store/auth"
import {
ActionIcon,
Button,
Group,
Paper,
ScrollArea,
Stack,
Text,
TextInput,
} from "@mantine/core"
import { notifications } from "@mantine/notifications"
import {
IconPlus,
IconRefresh,
IconSearch,
IconUserCircle,
} from "@tabler/icons-react"
import { useCallback, useEffect, useMemo, useState } from "react"
import CreateModal from "./components/CreateModal"
type NodeItem = {
key: string
name: string
isUser?: boolean
isManager?: boolean
children: NodeItem[]
}
function toNodeTree(data: Organization.Response[]): NodeItem[] {
return (data ?? []).map((item) => {
const children: NodeItem[] = []
if (Array.isArray(item.children) && item.children.length > 0) {
children.push(...toNodeTree(item.children))
}
if (item.leader_uid && item.leader_name) {
children.push({
key: `${item.group_uuid}/${item.leader_uid}`,
name: item.leader_name,
isUser: true,
isManager: true,
children: [],
})
}
if (Array.isArray(item.members) && item.members.length > 0) {
item.members.forEach((member) => {
if (member.id === item.leader_uid) return
children.push({
key: `${item.group_uuid}/${member.id}`,
name: member.name,
isUser: true,
children: [],
})
})
}
return {
key: item.group_uuid,
name: item.name,
children,
}
})
}
function filterTree(nodes: NodeItem[], keyword: string): NodeItem[] {
if (!keyword.trim()) return nodes
const lower = keyword.trim().toLowerCase()
return nodes
.map((node) => {
const matched = node.name.toLowerCase().includes(lower)
const children = filterTree(node.children, keyword)
if (!matched && children.length === 0) return null
return { ...node, children }
})
.filter((n): n is NodeItem => !!n)
}
type FlatNode = NodeItem & { depth: number }
function flattenTree(nodes: NodeItem[], depth = 0): FlatNode[] {
const list: FlatNode[] = []
nodes.forEach((node) => {
list.push({ ...node, depth })
if (node.children.length > 0) {
list.push(...flattenTree(node.children, depth + 1))
}
})
return list
}
function HighlightText(props: { text: string; keyword: string }) {
const { text, keyword } = props
if (!keyword.trim()) return <>{text}</>
const index = text.toLowerCase().indexOf(keyword.trim().toLowerCase())
if (index < 0) return <>{text}</>
const before = text.slice(0, index)
const match = text.slice(index, index + keyword.length)
const after = text.slice(index + keyword.length)
return (
<>
{before}
<Text component="span" c="blue" fw={700}>
{match}
</Text>
{after}
</>
)
}
export default function TeamOrganizationPage() {
const { isShow } = useAuth()
const setFlag = useAllUserStore((s) => s.setFlag)
const [open, setOpen] = useState(false)
const [groupName, setGroupName] = useState("")
const [data, setData] = useState<NodeItem[]>([])
const [loading, setLoading] = useState(false)
const load = useCallback(async () => {
try {
setLoading(true)
const res = await getUserGroupTree()
setData(toNodeTree(res ?? []))
} catch (e) {
setData([])
notifications.show({
color: "red",
title: "加载失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
queueMicrotask(load)
}, [load])
const rows = useMemo(() => {
const filtered = filterTree(data, groupName)
return flattenTree(filtered)
}, [data, groupName])
return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Group justify="space-between" wrap="wrap">
<Group>
<TextInput
placeholder="请输入用户组名称或用户名称进行搜索"
size="xs"
radius="xs"
leftSection={<IconSearch size={14} />}
value={groupName}
onChange={(e) => setGroupName(e.currentTarget.value)}
style={{ minWidth: 360 }}
/>
<ActionIcon
variant="default"
size="md"
onClick={load}
loading={loading}>
<IconRefresh size={16} />
</ActionIcon>
</Group>
{isShow("group_add") ? (
<Button
size="xs"
radius="xs"
leftSection={<IconPlus size={16} />}
onClick={() => setOpen(true)}>
</Button>
) : null}
</Group>
</Paper>
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
flex: 1,
minHeight: 0,
}}>
<ScrollArea h="100%" type="auto">
<Stack gap={6}>
{rows.map((node) => {
return (
<Paper
key={node.key}
withBorder
radius="sm"
p="xs"
style={{
marginLeft: node.depth * 18,
borderColor:
"light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))",
backgroundColor: node.isUser
? "var(--mantine-color-gray-0)"
: "var(--mantine-color-blue-0)",
}}>
<Group gap={8}>
{node.isUser ? <IconUserCircle size={16} /> : null}
<Text size="sm">
<HighlightText text={node.name} keyword={groupName} />
{!node.isUser && node.children.length > 0
? `${node.children.length}`
: ""}
</Text>
{node.isManager ? (
<Text
size="xs"
c="blue"
style={{
backgroundColor: "var(--mantine-color-blue-0)",
padding: "2px 6px",
borderRadius: 4,
}}>
</Text>
) : null}
</Group>
</Paper>
)
})}
{rows.length === 0 ? (
<Text c="dimmed" size="sm">
</Text>
) : null}
</Stack>
</ScrollArea>
</Paper>
<CreateModal
opened={open}
onCloseAction={(refresh) => {
setOpen(false)
if (refresh) {
setFlag(true)
load()
}
}}
/>
</Stack>
)
}