"use client" import { ProjectDetail } from "@/components/label/api/project/typing" import { getTaskList, taskRelease } from "@/components/label/api/task" import { Task } from "@/components/label/api/task/typing" import { useBackUrlStore, usePermissionStore, } from "@/components/label/store/auth" import { TaskStatusEnum, TaskStatusOpts, } from "@/components/label/utils/constants" import { ActionIcon, Button, Collapse, Flex, Group, MultiSelect, Paper, Select, SimpleGrid, Stack, Text, TextInput, } from "@mantine/core" import { modals } from "@mantine/modals" import { notifications } from "@mantine/notifications" import { IconChevronDown, IconChevronUp, IconRefresh, IconSearch, } from "@tabler/icons-react" import dayjs from "dayjs" import duration from "dayjs/plugin/duration" import { DataTable, DataTableColumn } from "mantine-datatable" import { useRouter, useSearchParams } from "next/navigation" import { useCallback, useEffect, useMemo, useState } from "react" import * as XLSX from "xlsx-js-style" import DispatchModal from "./components/DispatchModal" import ReleaseModal from "./components/ReleaseModal" import TaskStatusTag from "./components/TaskStatusTag" import WorkflowModal from "./components/WorkflowModal" dayjs.extend(duration) interface FilterFormRecord { search_id: string search_status: string rejected: string current_uid: string[] label_user: string[] review_user1: string[] review_user2: string[] } function createInitialFilters(): FilterFormRecord { return { search_id: "", search_status: "0", rejected: "", current_uid: [], label_user: [], review_user1: [], review_user2: [], } } export interface selectedDataProps { status: number uid: number | null reject: boolean } const checkSelectedData: ( keys: React.Key[], records: Task.DataProps[] ) => { flag: boolean resultData: selectedDataProps } = (keys: React.Key[], records: Task.DataProps[]) => { const checkData = records.filter((item: any) => keys.includes(item.id)) const initStatus = checkData[0].label_status const initReject = checkData[0].rejected const initCurrentUid = checkData[0].current_uid || null const initCurrentName = checkData[0].current_username || "" if (checkData.length === 1) return { flag: true, resultData: { uid: initCurrentUid, status: initStatus, reject: initReject, }, } let statusFlag = true let currentFlag = true let rejectFlag = true checkData.forEach((item: any) => { const s = item.label_status const c = item.current_username || "" const r = item.rejected if (s !== initStatus) statusFlag = false if (c !== initCurrentName) currentFlag = false if (r !== initReject) rejectFlag = false }) return { flag: statusFlag && currentFlag && rejectFlag, resultData: { uid: initCurrentUid, status: initStatus, reject: initReject, }, } } export default function TaskTableContainer(props: { info: ProjectDetail.DataProps | undefined userEnums: { allEnum: Map labelEnum: Map review1Enum: Map review2Enum: Map } }) { const { info, userEnums } = props false && console.log(info) const { user_id } = usePermissionStore() const urlParams = useSearchParams() const id = urlParams.get("id") const type = urlParams.get("type") const projectId = typeof id === "string" && !isNaN(Number(id)) ? Number(id) : -1 const router = useRouter() const { setBackProps } = useBackUrlStore() const [total, setTotal] = useState(0) const [pageNumber, setPageNumber] = useState(1) const [pageSize, setPageSize] = useState(15) const [searchExpanded, setSearchExpanded] = useState(false) const [filters, setFilters] = useState(() => createInitialFilters()) const [form, setForm] = useState(createInitialFilters()) const [records, setRecords] = useState([]) const [selectedRecords, setSelectedRecords] = useState([]) const [loading, setLoading] = useState(false) const [queryTrigger, setQueryTrigger] = useState(0) const [workflowTaskId, setWorkflowTaskId] = useState(null) const [dispatchRecordModal, setDispatchModalConfig] = useState<{ open: boolean dispatchData?: selectedDataProps task_ids: Task.DataProps[] }>({ open: false, task_ids: [], }) const [releaseRecord, setReleaseRecord] = useState( null ) const userOptions = useMemo(() => { const arr: Array<{ label: string; value: string }> = [] userEnums.allEnum?.forEach((label, value) => { arr.push({ label: String(label), value: String(value) }) }) return arr }, [userEnums.allEnum]) const appliedParams = useMemo(() => { const obj: Task.ListRequest = { page_number: pageNumber ?? 1, page_size: pageSize ?? 20, project_id: projectId !== -1 ? [projectId] : undefined, get_data: false, } if (form.search_id) { const idValue = Number(form.search_id) if (Number.isFinite(idValue)) obj.id = [idValue] } if (form.search_status !== "0") { const statusValue = Number(form.search_status) if (Number.isFinite(statusValue)) obj.label_status = [statusValue] } else { delete obj.label_status } if (form.rejected !== "") { const rejected = Number(form.rejected) if (Number.isFinite(rejected)) obj.rejected = rejected === 1 ? true : false } if (form.current_uid?.length) { obj.current_uid = form.current_uid?.map((item) => Number(item)) } if (form.label_user?.length) { obj.label_user = form.label_user?.map((item) => Number(item)) } if (form.review_user1?.length) { obj.review_user1 = form.review_user1?.map((item) => Number(item)) } if (form.review_user2?.length) { obj.review_user2 = form.review_user2?.map((item) => Number(item)) } return obj }, [ form.current_uid, form.label_user, form.rejected, form.review_user1, form.review_user2, form.search_id, form.search_status, pageNumber, pageSize, projectId, ]) const load = useCallback(async () => { if (!queryTrigger) return if (projectId === -1) return setLoading(true) try { const res = await getTaskList(appliedParams) setRecords(res?.task_list ?? []) setTotal(res?.total_items ?? 0) } catch (e) { setRecords([]) setTotal(0) notifications.show({ color: "red", title: "加载失败", message: e instanceof Error ? e.message : "请求失败", }) } finally { setLoading(false) } }, [appliedParams, projectId, queryTrigger, setTotal]) useEffect(() => { load() }, [load]) useEffect(() => { queueMicrotask(() => { setQueryTrigger((v) => v + 1) }) }, []) const clickDispatchTask = () => { if (!selectedRecords.length) { notifications.show({ color: "yellow", title: "警告", message: "请先选择任务", }) return } const { flag, resultData } = checkSelectedData( selectedRecords.map((item) => item.id), records ) if (!flag) { notifications.show({ color: "yellow", title: "警告", message: "请保证选取任务的标注状态及负责人一致", }) return } setDispatchModalConfig({ open: true, dispatchData: resultData, task_ids: selectedRecords, }) } const clickReleaseTask = () => { if (!selectedRecords.length) { notifications.show({ color: "yellow", title: "警告", message: "请先选择任务", }) return } const { flag, resultData } = checkSelectedData( selectedRecords.map((item) => item.id), records ) if (!flag) { notifications.show({ color: "yellow", title: "警告", message: "请保证选取任务的标注状态及负责人一致", }) return } if (![2, 4, 6, 7].includes(resultData.status)) { notifications.show({ color: "red", title: "错误", message: "所选任务状态无法释放", }) return } modals.openConfirmModal({ title: "释放任务", centered: true, children: 是否释放所选所有任务?, labels: { confirm: "释放", cancel: "取消" }, confirmProps: { color: "red" }, onConfirm: async () => { const reqParams = { operation_uid: user_id, task_ids: selectedRecords.map((item) => item.id), uid: resultData.uid || 0, reject: resultData.reject, task_status: resultData.status, } try { await taskRelease(reqParams) notifications.show({ color: "green", title: "操作成功", message: "已释放所选所有任务", }) load() } catch {} }, }) } const handleExport = async () => { try { notifications.show({ color: "yellow", title: "提示", message: "导出统计数据中...", }) const res = await getTaskList({ id: selectedRecords.map((item) => item.id), page_number: 1, page_size: selectedRecords.length, get_data: false, }) const { task_list } = res const exportTableHeader = [ "任务ID", "状态", "当前负责人", "标注员", "审核员", "复审员", "对象总数", "有效对象数", "新增对象数", "关键帧数", "动态对象数", "静态对象数", "预标注对象数", "预标注对象修改数", "问题数", "总问答组数", "单轮问答组数", "多轮问答组数", "新增问答组数", "新增单轮问答组数", "新增多轮问答组数", "审核问题数", "首次批注问题数", "审核对象数", "审核动态对象数", "审核静态对象数", "批注数(1)", "批注数(2)", "标注首次提交时间", "审核首次提交时间", "驳回次数", "标注总耗时", "审核总耗时", "复审总耗时", "标注首次耗时", "审核首次耗时", "复审首次耗时", "审核首次驳回时间", ] let data = task_list.map((item) => { return [ 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, item.label_object_size.valid_size, item.label_object_size.new_size, item.label_object_size.key_frame_size, item.label_object_size.dynamic_size, item.label_object_size.static_size, item.label_object_size.prelabel_size, item.label_object_size.prelabel_modify_size, item.label_object_size.question_size, item.label_object_size.total_qa_group_size, item.label_object_size.single_qa_group_size, item.label_object_size.multiple_qa_group_size, item.label_object_size.new_qa_group_size, item.label_object_size.new_single_qa_group_size, item.label_object_size.new_multiple_qa_group_size, item.label_object_size.review_question_size, item.label_object_size.comment_question_size, item.label_object_size.review_size, item.label_object_size.review_dynamic_size, item.label_object_size.review_static_size, item.label_object_size.first_comment_size, item.label_object_size.second_comment_size, item.first_commit_label_time, item.first_commit_review1_time, item.reject_times, item.work_time.label_work_time, item.work_time.first_review_work_time, item.work_time.second_review_work_time, item.label_first_work_time, item.review1_first_work_time, item.review2_first_work_time, item.first_reject_label_time, ] }) const exportData = [exportTableHeader, ...data] const wb = XLSX.utils.book_new() const ws = XLSX.utils.aoa_to_sheet(exportData) XLSX.utils.book_append_sheet(wb, ws, "任务列表数据") XLSX.writeFile(wb, `任务列表数据.xlsx`) } catch {} } const getTime = (time: number) => { return dayjs.duration(time, "seconds").format("HH:mm:ss") } const columns = useMemo(() => { const cols: DataTableColumn[] = [ { accessor: "id", title: "任务ID", width: 90, render: (record) => ( ), }, { accessor: "label_status", title: "状态", width: 110, render: (record) => ( ), }, { accessor: "current_username", title: "当前负责人", width: 120, render: (record) => record.current_username ?? "-", }, { accessor: "label_username", title: "标注员", width: 120, render: (record) => record.label_username ?? "-", }, { accessor: "review1_username", title: "审核员", width: 120, render: (record) => record.review1_username ?? "-", }, { accessor: "review2_username", title: "复审员", width: 120, render: (record) => record.review2_username ?? "-", }, { accessor: "label_object_size.object_size", title: "对象总数", width: 110, render: (record) => record.label_object_size?.object_size ?? "-", }, { accessor: "label_object_size.valid_size", title: "有效对象数", width: 120, render: (record) => record.label_object_size?.valid_size ?? "-", }, { accessor: "label_object_size.new_size", title: "新增对象数", width: 120, render: (record) => record.label_object_size?.new_size ?? "-", }, { accessor: "label_object_size.key_frame_size", title: "关键帧数", width: 120, render: (record) => record.label_object_size?.key_frame_size ?? "-", }, { accessor: "label_object_size.dynamic_size", title: "动态对象数", width: 120, render: (record) => record.label_object_size?.dynamic_size ?? "-", }, { accessor: "label_object_size.static_size", title: "静态对象数", width: 120, render: (record) => record.label_object_size?.static_size ?? "-", }, { accessor: "label_object_size.prelabel_size", title: "预标注对象数", width: 120, render: (record) => record.label_object_size?.prelabel_size ?? "-", }, { accessor: "label_object_size.prelabel_modify_size", title: "预标注对象修改数", width: 160, render: (record) => record.label_object_size?.prelabel_modify_size ?? "-", }, { accessor: "label_object_size.question_size", title: "问题数", width: 120, render: (record) => record.label_object_size?.question_size ?? "-", }, { accessor: "label_object_size.total_qa_group_size", title: "总问答组数", width: 120, render: (record) => record.label_object_size?.total_qa_group_size ?? "-", }, { accessor: "label_object_size.single_qa_group_size", title: "单轮问答组数", width: 120, render: (record) => record.label_object_size?.single_qa_group_size ?? "-", }, { accessor: "label_object_size.multiple_qa_group_size", title: "多轮问答组数", width: 120, render: (record) => record.label_object_size?.multiple_qa_group_size ?? "-", }, { accessor: "label_object_size.new_qa_group_size", title: "新增问答组数", width: 120, render: (record) => record.label_object_size?.new_qa_group_size ?? "-", }, { accessor: "label_object_size.new_single_qa_group_size", title: "新增单轮问答组数", width: 160, render: (record) => record.label_object_size?.new_single_qa_group_size ?? "-", }, { accessor: "label_object_size.new_multiple_qa_group_size", title: "新增多轮问答组数", width: 160, render: (record) => record.label_object_size?.new_multiple_qa_group_size ?? "-", }, { accessor: "label_object_size.review_question_size", title: "审核问题数", width: 120, render: (record) => record.label_object_size?.review_question_size ?? "-", }, { accessor: "label_object_size.comment_question_size", title: "首次批注问题数", width: 140, render: (record) => record.label_object_size?.comment_question_size ?? "-", }, { accessor: "label_object_size.review_size", title: "审核对象数", width: 120, render: (record) => record.label_object_size?.review_size ?? "-", }, { accessor: "label_object_size.review_dynamic_size", title: "审核动态对象数", width: 140, render: (record) => record.label_object_size?.review_dynamic_size ?? "-", }, { accessor: "label_object_size.review_static_size", title: "审核静态对象数", width: 140, render: (record) => record.label_object_size?.review_static_size ?? "-", }, { accessor: "label_object_size.first_comment_size", title: "批注数(1)", width: 120, render: (record) => record.label_object_size?.first_comment_size ?? "-", }, { accessor: "label_object_size.second_comment_size", title: "批注数(2)", width: 120, render: (record) => record.label_object_size?.second_comment_size ?? "-", }, { accessor: "first_commit_label_time", title: "标注首次提交时间", width: 180, render: (record) => record.first_commit_label_time ?? "-", }, { accessor: "first_commit_review1_time", title: "审核首次提交时间", width: 180, render: (record) => record.first_commit_review1_time ?? "-", }, { accessor: "reject_times", title: "驳回次数", width: 120, render: (record) => record.reject_times ?? "-", }, { accessor: "work_time.label_work_time", title: "标注总耗时", width: 120, render: (record) => record.work_time?.label_work_time ? getTime(record.work_time?.label_work_time) : "-", }, { accessor: "work_time.first_review_work_time", title: "审核总耗时", width: 120, render: (record) => record.work_time?.first_review_work_time ? getTime(record.work_time?.first_review_work_time) : "-", }, { accessor: "work_time.second_review_work_time", title: "复审总耗时", width: 120, render: (record) => record.work_time?.second_review_work_time ? getTime(record.work_time?.second_review_work_time) : "-", }, { accessor: "label_first_work_time", title: "标注首次耗时", width: 120, render: (record) => record.label_first_work_time ? getTime(record.label_first_work_time) : "-", }, { accessor: "review1_first_work_time", title: "审核首次耗时", width: 120, render: (record) => record.review1_first_work_time ? getTime(record.review1_first_work_time) : "-", }, { accessor: "review2_first_work_time", title: "复审首次耗时", width: 120, render: (record) => record.review2_first_work_time ? getTime(record.review2_first_work_time) : "-", }, { accessor: "first_reject_label_time", title: "审核首次驳回时间", width: 180, render: (record) => record.first_reject_label_time ?? "-", }, { accessor: "operation", title: "操作", width: 140, textAlign: "center", render: (record) => ( ), }, ] return cols }, [info, projectId, router, setBackProps, type]) return ( <> 任务列表 setFilters((s) => ({ ...s, search_id: e.target.value, })) } /> setFilters((s) => ({ ...s, rejected: v ?? "" })) } allowDeselect={false} /> ({ label: y, value: x.toString() }) )} searchable clearable value={filters.current_uid} onChange={(v) => setFilters((s) => ({ ...s, current_uid: v })) } /> ({ label: y, value: x.toString() }) )} searchable clearable value={filters.label_user} onChange={(v) => setFilters((s) => ({ ...s, label_user: v }))} /> ({ label: y, value: x.toString() }) )} searchable clearable value={filters.review_user1} onChange={(v) => setFilters((s) => ({ ...s, review_user1: v })) } /> ({ label: y, value: x.toString() }) )} searchable clearable value={filters.review_user2} onChange={(v) => setFilters((s) => ({ ...s, review_user2: v })) } /> width="100%" style={{ width: "100%" }} withTableBorder withRowBorders pinFirstColumn pinLastColumn scrollAreaProps={{ type: "auto" }} fetching={loading} records={records} columns={columns} totalRecords={total} recordsPerPage={pageSize} page={pageNumber} onPageChange={(p) => setPageNumber(p)} onRecordsPerPageChange={(s) => { setPageSize(s) setPageNumber(1) }} selectedRecords={selectedRecords} onSelectedRecordsChange={setSelectedRecords} recordsPerPageOptions={[10, 15, 20]} noRecordsText="暂无数据" recordsPerPageLabel="当前页数" /> setWorkflowTaskId(null)} /> { setDispatchModalConfig({ open: false, task_ids: [], }) if (refresh) setQueryTrigger((v) => v + 1) }} /> { setReleaseRecord(null) if (refresh) setQueryTrigger((v) => v + 1) }} /> ) }