Files
labelimage/app/project/detail/TaskTableContainer.tsx
2026-04-03 13:37:37 +08:00

1030 lines
31 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 {
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<any, any>
labelEnum: Map<any, any>
review1Enum: Map<any, any>
review2Enum: Map<any, any>
}
}) {
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<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 [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))
}
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: <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 getTime = (time: number) => {
return dayjs.duration(time, "seconds").format("HH:mm:ss")
}
const columns = useMemo(() => {
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 ?? "-",
},
{
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) => (
<Flex gap={6} justify="space-around" wrap="nowrap">
<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>
<Button
px={0}
variant="transparent"
onClick={() => setWorkflowTaskId(record.id)}>
</Button>
</Flex>
),
},
]
return cols
}, [info, projectId, router, setBackProps, type])
return (
<>
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
<Paper withBorder p="sm" radius="sm">
<Group justify="space-between" wrap="wrap" gap="sm">
<Group gap="xs">
<Text fw={700}></Text>
<Button
size="xs"
variant="transparent"
rightSection={
searchExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)
}
onClick={() => setSearchExpanded((v) => !v)}>
{searchExpanded ? "收起筛选" : "展开筛选"}
</Button>
</Group>
<Group gap="xs">
<Button
size="xs"
radius={"xs"}
variant="default"
leftSection={<IconRefresh size={16} />}
onClick={() => {
const next = createInitialFilters()
setFilters(next)
setForm(next)
setPageNumber(1)
}}>
</Button>
<Button
size="xs"
radius={"xs"}
leftSection={<IconSearch size={16} />}
onClick={() => {
setPageNumber(1)
setForm({ ...filters })
}}>
</Button>
</Group>
</Group>
<Collapse
in={searchExpanded}
transitionDuration={250}
transitionTimingFunction="ease"
animateOpacity>
<Stack gap="sm" mt="sm">
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
<TextInput
size="xs"
radius="xs"
label="任务ID"
value={filters.search_id}
onChange={(e) =>
setFilters((s) => ({
...s,
search_id: e.target.value,
}))
}
/>
<Select
size="xs"
radius="xs"
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
size="xs"
radius="xs"
label="是否返工"
data={[
{ label: "全部", value: "" },
{ label: "是", value: "1" },
{ label: "否", value: "0" },
]}
value={filters.rejected}
onChange={(v) =>
setFilters((s) => ({ ...s, rejected: v ?? "" }))
}
allowDeselect={false}
/>
<MultiSelect
label="当前负责人"
size="xs"
radius="xs"
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="标注员"
size="xs"
radius="xs"
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="审核员"
size="xs"
radius="xs"
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="复审员"
size="xs"
radius="xs"
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 }))
}
/>
</SimpleGrid>
</Stack>
</Collapse>
</Paper>
<Paper
withBorder
p="md"
radius="sm"
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
}}>
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
<Flex justify={"space-between"} align="center" w="100%">
<Flex gap={12}>
<Button
size="xs"
radius="xs"
variant="outline"
disabled={!selectedRecords.length}
onClick={handleExport}>
</Button>
<Button
size="xs"
radius="xs"
variant="outline"
disabled={!selectedRecords.length}
onClick={clickDispatchTask}>
</Button>
<Button
size="xs"
radius="xs"
variant="outline"
disabled={!selectedRecords.length}
onClick={clickReleaseTask}>
</Button>
</Flex>
<ActionIcon onClick={load} variant="transparent">
<IconRefresh size={16} />
</ActionIcon>
</Flex>
<DataTable<Task.DataProps>
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="当前页数"
/>
</Stack>
</Paper>
</Stack>
<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)
}}
/>
</>
)
}