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

521 lines
16 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 {
getProjectList,
getProjectTypeList,
} from "@/components/label/api/project"
import { getTaskList } from "@/components/label/api/task"
import { Task } from "@/components/label/api/task/typing"
import TaskStatusTag from "@/components/label/components/TaskStatusTag"
import { useAllUserStore } from "@/components/label/store/auth"
import { TaskStatusEnum } from "@/components/label/utils/constants"
import {
Button,
Collapse,
Group,
MultiSelect,
NumberInput,
Paper,
SimpleGrid,
Stack,
Text,
} from "@mantine/core"
import { notifications } from "@mantine/notifications"
import {
IconChevronDown,
IconChevronUp,
IconDownload,
IconRefresh,
IconSearch,
} from "@tabler/icons-react"
import { DataTable, DataTableColumn } from "mantine-datatable"
import { useEffect, useMemo, useState } from "react"
import * as XLSX from "xlsx-js-style"
import { flattenLeaderTree } from "../utils"
const emptyFilters = {
page_size: 1000,
project_type: [] as string[],
project_id: [] as string[],
label_status: [] as string[],
current_uid: [] as string[],
label_user: [] as string[],
review_user1: [] as string[],
review_user2: [] as string[],
}
function toNumberArray(values?: string[]) {
if (!values || values.length === 0) return undefined
const arr = values.map((v) => Number(v)).filter((v) => Number.isFinite(v))
return arr.length > 0 ? arr : undefined
}
export default function TaskListTable() {
const treeData = useAllUserStore((s) => s.treeData)
const userOptions = useMemo(
() => flattenLeaderTree(treeData ?? []),
[treeData]
)
const [collapsed, setCollapsed] = useState(true)
const [loading, setLoading] = useState(false)
const [projectTypeOpts, setProjectTypeOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [projectNameOpts, setProjectNameOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [filters, setFilters] = useState(emptyFilters)
const [records, setRecords] = useState<Task.DataProps[]>([])
const [total, setTotal] = useState<number>(0)
const [totalSize, setTotalSize] = useState<Task.SizeProps | null>(null)
useEffect(() => {
const run = async () => {
const [typesRes, projectsRes] = await Promise.allSettled([
getProjectTypeList(),
getProjectList({ page_number: 1, page_size: 10000 }),
])
if (typesRes.status === "fulfilled") {
setProjectTypeOpts(
(typesRes.value ?? []).map((t) => ({ label: t, value: t }))
)
} else {
setProjectTypeOpts([])
}
if (projectsRes.status === "fulfilled") {
const list = projectsRes.value?.project_list ?? []
setProjectNameOpts(
list.map((p) => ({ label: p.name, value: String(p.id) }))
)
} else {
setProjectNameOpts([])
}
}
queueMicrotask(run)
}, [])
const handleSearch = async (nextFilters = filters) => {
try {
setLoading(true)
if (!nextFilters.page_size) {
notifications.show({
color: "yellow",
title: "参数不完整",
message: "请填写展示数量",
})
return
}
const payload: any = {
page_number: 1,
page_size: nextFilters.page_size,
get_data: false,
}
if (nextFilters.project_type.length > 0)
payload.project_type = nextFilters.project_type
if (nextFilters.project_id.length > 0) {
payload.project_id = toNumberArray(nextFilters.project_id)
}
if (nextFilters.label_status.length > 0) {
payload.label_status = toNumberArray(nextFilters.label_status)
}
if (nextFilters.current_uid.length > 0) {
payload.current_uid = toNumberArray(nextFilters.current_uid)
}
if (nextFilters.label_user.length > 0) {
payload.label_user = toNumberArray(nextFilters.label_user)
}
if (nextFilters.review_user1.length > 0) {
payload.review_user1 = toNumberArray(nextFilters.review_user1)
}
if (nextFilters.review_user2.length > 0) {
payload.review_user2 = toNumberArray(nextFilters.review_user2)
}
const res = await getTaskList(payload)
setRecords(res?.task_list ?? [])
setTotal(res?.total_items ?? 0)
setTotalSize(res?.total_size ?? null)
} catch (e) {
setRecords([])
setTotal(0)
setTotalSize(null)
notifications.show({
color: "red",
title: "加载失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setLoading(false)
}
}
useEffect(() => {
queueMicrotask(() => handleSearch(emptyFilters))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const columns = useMemo(() => {
const cols: DataTableColumn<Task.DataProps>[] = [
{ accessor: "project_name", title: "项目名称", width: 220 },
{ accessor: "id", title: "任务ID", width: 100 },
{
accessor: "label_status",
title: "状态",
width: 160,
render: (record) => (
<TaskStatusTag
status={record.label_status}
rejected={record.rejected}
/>
),
},
{ accessor: "current_username", title: "当前负责人", width: 120 },
{ accessor: "label_username", title: "标注员", width: 120 },
{ accessor: "review1_username", title: "审核员", width: 120 },
{ accessor: "review2_username", title: "复审员", width: 120 },
{
accessor: "object_size",
title: "对象总数",
width: 120,
render: (record) => record.label_object_size?.object_size ?? 0,
},
{
accessor: "new_size",
title: "新增对象数",
width: 120,
render: (record) => record.label_object_size?.new_size ?? 0,
},
{
accessor: "key_frame_size",
title: "关键帧数",
width: 120,
render: (record) => record.label_object_size?.key_frame_size ?? 0,
},
{
accessor: "dynamic_size",
title: "动态对象数",
width: 120,
render: (record) => record.label_object_size?.dynamic_size ?? 0,
},
{
accessor: "static_size",
title: "静态对象数",
width: 120,
render: (record) => record.label_object_size?.static_size ?? 0,
},
{
accessor: "question_size",
title: "问题数",
width: 120,
render: (record) => record.label_object_size?.question_size ?? 0,
},
{
accessor: "review_size",
title: "审核对象数",
width: 120,
render: (record) => record.label_object_size?.review_size ?? 0,
},
{
accessor: "first_comment_size",
title: "批注数(1)",
width: 120,
render: (record) => record.label_object_size?.first_comment_size ?? 0,
},
{
accessor: "second_comment_size",
title: "批注数(2)",
width: 120,
render: (record) => record.label_object_size?.second_comment_size ?? 0,
},
{
accessor: "first_commit_label_time",
title: "标注首次提交时间",
width: 180,
},
{
accessor: "first_commit_review1_time",
title: "审核首次提交时间",
width: 180,
},
{ accessor: "reject_times", title: "驳回次数", width: 100 },
{
accessor: "label_work_time",
title: "标注总耗时",
width: 120,
render: (record) => record.work_time?.label_work_time ?? 0,
},
{
accessor: "first_review_work_time",
title: "审核总耗时",
width: 120,
render: (record) => record.work_time?.first_review_work_time ?? 0,
},
{
accessor: "second_review_work_time",
title: "复审总耗时",
width: 120,
render: (record) => record.work_time?.second_review_work_time ?? 0,
},
]
return cols
}, [])
const handleExport = () => {
if (records.length === 0) {
notifications.show({
color: "yellow",
title: "暂无数据",
message: "当前无可导出任务数据",
})
return
}
const header = [
"项目名称",
"任务ID",
"状态",
"当前负责人",
"标注员",
"审核员",
"复审员",
"对象总数",
"新增对象数",
"关键帧数",
"动态对象数",
"静态对象数",
"问题数",
"审核对象数",
"批注数(1)",
"批注数(2)",
"标注首次提交时间",
"审核首次提交时间",
"驳回次数",
"标注总耗时",
"审核总耗时",
"复审总耗时",
]
const rows = records.map((item) => [
item.project_name,
item.id,
`${TaskStatusEnum.get(item.label_status) ?? "-"}${item.rejected ? "返工" : ""}`,
item.current_username ?? "-",
item.label_username ?? "-",
item.review1_username ?? "-",
item.review2_username ?? "-",
item.label_object_size?.object_size ?? 0,
item.label_object_size?.new_size ?? 0,
item.label_object_size?.key_frame_size ?? 0,
item.label_object_size?.dynamic_size ?? 0,
item.label_object_size?.static_size ?? 0,
item.label_object_size?.question_size ?? 0,
item.label_object_size?.review_size ?? 0,
item.label_object_size?.first_comment_size ?? 0,
item.label_object_size?.second_comment_size ?? 0,
item.first_commit_label_time ?? "-",
item.first_commit_review1_time ?? "-",
item.reject_times ?? 0,
item.work_time?.label_work_time ?? 0,
item.work_time?.first_review_work_time ?? 0,
item.work_time?.second_review_work_time ?? 0,
])
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet([header, ...rows])
XLSX.utils.book_append_sheet(wb, ws, "任务列表数据")
XLSX.writeFile(wb, "任务列表数据.xlsx")
}
return (
<Stack h="100%" gap="sm">
<Paper withBorder p="sm" radius="sm">
<Stack gap="xs">
<Group justify="space-between" wrap="wrap">
<Group gap="sm">
<NumberInput
label="展示数量"
size="xs"
radius="xs"
min={10}
step={100}
value={filters.page_size}
onChange={(v) =>
setFilters((s) => ({ ...s, page_size: Number(v) || 1000 }))
}
style={{ width: 140 }}
/>
<Text size="sm" c="dimmed">
{records.length} / {total}
</Text>
</Group>
<Group>
<Button
variant="subtle"
size="xs"
radius="xs"
rightSection={
collapsed ? (
<IconChevronDown size={14} />
) : (
<IconChevronUp size={14} />
)
}
onClick={() => setCollapsed((v) => !v)}>
{collapsed ? "展开筛选" : "收起筛选"}
</Button>
<Button
size="xs"
radius="xs"
leftSection={<IconSearch size={16} />}
onClick={() => handleSearch(filters)}>
</Button>
<Button
variant="default"
size="xs"
radius="xs"
leftSection={<IconRefresh size={16} />}
onClick={() => {
setFilters(emptyFilters)
handleSearch(emptyFilters)
}}>
</Button>
<Button
variant="default"
size="xs"
radius="xs"
leftSection={<IconDownload size={16} />}
onClick={handleExport}>
</Button>
</Group>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="sm">
<MultiSelect
label="项目类型"
size="xs"
radius="xs"
data={projectTypeOpts}
searchable
clearable
value={filters.project_type}
onChange={(v) => setFilters((s) => ({ ...s, project_type: v }))}
/>
<MultiSelect
label="项目名称"
size="xs"
radius="xs"
data={projectNameOpts}
searchable
clearable
value={filters.project_id}
onChange={(v) => setFilters((s) => ({ ...s, project_id: v }))}
/>
<MultiSelect
label="任务状态"
size="xs"
radius="xs"
data={Array.from(TaskStatusEnum.entries()).map(
([value, label]) => ({
label,
value: String(value),
})
)}
searchable
clearable
value={filters.label_status}
onChange={(v) => setFilters((s) => ({ ...s, label_status: v }))}
/>
</SimpleGrid>
<Collapse in={!collapsed}>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="sm">
<MultiSelect
label="当前负责人"
size="xs"
radius="xs"
data={userOptions}
searchable
clearable
value={filters.current_uid}
onChange={(v) => setFilters((s) => ({ ...s, current_uid: v }))}
/>
<MultiSelect
label="标注员"
size="xs"
radius="xs"
data={userOptions}
searchable
clearable
value={filters.label_user}
onChange={(v) => setFilters((s) => ({ ...s, label_user: v }))}
/>
<MultiSelect
label="审核员"
size="xs"
radius="xs"
data={userOptions}
searchable
clearable
value={filters.review_user1}
onChange={(v) => setFilters((s) => ({ ...s, review_user1: v }))}
/>
<MultiSelect
label="复审员"
size="xs"
radius="xs"
data={userOptions}
searchable
clearable
value={filters.review_user2}
onChange={(v) => setFilters((s) => ({ ...s, review_user2: v }))}
/>
</SimpleGrid>
</Collapse>
</Stack>
</Paper>
<Paper
withBorder
p="sm"
radius="sm"
style={{ flex: 1, minHeight: 0, display: "flex" }}>
<DataTable<Task.DataProps>
withTableBorder
withRowBorders
fetching={loading}
records={records}
columns={columns}
noRecordsText="暂无数据"
idAccessor="id"
scrollAreaProps={{ type: "auto" }}
style={{ width: "100%" }}
/>
</Paper>
<Paper withBorder p="sm" radius="sm">
<Group wrap="wrap" gap="md">
<Text size="sm" fw={700}>
</Text>
<Text size="sm">{totalSize?.object_size ?? 0}</Text>
<Text size="sm">{totalSize?.new_size ?? 0}</Text>
<Text size="sm">{totalSize?.key_frame_size ?? 0}</Text>
<Text size="sm">{totalSize?.dynamic_size ?? 0}</Text>
<Text size="sm">{totalSize?.static_size ?? 0}</Text>
<Text size="sm">{totalSize?.question_size ?? 0}</Text>
<Text size="sm">{totalSize?.review_size ?? 0}</Text>
<Text size="sm">(1){totalSize?.first_comment_size ?? 0}</Text>
<Text size="sm">
(2){totalSize?.second_comment_size ?? 0}
</Text>
</Group>
</Paper>
</Stack>
)
}