feat(team): add page
This commit is contained in:
@@ -1,5 +1,262 @@
|
||||
"use client"
|
||||
|
||||
export default function OrganizationPage() {
|
||||
return <></>
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user