feat(team): add page
This commit is contained in:
140
app/management/team/organization/components/CreateModal.tsx
Normal file
140
app/management/team/organization/components/CreateModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
getUserAll,
|
||||
getUserGroupAll,
|
||||
userGroupAdd,
|
||||
} from "@/components/label/api/user"
|
||||
import { useAllUserStore } from "@/components/label/store/auth"
|
||||
import { Button, Group, Modal, Select, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
|
||||
interface ComponentProps {
|
||||
opened: boolean
|
||||
onCloseAction: (refresh?: boolean) => void
|
||||
}
|
||||
|
||||
export default function CreateModal({ opened, onCloseAction }: ComponentProps) {
|
||||
const storeUserOpts = useAllUserStore((s) => s.userOpts)
|
||||
|
||||
const [groupOpts, setGroupOpts] = useState<Array<{ label: string; value: string }>>(
|
||||
[]
|
||||
)
|
||||
const [userOpts, setUserOpts] = useState<Array<{ label: string; value: string }>>(
|
||||
[]
|
||||
)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const finalUserOpts = useMemo(() => {
|
||||
return userOpts.length > 0 ? userOpts : (storeUserOpts as any[])
|
||||
}, [storeUserOpts, userOpts])
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
group_name: "",
|
||||
leader_uid: "",
|
||||
parent_group_uuid: "",
|
||||
},
|
||||
validate: {
|
||||
group_name: (v) => (v.trim() ? null : "请输入用户组名称"),
|
||||
leader_uid: (v) => (v ? null : "请选择用户组负责人"),
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) return
|
||||
const run = async () => {
|
||||
const [groupRes, userRes] = await Promise.all([
|
||||
getUserGroupAll(),
|
||||
getUserAll(),
|
||||
])
|
||||
setGroupOpts(groupRes ?? [])
|
||||
setUserOpts((userRes ?? []).map((u) => ({ label: u.label, value: String(u.value) })))
|
||||
}
|
||||
queueMicrotask(run)
|
||||
}, [opened])
|
||||
|
||||
const submit = form.onSubmit(async (values) => {
|
||||
try {
|
||||
setSubmitting(true)
|
||||
await userGroupAdd({
|
||||
group_name: values.group_name.trim(),
|
||||
leader_uid: Number(values.leader_uid),
|
||||
parent_group_uuid: values.parent_group_uuid,
|
||||
})
|
||||
notifications.show({
|
||||
color: "green",
|
||||
title: "操作成功",
|
||||
message: "已新增用户组",
|
||||
})
|
||||
form.reset()
|
||||
onCloseAction(true)
|
||||
} catch (e) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: "操作失败",
|
||||
message: e instanceof Error ? e.message : "请求失败",
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => onCloseAction()}
|
||||
title="新增用户组"
|
||||
centered
|
||||
size={560}
|
||||
closeOnClickOutside={false}>
|
||||
<form onSubmit={submit}>
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="用户组名称"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
{...form.getInputProps("group_name")}
|
||||
/>
|
||||
<Select
|
||||
label="用户组负责人"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
searchable
|
||||
data={finalUserOpts}
|
||||
{...form.getInputProps("leader_uid")}
|
||||
/>
|
||||
<Select
|
||||
label="上级用户组"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
searchable
|
||||
clearable
|
||||
data={groupOpts}
|
||||
{...form.getInputProps("parent_group_uuid")}
|
||||
/>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
onClick={() => onCloseAction()}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -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