268 lines
7.3 KiB
TypeScript
268 lines
7.3 KiB
TypeScript
"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>
|
||
)
|
||
}
|