521 lines
16 KiB
TypeScript
521 lines
16 KiB
TypeScript
"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>
|
||
)
|
||
}
|