1170 lines
36 KiB
TypeScript
1170 lines
36 KiB
TypeScript
"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 {
|
||
SettingContentPanel,
|
||
SettingDataTable,
|
||
SettingFilterActions,
|
||
SettingFilterPanel,
|
||
SettingHeaderActions,
|
||
SettingListHeader,
|
||
} from "@/components/setting/PageSurface"
|
||
import {
|
||
Button,
|
||
Collapse,
|
||
Flex,
|
||
Group,
|
||
Modal,
|
||
MultiSelect,
|
||
Select,
|
||
SimpleGrid,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
} from "@mantine/core"
|
||
import { DatePickerInput, type DatesRangeValue } from "@mantine/dates"
|
||
import { modals } from "@mantine/modals"
|
||
import { notifications } from "@mantine/notifications"
|
||
import {
|
||
IconChevronDown,
|
||
IconChevronUp,
|
||
IconRefresh,
|
||
IconSearch,
|
||
} from "@tabler/icons-react"
|
||
import dayjs from "dayjs"
|
||
import "dayjs/locale/zh-cn"
|
||
import duration from "dayjs/plugin/duration"
|
||
import { DataTableColumn, type DataTableSortStatus } 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[]
|
||
first_commit_label_time: DatesRangeValue
|
||
first_commit_review1_time: DatesRangeValue
|
||
}
|
||
|
||
function createInitialFilters(): FilterFormRecord {
|
||
return {
|
||
search_id: "",
|
||
search_status: "0",
|
||
rejected: "",
|
||
current_uid: [],
|
||
label_user: [],
|
||
review_user1: [],
|
||
review_user2: [],
|
||
first_commit_label_time: [null, null],
|
||
first_commit_review1_time: [null, null],
|
||
}
|
||
}
|
||
|
||
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20]
|
||
const INITIAL_SORT_ACCESSOR = "__initial__"
|
||
|
||
type SortableTaskSizeAccessor = keyof Task.SizeProps
|
||
|
||
const SORTABLE_TASK_SIZE_COLUMNS: Array<{
|
||
accessor: SortableTaskSizeAccessor
|
||
title: string
|
||
width: number
|
||
}> = [
|
||
{ accessor: "object_size", title: "对象总数", width: 110 },
|
||
{ accessor: "valid_size", title: "有效对象数", width: 120 },
|
||
{ accessor: "new_size", title: "新增对象数", width: 120 },
|
||
{ accessor: "key_frame_size", title: "关键帧数", width: 120 },
|
||
{ accessor: "dynamic_size", title: "动态对象数", width: 120 },
|
||
{ accessor: "static_size", title: "静态对象数", width: 120 },
|
||
{ accessor: "prelabel_size", title: "预标注对象数", width: 120 },
|
||
{ accessor: "prelabel_modify_size", title: "预标注对象修改数", width: 160 },
|
||
{ accessor: "question_size", title: "问题数", width: 120 },
|
||
{ accessor: "total_qa_group_size", title: "总问答组数", width: 120 },
|
||
{ accessor: "single_qa_group_size", title: "单轮问答组数", width: 120 },
|
||
{ accessor: "multiple_qa_group_size", title: "多轮问答组数", width: 120 },
|
||
{ accessor: "new_qa_group_size", title: "新增问答组数", width: 120 },
|
||
{
|
||
accessor: "new_single_qa_group_size",
|
||
title: "新增单轮问答组数",
|
||
width: 160,
|
||
},
|
||
{
|
||
accessor: "new_multiple_qa_group_size",
|
||
title: "新增多轮问答组数",
|
||
width: 160,
|
||
},
|
||
{ accessor: "review_question_size", title: "审核问题数", width: 120 },
|
||
{ accessor: "comment_question_size", title: "首次批注问题数", width: 140 },
|
||
{ accessor: "review_size", title: "审核对象数", width: 120 },
|
||
{ accessor: "review_dynamic_size", title: "审核动态对象数", width: 140 },
|
||
{ accessor: "review_static_size", title: "审核静态对象数", width: 140 },
|
||
{ accessor: "first_comment_size", title: "批注数(1)", width: 120 },
|
||
{ accessor: "second_comment_size", title: "批注数(2)", width: 120 },
|
||
]
|
||
|
||
const EMPTY_TASK_SIZE: Task.SizeProps = Object.freeze({
|
||
object_size: 0,
|
||
valid_size: 0,
|
||
first_comment_size: 0,
|
||
second_comment_size: 0,
|
||
new_size: 0,
|
||
prelabel_size: 0,
|
||
prelabel_modify_size: 0,
|
||
dynamic_size: 0,
|
||
static_size: 0,
|
||
review_size: 0,
|
||
review_dynamic_size: 0,
|
||
review_static_size: 0,
|
||
key_frame_size: 0,
|
||
question_size: 0,
|
||
total_qa_group_size: 0,
|
||
single_qa_group_size: 0,
|
||
multiple_qa_group_size: 0,
|
||
new_qa_group_size: 0,
|
||
new_single_qa_group_size: 0,
|
||
new_multiple_qa_group_size: 0,
|
||
review_question_size: 0,
|
||
comment_question_size: 0,
|
||
})
|
||
|
||
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<any, any>
|
||
labelEnum: Map<any, any>
|
||
review1Enum: Map<any, any>
|
||
review2Enum: Map<any, any>
|
||
}
|
||
}) {
|
||
const { info, userEnums } = props
|
||
|
||
const { user_id, detailInfo } = usePermissionStore()
|
||
const hasRolePermissions = Boolean(detailInfo?.roles?.length)
|
||
|
||
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 [totalSize, setTotalSize] = useState<Task.SizeProps>(EMPTY_TASK_SIZE)
|
||
|
||
const [pageNumber, setPageNumber] = useState(1)
|
||
const [pageSize, setPageSize] = useState(15)
|
||
const [recordsPerPageOptions, setRecordsPerPageOptions] = useState<number[]>(
|
||
() => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS]
|
||
)
|
||
const [customPageSizeInput, setCustomPageSizeInput] = useState("")
|
||
const [customPageSizeModalOpened, setCustomPageSizeModalOpened] =
|
||
useState(false)
|
||
|
||
const [searchExpanded, setSearchExpanded] = useState(false)
|
||
|
||
const [filters, setFilters] = useState(() => createInitialFilters())
|
||
const [form, setForm] = useState<FilterFormRecord>(createInitialFilters())
|
||
|
||
const [records, setRecords] = useState<Task.DataProps[]>([])
|
||
const [selectedRecords, setSelectedRecords] = useState<Task.DataProps[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [queryTrigger, setQueryTrigger] = useState(0)
|
||
const [sortStatus, setSortStatus] = useState<
|
||
DataTableSortStatus<Task.DataProps>
|
||
>({
|
||
columnAccessor: INITIAL_SORT_ACCESSOR,
|
||
direction: "asc",
|
||
})
|
||
|
||
const [workflowTaskId, setWorkflowTaskId] = useState<number | null>(null)
|
||
const [dispatchRecordModal, setDispatchModalConfig] = useState<{
|
||
open: boolean
|
||
dispatchData?: selectedDataProps
|
||
task_ids: Task.DataProps[]
|
||
}>({
|
||
open: false,
|
||
task_ids: [],
|
||
})
|
||
const [releaseRecord, setReleaseRecord] = useState<Task.DataProps | null>(
|
||
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))
|
||
}
|
||
if (form.first_commit_label_time[0] && form.first_commit_label_time[1]) {
|
||
obj.first_commit_label_time_start = dayjs(
|
||
form.first_commit_label_time[0]
|
||
).format("YYYY-MM-DD")
|
||
obj.first_commit_label_time_end = dayjs(
|
||
form.first_commit_label_time[1]
|
||
).format("YYYY-MM-DD")
|
||
}
|
||
if (
|
||
form.first_commit_review1_time[0] &&
|
||
form.first_commit_review1_time[1]
|
||
) {
|
||
obj.first_commit_review1_time_start = dayjs(
|
||
form.first_commit_review1_time[0]
|
||
).format("YYYY-MM-DD")
|
||
obj.first_commit_review1_time_end = dayjs(
|
||
form.first_commit_review1_time[1]
|
||
).format("YYYY-MM-DD")
|
||
}
|
||
if (sortStatus.columnAccessor !== INITIAL_SORT_ACCESSOR) {
|
||
obj.order_by = `${sortStatus.columnAccessor} ${sortStatus.direction}`
|
||
}
|
||
return obj
|
||
}, [
|
||
form.current_uid,
|
||
form.first_commit_label_time,
|
||
form.first_commit_review1_time,
|
||
form.label_user,
|
||
form.rejected,
|
||
form.review_user1,
|
||
form.review_user2,
|
||
form.search_id,
|
||
form.search_status,
|
||
pageNumber,
|
||
pageSize,
|
||
projectId,
|
||
sortStatus.columnAccessor,
|
||
sortStatus.direction,
|
||
])
|
||
|
||
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)
|
||
setTotalSize(res?.total_size ?? EMPTY_TASK_SIZE)
|
||
} catch (e) {
|
||
setRecords([])
|
||
setTotal(0)
|
||
setTotalSize(EMPTY_TASK_SIZE)
|
||
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 openCustomPageSizeModal = useCallback(() => {
|
||
setCustomPageSizeModalOpened(true)
|
||
}, [])
|
||
|
||
const closeCustomPageSizeModal = useCallback(() => {
|
||
setCustomPageSizeModalOpened(false)
|
||
setCustomPageSizeInput("")
|
||
}, [])
|
||
|
||
const handleAddCustomPageSize = useCallback(() => {
|
||
const rawValue = customPageSizeInput.trim()
|
||
if (!rawValue) return
|
||
const size = Number(rawValue)
|
||
if (!Number.isInteger(size) || size <= 0) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "输入无效",
|
||
message: "请填写大于 0 的整数条数",
|
||
})
|
||
return
|
||
}
|
||
if (recordsPerPageOptions.includes(size)) {
|
||
notifications.show({
|
||
color: "blue",
|
||
title: "已存在",
|
||
message: `每页 ${size} 条已在可选项中`,
|
||
})
|
||
return
|
||
}
|
||
setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b))
|
||
closeCustomPageSizeModal()
|
||
notifications.show({
|
||
color: "green",
|
||
title: "添加成功",
|
||
message: `已新增每页 ${size} 条`,
|
||
})
|
||
}, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions])
|
||
|
||
const handleCustomPageSizeEnter = useCallback(
|
||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||
if (event.key !== "Enter") return
|
||
event.preventDefault()
|
||
handleAddCustomPageSize()
|
||
},
|
||
[handleAddCustomPageSize]
|
||
)
|
||
|
||
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: <Text size="sm">是否释放所选所有任务?</Text>,
|
||
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 handleSearch = () => {
|
||
setPageNumber(1)
|
||
setForm({ ...filters })
|
||
setQueryTrigger((v) => v + 1)
|
||
}
|
||
|
||
const handleReset = () => {
|
||
const next = createInitialFilters()
|
||
setFilters(next)
|
||
setForm(next)
|
||
setPageNumber(1)
|
||
setQueryTrigger((v) => v + 1)
|
||
}
|
||
|
||
const handleRefresh = () => {
|
||
setQueryTrigger((v) => v + 1)
|
||
}
|
||
|
||
const getTime = (time: number) => {
|
||
return dayjs.duration(time, "seconds").format("HH:mm:ss")
|
||
}
|
||
|
||
const columns = useMemo(() => {
|
||
const createSortableSizeColumn = ({
|
||
accessor,
|
||
title,
|
||
width,
|
||
}: {
|
||
accessor: SortableTaskSizeAccessor
|
||
title: string
|
||
width: number
|
||
}): DataTableColumn<Task.DataProps> => ({
|
||
accessor,
|
||
title,
|
||
width,
|
||
sortable: true,
|
||
render: (record) => record.label_object_size?.[accessor] ?? "-",
|
||
footer: totalSize[accessor] ?? 0,
|
||
})
|
||
|
||
const cols: DataTableColumn<Task.DataProps>[] = [
|
||
{
|
||
accessor: "id",
|
||
title: "任务ID",
|
||
width: 90,
|
||
footer: "总计",
|
||
render: (record) => (
|
||
<Button
|
||
variant="transparent"
|
||
size="xs"
|
||
style={{ paddingLeft: 6, paddingRight: 6 }}
|
||
onClick={async () => {
|
||
const backUrl = type
|
||
? `/project/detail?id=${projectId}&type=${type}`
|
||
: `/project/detail?id=${projectId}`
|
||
setBackProps(backUrl, "all", "/image")
|
||
|
||
if (info && [4, 5, 6, 7].includes(info.label_type)) {
|
||
router.push(
|
||
`/label?project_id=${projectId}&task_id=${record.id}`
|
||
)
|
||
} else {
|
||
// await runCommand(
|
||
// String(record.id),
|
||
// user_name ?? "",
|
||
// user_password ?? ""
|
||
// )
|
||
}
|
||
}}>
|
||
{record.id}
|
||
</Button>
|
||
),
|
||
},
|
||
{
|
||
accessor: "label_status",
|
||
title: "状态",
|
||
width: 150,
|
||
render: (record) => (
|
||
<TaskStatusTag
|
||
status={record.label_status}
|
||
rejected={record.rejected}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
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 ?? "-",
|
||
},
|
||
...SORTABLE_TASK_SIZE_COLUMNS.map(createSortableSizeColumn),
|
||
{
|
||
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) => (
|
||
<Flex gap={6} justify="space-around" wrap="nowrap">
|
||
{hasRolePermissions ? (
|
||
<Button
|
||
px={0}
|
||
variant="transparent"
|
||
onClick={() => {
|
||
setDispatchModalConfig({
|
||
open: true,
|
||
dispatchData: {
|
||
status: record.label_status,
|
||
uid: record.current_uid || null,
|
||
reject: record.rejected,
|
||
},
|
||
task_ids: [record],
|
||
})
|
||
}}>
|
||
调度
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
px={0}
|
||
variant="transparent"
|
||
onClick={() => setWorkflowTaskId(record.id)}>
|
||
流转记录
|
||
</Button>
|
||
</Flex>
|
||
),
|
||
},
|
||
]
|
||
return cols
|
||
}, [
|
||
hasRolePermissions,
|
||
info,
|
||
projectId,
|
||
router,
|
||
setBackProps,
|
||
totalSize,
|
||
type,
|
||
])
|
||
|
||
return (
|
||
<>
|
||
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
||
<SettingFilterPanel>
|
||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||
<Group gap="xs">
|
||
<Text fw={700}>筛选条件</Text>
|
||
<Button
|
||
variant="transparent"
|
||
rightSection={
|
||
searchExpanded ? (
|
||
<IconChevronUp size={16} />
|
||
) : (
|
||
<IconChevronDown size={16} />
|
||
)
|
||
}
|
||
onClick={() => setSearchExpanded((v) => !v)}>
|
||
{searchExpanded ? "收起筛选" : "展开筛选"}
|
||
</Button>
|
||
</Group>
|
||
</Group>
|
||
<Collapse
|
||
in={searchExpanded}
|
||
transitionDuration={250}
|
||
transitionTimingFunction="ease"
|
||
animateOpacity>
|
||
<Stack gap="md" mt="md">
|
||
<SimpleGrid
|
||
cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
|
||
spacing="md"
|
||
verticalSpacing="md">
|
||
<TextInput
|
||
label="任务ID"
|
||
placeholder="请输入任务 ID"
|
||
value={filters.search_id}
|
||
onChange={(e) =>
|
||
setFilters((s) => ({
|
||
...s,
|
||
search_id: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<Select
|
||
label="任务状态"
|
||
data={Object.entries(TaskStatusOpts).map(
|
||
([value, label]) => ({
|
||
value,
|
||
label,
|
||
})
|
||
)}
|
||
value={filters.search_status}
|
||
onChange={(v) =>
|
||
setFilters((s) => ({ ...s, search_status: v ?? "0" }))
|
||
}
|
||
allowDeselect={false}
|
||
/>
|
||
<Select
|
||
label="是否返工"
|
||
data={[
|
||
{ label: "全部", value: "" },
|
||
{ label: "是", value: "1" },
|
||
{ label: "否", value: "0" },
|
||
]}
|
||
value={filters.rejected}
|
||
onChange={(v) =>
|
||
setFilters((s) => ({ ...s, rejected: v ?? "" }))
|
||
}
|
||
allowDeselect={false}
|
||
/>
|
||
<MultiSelect
|
||
label="当前负责人"
|
||
data={Array.from(userEnums.allEnum.entries()).map(
|
||
([x, y]) => ({ label: y, value: x.toString() })
|
||
)}
|
||
searchable
|
||
clearable
|
||
value={filters.current_uid}
|
||
onChange={(v) =>
|
||
setFilters((s) => ({ ...s, current_uid: v }))
|
||
}
|
||
/>
|
||
<MultiSelect
|
||
label="标注员"
|
||
data={Array.from(userEnums.labelEnum.entries()).map(
|
||
([x, y]) => ({ label: y, value: x.toString() })
|
||
)}
|
||
searchable
|
||
clearable
|
||
value={filters.label_user}
|
||
onChange={(v) => setFilters((s) => ({ ...s, label_user: v }))}
|
||
/>
|
||
<MultiSelect
|
||
label="审核员"
|
||
data={Array.from(userEnums.review1Enum.entries()).map(
|
||
([x, y]) => ({ label: y, value: x.toString() })
|
||
)}
|
||
searchable
|
||
clearable
|
||
value={filters.review_user1}
|
||
onChange={(v) =>
|
||
setFilters((s) => ({ ...s, review_user1: v }))
|
||
}
|
||
/>
|
||
<MultiSelect
|
||
label="复审员"
|
||
data={Array.from(userEnums.review2Enum.entries()).map(
|
||
([x, y]) => ({ label: y, value: x.toString() })
|
||
)}
|
||
searchable
|
||
clearable
|
||
value={filters.review_user2}
|
||
onChange={(v) =>
|
||
setFilters((s) => ({ ...s, review_user2: v }))
|
||
}
|
||
/>
|
||
<DatePickerInput
|
||
type="range"
|
||
label="标注首次提交时间"
|
||
placeholder="请选择时间范围"
|
||
valueFormat="YYYY-MM-DD"
|
||
locale="zh-cn"
|
||
clearable
|
||
value={filters.first_commit_label_time}
|
||
onChange={(value: DatesRangeValue) =>
|
||
setFilters((s) => ({
|
||
...s,
|
||
first_commit_label_time: value,
|
||
}))
|
||
}
|
||
/>
|
||
<DatePickerInput
|
||
type="range"
|
||
label="审核首次提交时间"
|
||
placeholder="请选择时间范围"
|
||
valueFormat="YYYY-MM-DD"
|
||
locale="zh-cn"
|
||
clearable
|
||
value={filters.first_commit_review1_time}
|
||
onChange={(value: DatesRangeValue) =>
|
||
setFilters((s) => ({
|
||
...s,
|
||
first_commit_review1_time: value,
|
||
}))
|
||
}
|
||
/>
|
||
</SimpleGrid>
|
||
</Stack>
|
||
</Collapse>
|
||
<SettingFilterActions>
|
||
<Button
|
||
variant="default"
|
||
leftSection={<IconRefresh size={16} />}
|
||
onClick={handleReset}>
|
||
重置
|
||
</Button>
|
||
<Button
|
||
leftSection={<IconSearch size={16} />}
|
||
onClick={handleSearch}
|
||
loading={loading}>
|
||
查询
|
||
</Button>
|
||
</SettingFilterActions>
|
||
</SettingFilterPanel>
|
||
<SettingContentPanel
|
||
style={{
|
||
flex: 1,
|
||
minHeight: 0,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}>
|
||
<Stack gap="sm" align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
||
<SettingListHeader
|
||
title="任务列表"
|
||
count={
|
||
selectedRecords.length
|
||
? `${total} 条 / 已选 ${selectedRecords.length}`
|
||
: `${total} 条`
|
||
}
|
||
actions={
|
||
<SettingHeaderActions>
|
||
{hasRolePermissions ? (
|
||
<Button
|
||
variant="default"
|
||
leftSection={<IconRefresh size={16} />}
|
||
onClick={handleRefresh}
|
||
loading={loading}>
|
||
刷新
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
variant="default"
|
||
disabled={!selectedRecords.length}
|
||
onClick={handleExport}>
|
||
下载选中项
|
||
</Button>
|
||
{hasRolePermissions ? (
|
||
<Button
|
||
variant="default"
|
||
disabled={!selectedRecords.length}
|
||
onClick={clickDispatchTask}>
|
||
调度选中项
|
||
</Button>
|
||
) : null}
|
||
{hasRolePermissions ? (
|
||
<Button
|
||
variant="default"
|
||
disabled={!selectedRecords.length}
|
||
onClick={clickReleaseTask}>
|
||
释放选中项
|
||
</Button>
|
||
) : null}
|
||
</SettingHeaderActions>
|
||
}
|
||
/>
|
||
<SettingDataTable<Task.DataProps>
|
||
width="100%"
|
||
minHeight={0}
|
||
style={{ width: "100%", flex: 1 }}
|
||
pinFirstColumn
|
||
pinLastColumn
|
||
scrollAreaProps={{ type: "auto" }}
|
||
fetching={loading}
|
||
records={records}
|
||
columns={columns}
|
||
sortStatus={sortStatus}
|
||
onSortStatusChange={(
|
||
nextSortStatus: DataTableSortStatus<Task.DataProps>
|
||
) => {
|
||
setPageNumber(1)
|
||
setSortStatus(nextSortStatus)
|
||
}}
|
||
totalRecords={total}
|
||
recordsPerPage={pageSize}
|
||
page={pageNumber}
|
||
onPageChange={(p) => setPageNumber(p)}
|
||
onRecordsPerPageChange={(s) => {
|
||
setPageSize(s)
|
||
setPageNumber(1)
|
||
}}
|
||
selectedRecords={selectedRecords}
|
||
onSelectedRecordsChange={setSelectedRecords}
|
||
recordsPerPageOptions={recordsPerPageOptions}
|
||
renderPagination={({ Controls }) => (
|
||
<Group
|
||
justify="space-between"
|
||
align="center"
|
||
w="100%"
|
||
gap="sm"
|
||
wrap="wrap">
|
||
<Group gap="xs" align="center" wrap="wrap">
|
||
<Controls.Text />
|
||
<Controls.PageSizeSelector />
|
||
<Button
|
||
variant="light"
|
||
color="gray"
|
||
size="xs"
|
||
onClick={openCustomPageSizeModal}>
|
||
自定义每页条数
|
||
</Button>
|
||
</Group>
|
||
<Controls.Pagination />
|
||
</Group>
|
||
)}
|
||
/>
|
||
</Stack>
|
||
</SettingContentPanel>
|
||
</Stack>
|
||
|
||
<Modal
|
||
opened={customPageSizeModalOpened}
|
||
onClose={closeCustomPageSizeModal}
|
||
title="添加每页条数"
|
||
centered>
|
||
<Stack gap="sm">
|
||
<TextInput
|
||
data-autofocus
|
||
label="每页条数"
|
||
placeholder="请输入大于 0 的整数,例如 99"
|
||
value={customPageSizeInput}
|
||
onChange={(event) =>
|
||
setCustomPageSizeInput(event.currentTarget.value)
|
||
}
|
||
onKeyDown={handleCustomPageSizeEnter}
|
||
/>
|
||
<Group justify="flex-end">
|
||
<Button variant="default" onClick={closeCustomPageSizeModal}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleAddCustomPageSize}>添加</Button>
|
||
</Group>
|
||
</Stack>
|
||
</Modal>
|
||
|
||
<WorkflowModal
|
||
opened={workflowTaskId !== null}
|
||
taskId={workflowTaskId}
|
||
onCloseAction={() => setWorkflowTaskId(null)}
|
||
/>
|
||
<DispatchModal
|
||
opened={dispatchRecordModal.open}
|
||
dispatchData={dispatchRecordModal.dispatchData}
|
||
task_ids={dispatchRecordModal.task_ids}
|
||
userOptions={userOptions}
|
||
onCloseAction={(refresh) => {
|
||
setDispatchModalConfig({
|
||
open: false,
|
||
task_ids: [],
|
||
})
|
||
if (refresh) setQueryTrigger((v) => v + 1)
|
||
}}
|
||
/>
|
||
<ReleaseModal
|
||
opened={releaseRecord !== null}
|
||
record={releaseRecord}
|
||
onCloseAction={(refresh) => {
|
||
setReleaseRecord(null)
|
||
if (refresh) setQueryTrigger((v) => v + 1)
|
||
}}
|
||
/>
|
||
</>
|
||
)
|
||
}
|