Files
labelimage/app/project/detail/TaskTableContainer.tsx
2026-04-11 17:37:36 +08:00

1132 lines
35 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 { 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 },
]
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 [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)
} 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 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] ?? "-",
})
const cols: DataTableColumn<Task.DataProps>[] = [
{
accessor: "id",
title: "任务ID",
width: 90,
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: 110,
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, 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)
}}
/>
</>
)
}