Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!6
This commit is contained in:
@@ -9,12 +9,9 @@ import {
|
|||||||
} from "@/components/label/store/auth"
|
} from "@/components/label/store/auth"
|
||||||
import { OwnTaskStatusOpts } from "@/components/label/utils/constants"
|
import { OwnTaskStatusOpts } from "@/components/label/utils/constants"
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Button,
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -28,9 +25,17 @@ import {
|
|||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
SettingContentPanel,
|
||||||
|
SettingDataTable,
|
||||||
|
SettingFilterActions,
|
||||||
|
SettingFilterPanel,
|
||||||
|
SettingHeaderActions,
|
||||||
|
SettingListHeader,
|
||||||
|
} from "@/components/setting/PageSurface"
|
||||||
import TaskStatusTag from "./components/TaskStatusTag"
|
import TaskStatusTag from "./components/TaskStatusTag"
|
||||||
|
|
||||||
function createInitialFilters() {
|
function createInitialFilters() {
|
||||||
@@ -109,7 +114,6 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
}, [
|
}, [
|
||||||
,
|
|
||||||
form.search_id,
|
form.search_id,
|
||||||
form.search_status,
|
form.search_status,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
@@ -173,6 +177,24 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
const cols: DataTableColumn<Task.DataProps>[] = [
|
const cols: DataTableColumn<Task.DataProps>[] = [
|
||||||
{
|
{
|
||||||
@@ -267,12 +289,11 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Paper withBorder p="sm" radius="sm">
|
<SettingFilterPanel>
|
||||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Text fw={700}>我的任务</Text>
|
<Text fw={700}>筛选条件</Text>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
rightSection={
|
rightSection={
|
||||||
searchExpanded ? (
|
searchExpanded ? (
|
||||||
@@ -285,43 +306,20 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
{searchExpanded ? "收起筛选" : "展开筛选"}
|
{searchExpanded ? "收起筛选" : "展开筛选"}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</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>
|
</Group>
|
||||||
<Collapse
|
<Collapse
|
||||||
in={searchExpanded}
|
in={searchExpanded}
|
||||||
transitionDuration={250}
|
transitionDuration={250}
|
||||||
transitionTimingFunction="ease"
|
transitionTimingFunction="ease"
|
||||||
animateOpacity>
|
animateOpacity>
|
||||||
<Stack gap="sm" mt="sm">
|
<Stack gap="md" mt="md">
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
|
<SimpleGrid
|
||||||
|
cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
|
||||||
|
spacing="md"
|
||||||
|
verticalSpacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
label="任务ID"
|
label="任务ID"
|
||||||
|
placeholder="请输入任务 ID"
|
||||||
value={filters.search_id}
|
value={filters.search_id}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFilters((s) => ({
|
setFilters((s) => ({
|
||||||
@@ -332,8 +330,6 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
style={{ width: 160 }}
|
style={{ width: 160 }}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
label="状态"
|
label="状态"
|
||||||
data={Object.entries(OwnTaskStatusOpts).map(
|
data={Object.entries(OwnTaskStatusOpts).map(
|
||||||
([value, label]) => ({
|
([value, label]) => ({
|
||||||
@@ -346,60 +342,70 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
setFilters((s) => ({ ...s, search_status: v ?? "0" }))
|
setFilters((s) => ({ ...s, search_status: v ?? "0" }))
|
||||||
}
|
}
|
||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
style={{ width: 160 }}
|
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Paper>
|
<SettingFilterActions>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</SettingFilterActions>
|
||||||
|
</SettingFilterPanel>
|
||||||
|
|
||||||
<Paper
|
<SettingContentPanel
|
||||||
withBorder
|
|
||||||
p="md"
|
|
||||||
radius="sm"
|
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}>
|
}}>
|
||||||
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
<Stack gap="sm" align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Flex justify={"space-between"} align="center" w="100%">
|
<SettingListHeader
|
||||||
<Flex gap={12}>
|
title="我的任务"
|
||||||
|
count={`${total} 条`}
|
||||||
|
actions={
|
||||||
|
<SettingHeaderActions>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={!isLabel}
|
disabled={!isLabel}
|
||||||
onClick={() => handleAcquireTask(1)}>
|
onClick={() => handleAcquireTask(1)}>
|
||||||
领取标注
|
领取标注
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={!isReview1}
|
disabled={!isReview1}
|
||||||
onClick={() => handleAcquireTask(2)}>
|
onClick={() => handleAcquireTask(2)}>
|
||||||
领取审核
|
领取审核
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={!isReview2}
|
disabled={!isReview2}
|
||||||
onClick={() => handleAcquireTask(3)}>
|
onClick={() => handleAcquireTask(3)}>
|
||||||
领取复审
|
领取复审
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</SettingHeaderActions>
|
||||||
<ActionIcon onClick={load} variant="transparent">
|
}
|
||||||
<IconRefresh size={16} />
|
/>
|
||||||
</ActionIcon>
|
<SettingDataTable<Task.DataProps>
|
||||||
</Flex>
|
|
||||||
<DataTable<Task.DataProps>
|
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%", flex: 1 }}
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
pinFirstColumn
|
pinFirstColumn
|
||||||
pinLastColumn
|
pinLastColumn
|
||||||
scrollAreaProps={{ type: "auto" }}
|
scrollAreaProps={{ type: "auto" }}
|
||||||
@@ -415,11 +421,9 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
setPageNumber(1)
|
setPageNumber(1)
|
||||||
}}
|
}}
|
||||||
recordsPerPageOptions={[10, 15, 20]}
|
recordsPerPageOptions={[10, 15, 20]}
|
||||||
noRecordsText="暂无数据"
|
|
||||||
recordsPerPageLabel="当前页数"
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</SettingContentPanel>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ import {
|
|||||||
TaskStatusOpts,
|
TaskStatusOpts,
|
||||||
} from "@/components/label/utils/constants"
|
} from "@/components/label/utils/constants"
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
SettingContentPanel,
|
||||||
|
SettingDataTable,
|
||||||
|
SettingFilterActions,
|
||||||
|
SettingFilterPanel,
|
||||||
|
SettingHeaderActions,
|
||||||
|
SettingListHeader,
|
||||||
|
} from "@/components/setting/PageSurface"
|
||||||
|
import {
|
||||||
Button,
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
Paper,
|
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -35,7 +41,7 @@ import {
|
|||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import duration from "dayjs/plugin/duration"
|
import duration from "dayjs/plugin/duration"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import * as XLSX from "xlsx-js-style"
|
import * as XLSX from "xlsx-js-style"
|
||||||
@@ -125,7 +131,6 @@ export default function TaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const { info, userEnums } = props
|
const { info, userEnums } = props
|
||||||
false && console.log(info)
|
|
||||||
|
|
||||||
const { user_id } = usePermissionStore()
|
const { user_id } = usePermissionStore()
|
||||||
|
|
||||||
@@ -444,6 +449,24 @@ export default function TaskTableContainer(props: {
|
|||||||
} catch {}
|
} 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) => {
|
const getTime = (time: number) => {
|
||||||
return dayjs.duration(time, "seconds").format("HH:mm:ss")
|
return dayjs.duration(time, "seconds").format("HH:mm:ss")
|
||||||
}
|
}
|
||||||
@@ -775,12 +798,11 @@ export default function TaskTableContainer(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Paper withBorder p="sm" radius="sm">
|
<SettingFilterPanel>
|
||||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Text fw={700}>任务列表</Text>
|
<Text fw={700}>筛选条件</Text>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
rightSection={
|
rightSection={
|
||||||
searchExpanded ? (
|
searchExpanded ? (
|
||||||
@@ -793,43 +815,20 @@ export default function TaskTableContainer(props: {
|
|||||||
{searchExpanded ? "收起筛选" : "展开筛选"}
|
{searchExpanded ? "收起筛选" : "展开筛选"}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</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>
|
</Group>
|
||||||
<Collapse
|
<Collapse
|
||||||
in={searchExpanded}
|
in={searchExpanded}
|
||||||
transitionDuration={250}
|
transitionDuration={250}
|
||||||
transitionTimingFunction="ease"
|
transitionTimingFunction="ease"
|
||||||
animateOpacity>
|
animateOpacity>
|
||||||
<Stack gap="sm" mt="sm">
|
<Stack gap="md" mt="md">
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
|
<SimpleGrid
|
||||||
|
cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
|
||||||
|
spacing="md"
|
||||||
|
verticalSpacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
label="任务ID"
|
label="任务ID"
|
||||||
|
placeholder="请输入任务 ID"
|
||||||
value={filters.search_id}
|
value={filters.search_id}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFilters((s) => ({
|
setFilters((s) => ({
|
||||||
@@ -839,8 +838,6 @@ export default function TaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
label="任务状态"
|
label="任务状态"
|
||||||
data={Object.entries(TaskStatusOpts).map(
|
data={Object.entries(TaskStatusOpts).map(
|
||||||
([value, label]) => ({
|
([value, label]) => ({
|
||||||
@@ -855,8 +852,6 @@ export default function TaskTableContainer(props: {
|
|||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
label="是否返工"
|
label="是否返工"
|
||||||
data={[
|
data={[
|
||||||
{ label: "全部", value: "" },
|
{ label: "全部", value: "" },
|
||||||
@@ -871,8 +866,6 @@ export default function TaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="当前负责人"
|
label="当前负责人"
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
data={Array.from(userEnums.allEnum.entries()).map(
|
data={Array.from(userEnums.allEnum.entries()).map(
|
||||||
([x, y]) => ({ label: y, value: x.toString() })
|
([x, y]) => ({ label: y, value: x.toString() })
|
||||||
)}
|
)}
|
||||||
@@ -885,8 +878,6 @@ export default function TaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="标注员"
|
label="标注员"
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
data={Array.from(userEnums.labelEnum.entries()).map(
|
data={Array.from(userEnums.labelEnum.entries()).map(
|
||||||
([x, y]) => ({ label: y, value: x.toString() })
|
([x, y]) => ({ label: y, value: x.toString() })
|
||||||
)}
|
)}
|
||||||
@@ -897,8 +888,6 @@ export default function TaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="审核员"
|
label="审核员"
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
data={Array.from(userEnums.review1Enum.entries()).map(
|
data={Array.from(userEnums.review1Enum.entries()).map(
|
||||||
([x, y]) => ({ label: y, value: x.toString() })
|
([x, y]) => ({ label: y, value: x.toString() })
|
||||||
)}
|
)}
|
||||||
@@ -911,8 +900,6 @@ export default function TaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="复审员"
|
label="复审员"
|
||||||
size="xs"
|
|
||||||
radius="xs"
|
|
||||||
data={Array.from(userEnums.review2Enum.entries()).map(
|
data={Array.from(userEnums.review2Enum.entries()).map(
|
||||||
([x, y]) => ({ label: y, value: x.toString() })
|
([x, y]) => ({ label: y, value: x.toString() })
|
||||||
)}
|
)}
|
||||||
@@ -926,54 +913,69 @@ export default function TaskTableContainer(props: {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Paper>
|
<SettingFilterActions>
|
||||||
<Paper
|
<Button
|
||||||
withBorder
|
variant="default"
|
||||||
p="md"
|
leftSection={<IconRefresh size={16} />}
|
||||||
radius="sm"
|
onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</SettingFilterActions>
|
||||||
|
</SettingFilterPanel>
|
||||||
|
<SettingContentPanel
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}>
|
}}>
|
||||||
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
<Stack gap="sm" align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Flex justify={"space-between"} align="center" w="100%">
|
<SettingListHeader
|
||||||
<Flex gap={12}>
|
title="任务列表"
|
||||||
|
count={
|
||||||
|
selectedRecords.length
|
||||||
|
? `${total} 条 / 已选 ${selectedRecords.length}`
|
||||||
|
: `${total} 条`
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<SettingHeaderActions>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
variant="default"
|
||||||
radius="xs"
|
leftSection={<IconRefresh size={16} />}
|
||||||
variant="outline"
|
onClick={handleRefresh}
|
||||||
|
loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
disabled={!selectedRecords.length}
|
disabled={!selectedRecords.length}
|
||||||
onClick={handleExport}>
|
onClick={handleExport}>
|
||||||
下载选中项
|
下载选中项
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
variant="default"
|
||||||
radius="xs"
|
|
||||||
variant="outline"
|
|
||||||
disabled={!selectedRecords.length}
|
disabled={!selectedRecords.length}
|
||||||
onClick={clickDispatchTask}>
|
onClick={clickDispatchTask}>
|
||||||
调度选中项
|
调度选中项
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
variant="default"
|
||||||
radius="xs"
|
|
||||||
variant="outline"
|
|
||||||
disabled={!selectedRecords.length}
|
disabled={!selectedRecords.length}
|
||||||
onClick={clickReleaseTask}>
|
onClick={clickReleaseTask}>
|
||||||
释放选中项
|
释放选中项
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</SettingHeaderActions>
|
||||||
<ActionIcon onClick={load} variant="transparent">
|
}
|
||||||
<IconRefresh size={16} />
|
/>
|
||||||
</ActionIcon>
|
<SettingDataTable<Task.DataProps>
|
||||||
</Flex>
|
|
||||||
<DataTable<Task.DataProps>
|
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%", flex: 1 }}
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
pinFirstColumn
|
pinFirstColumn
|
||||||
pinLastColumn
|
pinLastColumn
|
||||||
scrollAreaProps={{ type: "auto" }}
|
scrollAreaProps={{ type: "auto" }}
|
||||||
@@ -991,11 +993,9 @@ export default function TaskTableContainer(props: {
|
|||||||
selectedRecords={selectedRecords}
|
selectedRecords={selectedRecords}
|
||||||
onSelectedRecordsChange={setSelectedRecords}
|
onSelectedRecordsChange={setSelectedRecords}
|
||||||
recordsPerPageOptions={[10, 15, 20]}
|
recordsPerPageOptions={[10, 15, 20]}
|
||||||
noRecordsText="暂无数据"
|
|
||||||
recordsPerPageLabel="当前页数"
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</SettingContentPanel>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<WorkflowModal
|
<WorkflowModal
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
selectModeMap,
|
selectModeMap,
|
||||||
staticsColor,
|
staticsColor,
|
||||||
} from "@/components/label/utils/constants"
|
} from "@/components/label/utils/constants"
|
||||||
|
import { SettingDataTable } from "@/components/setting/PageSurface"
|
||||||
import { Badge, Button, Group, Modal, Paper, Stack, Text } from "@mantine/core"
|
import { Badge, Button, Group, Modal, Paper, Stack, Text } from "@mantine/core"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
type DetailRow = ProjectDetail.SubAttributesDescribe
|
type DetailRow = ProjectDetail.SubAttributesDescribe
|
||||||
@@ -188,9 +189,7 @@ export default function LabelSchemeContainer(props: {
|
|||||||
}}>
|
}}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Text fw={700}>标注方案</Text>
|
<Text fw={700}>标注方案</Text>
|
||||||
<DataTable<ProjectDetail.LabelSchemaList>
|
<SettingDataTable<ProjectDetail.LabelSchemaList>
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
idAccessor="category_id"
|
idAccessor="category_id"
|
||||||
records={schemas}
|
records={schemas}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -209,9 +208,7 @@ export default function LabelSchemeContainer(props: {
|
|||||||
title="子属性描述"
|
title="子属性描述"
|
||||||
centered
|
centered
|
||||||
size="70%">
|
size="70%">
|
||||||
<DataTable<DetailRow>
|
<SettingDataTable<DetailRow>
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
records={detailRows}
|
records={detailRows}
|
||||||
columns={detailColumns}
|
columns={detailColumns}
|
||||||
noRecordsText="暂无数据"
|
noRecordsText="暂无数据"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { projectConfig } from "@/components/label/api/project"
|
import { projectConfig } from "@/components/label/api/project"
|
||||||
import { ProjectDetail } from "@/components/label/api/project/typing"
|
import { ProjectDetail } from "@/components/label/api/project/typing"
|
||||||
import { usePermissionStore } from "@/components/label/store/auth"
|
import { usePermissionStore } from "@/components/label/store/auth"
|
||||||
|
import { SettingDataTable } from "@/components/setting/PageSurface"
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -18,7 +19,7 @@ import { modals } from "@mantine/modals"
|
|||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import { IconPlus, IconTrash } from "@tabler/icons-react"
|
import { IconPlus, IconTrash } from "@tabler/icons-react"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
|
|
||||||
type RulePayload = {
|
type RulePayload = {
|
||||||
@@ -221,9 +222,7 @@ export default function NoteRulesTable(props: {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<DataTable<RuleRow>
|
<SettingDataTable<RuleRow>
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
records={rows}
|
records={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
noRecordsText="暂无规则"
|
noRecordsText="暂无规则"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { projectConfig } from "@/components/label/api/project"
|
import { projectConfig } from "@/components/label/api/project"
|
||||||
import { Project, ProjectDetail } from "@/components/label/api/project/typing"
|
import { Project, ProjectDetail } from "@/components/label/api/project/typing"
|
||||||
|
import { SettingDataTable } from "@/components/setting/PageSurface"
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -22,7 +23,7 @@ import {
|
|||||||
import { modals } from "@mantine/modals"
|
import { modals } from "@mantine/modals"
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import { IconEdit, IconPlus, IconX } from "@tabler/icons-react"
|
import { IconEdit, IconPlus, IconX } from "@tabler/icons-react"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { ReactNode, useEffect, useMemo, useState } from "react"
|
import { ReactNode, useEffect, useMemo, useState } from "react"
|
||||||
import StageUserModal from "./StageUserModal"
|
import StageUserModal from "./StageUserModal"
|
||||||
import {
|
import {
|
||||||
@@ -1429,9 +1430,7 @@ export default function TaskStageTabsContainer(props: {
|
|||||||
批量操作
|
批量操作
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<DataTable<StageUserRow>
|
<SettingDataTable<StageUserRow>
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
records={rows}
|
records={rows}
|
||||||
columns={userColumns(stage)}
|
columns={userColumns(stage)}
|
||||||
noRecordsText="暂无人员"
|
noRecordsText="暂无人员"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { getTaskWorkFlow } from "@/components/label/api/task"
|
import { getTaskWorkFlow } from "@/components/label/api/task"
|
||||||
|
import { SettingDataTable } from "@/components/setting/PageSurface"
|
||||||
import { Button, Group, Modal, ScrollArea, Stack, Text } from "@mantine/core"
|
import { Button, Group, Modal, ScrollArea, Stack, Text } from "@mantine/core"
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn } from "mantine-datatable"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import TaskStatusTag from "./TaskStatusTag"
|
import TaskStatusTag from "./TaskStatusTag"
|
||||||
|
|
||||||
@@ -139,15 +140,13 @@ export default function WorkflowModal(props: {
|
|||||||
size={720}>
|
size={720}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<ScrollArea h={360} type="auto">
|
<ScrollArea h={360} type="auto">
|
||||||
<DataTable<WorkflowRow>
|
<SettingDataTable<WorkflowRow>
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
|
||||||
pinLastColumn
|
pinLastColumn
|
||||||
|
fetching={loading}
|
||||||
idAccessor="__rowId"
|
idAccessor="__rowId"
|
||||||
records={rows}
|
records={rows}
|
||||||
width={Math.max(columns.length * 140, 640)}
|
width={Math.max(columns.length * 140, 640)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
noRecordsText={loading ? "加载中..." : "暂无数据"}
|
|
||||||
minHeight={220}
|
minHeight={220}
|
||||||
/>
|
/>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ import {
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from "@mantine/core"
|
} from "@mantine/core"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
SettingFilterPanel,
|
||||||
|
SettingPage,
|
||||||
|
} from "@/components/setting/PageSurface"
|
||||||
import InfoSettingContainer from "./InfoSettingContainer"
|
import InfoSettingContainer from "./InfoSettingContainer"
|
||||||
import OwnTaskTableContainer from "./OwnTaskTableContainer"
|
import OwnTaskTableContainer from "./OwnTaskTableContainer"
|
||||||
import TaskBoardContainer from "./TaskBoardContainer"
|
import TaskBoardContainer from "./TaskBoardContainer"
|
||||||
@@ -193,15 +196,15 @@ export default function CollectionDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
<SettingPage>
|
||||||
<Paper withBorder p="md" radius="sm">
|
<SettingFilterPanel>
|
||||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||||
<Flex gap="sm" w="100%" justify="space-between">
|
<Flex gap="sm" w="100%" justify="space-between">
|
||||||
<Breadcrumbs separator=">">{breadcrumbsItems}</Breadcrumbs>
|
<Breadcrumbs separator=">">{breadcrumbsItems}</Breadcrumbs>
|
||||||
{DetailMenuItems()}
|
{DetailMenuItems()}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
</SettingFilterPanel>
|
||||||
|
|
||||||
<Stack style={{ flex: 1, minHeight: 0 }} gap="md">
|
<Stack style={{ flex: 1, minHeight: 0 }} gap="md">
|
||||||
{selectKey === "own" ? (
|
{selectKey === "own" ? (
|
||||||
@@ -219,6 +222,6 @@ export default function CollectionDetailPage() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</SettingPage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
77
components/label/components/AssistShapeComponent.tsx
Normal file
77
components/label/components/AssistShapeComponent.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client"
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
interface AssistShapeComponentProps {
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssistShapeComponent = (
|
||||||
|
props: AssistShapeComponentProps,
|
||||||
|
ref: React.Ref<unknown> | undefined
|
||||||
|
) => {
|
||||||
|
const { size } = props
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
|
const drawAssistShape = useCallback(
|
||||||
|
(centerX: number, centerY: number) => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas?.getContext("2d")
|
||||||
|
if (!canvas || !ctx) return
|
||||||
|
|
||||||
|
const radius = Math.max(1, size)
|
||||||
|
const side = radius * 2
|
||||||
|
const left = centerX - radius
|
||||||
|
const top = centerY - radius
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = "#EF4444"
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.setLineDash([6, 4])
|
||||||
|
ctx.strokeRect(left, top, side, side)
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
},
|
||||||
|
[size]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
canvas.width = window.innerWidth
|
||||||
|
canvas.height = window.innerHeight
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
drawAssistShape(rect.width / 2, rect.height / 2)
|
||||||
|
}, [drawAssistShape])
|
||||||
|
|
||||||
|
const updateAssistShape = (event: { clientX: number; clientY: number }) => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
const centerX = event.clientX - rect.left
|
||||||
|
const centerY = event.clientY - rect.top
|
||||||
|
drawAssistShape(centerX, centerY)
|
||||||
|
}
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({ updateAssistShape }))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 52,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(AssistShapeComponent)
|
||||||
@@ -21,6 +21,7 @@ const CrosshairComponent = (
|
|||||||
const { scale } = useTopToolsStore()
|
const { scale } = useTopToolsStore()
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
const getFixed = (sparsity: number) => {
|
const getFixed = (sparsity: number) => {
|
||||||
const pointIdx = String(sparsity).indexOf(".")
|
const pointIdx = String(sparsity).indexOf(".")
|
||||||
@@ -76,6 +77,7 @@ const CrosshairComponent = (
|
|||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.setLineDash([])
|
ctx.setLineDash([])
|
||||||
ctx.strokeStyle = "#EF4444"
|
ctx.strokeStyle = "#EF4444"
|
||||||
|
ctx.fillStyle = "#EF4444"
|
||||||
let carveLineWidth = 1
|
let carveLineWidth = 1
|
||||||
ctx.lineWidth = carveLineWidth
|
ctx.lineWidth = carveLineWidth
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ const CrosshairComponent = (
|
|||||||
sparsity
|
sparsity
|
||||||
).toFixed(fixed)
|
).toFixed(fixed)
|
||||||
const textWidth = ctx.measureText(text).width
|
const textWidth = ctx.measureText(text).width
|
||||||
|
ctx.fillStyle = "#EF4444"
|
||||||
ctx.fillText(text, indexX - textWidth / 2, centerY - tickLength)
|
ctx.fillText(text, indexX - textWidth / 2, centerY - tickLength)
|
||||||
} else {
|
} else {
|
||||||
ctx.moveTo(indexX, centerY - tickLength / 2 - carveLineWidth)
|
ctx.moveTo(indexX, centerY - tickLength / 2 - carveLineWidth)
|
||||||
@@ -143,7 +146,11 @@ const CrosshairComponent = (
|
|||||||
canvas.width = window.innerWidth
|
canvas.width = window.innerWidth
|
||||||
canvas.height = window.innerHeight
|
canvas.height = window.innerHeight
|
||||||
const rect = canvas.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
drawCrosshair(rect.width / 2, rect.height / 2)
|
const center = lastPointerRef.current || {
|
||||||
|
x: rect.width / 2,
|
||||||
|
y: rect.height / 2,
|
||||||
|
}
|
||||||
|
drawCrosshair(center.x, center.y)
|
||||||
}
|
}
|
||||||
}, [drawCrosshair])
|
}, [drawCrosshair])
|
||||||
|
|
||||||
@@ -153,6 +160,7 @@ const CrosshairComponent = (
|
|||||||
const rect = canvas.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const centerX = event.clientX - rect.left
|
const centerX = event.clientX - rect.left
|
||||||
const centerY = event.clientY - rect.top
|
const centerY = event.clientY - rect.top
|
||||||
|
lastPointerRef.current = { x: centerX, y: centerY }
|
||||||
drawCrosshair(centerX, centerY)
|
drawCrosshair(centerX, centerY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { checkCommentsIsSame } from "../util"
|
|||||||
import { safeClone } from "../utils/clone"
|
import { safeClone } from "../utils/clone"
|
||||||
import { labelTypeMap } from "../utils/constants"
|
import { labelTypeMap } from "../utils/constants"
|
||||||
import { adjustPoints } from "../utils/paperjs"
|
import { adjustPoints } from "../utils/paperjs"
|
||||||
|
import AssistShapeComponent from "./AssistShapeComponent"
|
||||||
import CrosshairComponent from "./CrosshairComponent"
|
import CrosshairComponent from "./CrosshairComponent"
|
||||||
import { renderOperationIcon } from "./RightObjectTools"
|
import { renderOperationIcon } from "./RightObjectTools"
|
||||||
|
|
||||||
@@ -222,6 +223,8 @@ const PaperContainer = (
|
|||||||
objectOperations,
|
objectOperations,
|
||||||
imageFilter,
|
imageFilter,
|
||||||
crosshairStatus,
|
crosshairStatus,
|
||||||
|
assistToolEnabled,
|
||||||
|
assistToolSize,
|
||||||
showTags,
|
showTags,
|
||||||
showTagsConfigs,
|
showTagsConfigs,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
@@ -2363,10 +2366,14 @@ const PaperContainer = (
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={contextMenuRef}
|
ref={contextMenuRef}
|
||||||
|
data-label-context-menu="true"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
padding="xs"
|
padding="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
withBorder
|
withBorder
|
||||||
|
onWheel={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
@@ -2667,9 +2674,11 @@ const PaperContainer = (
|
|||||||
}, [activeImage])
|
}, [activeImage])
|
||||||
|
|
||||||
const crosshairComponentRef = useRef<any>(null)
|
const crosshairComponentRef = useRef<any>(null)
|
||||||
|
const assistShapeComponentRef = useRef<any>(null)
|
||||||
|
|
||||||
const handleCrosshairMove = (event: { clientX: number; clientY: number }) => {
|
const handleCrosshairMove = (event: { clientX: number; clientY: number }) => {
|
||||||
crosshairComponentRef.current?.updateCrosshair(event)
|
crosshairComponentRef.current?.updateCrosshair(event)
|
||||||
|
assistShapeComponentRef.current?.updateAssistShape(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -2699,6 +2708,12 @@ const PaperContainer = (
|
|||||||
ref={crosshairComponentRef}
|
ref={crosshairComponentRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{assistToolEnabled && (
|
||||||
|
<AssistShapeComponent
|
||||||
|
size={assistToolSize}
|
||||||
|
ref={assistShapeComponentRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{imgSize?.clientWidth && imgSize?.clientHeight && (
|
{imgSize?.clientWidth && imgSize?.clientHeight && (
|
||||||
<canvas
|
<canvas
|
||||||
id="paper-canvas"
|
id="paper-canvas"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore"
|
|||||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
import { getSubAttrStatus } from "../util"
|
import { getSubAttrStatus } from "../util"
|
||||||
|
import { toggleObjectHideByShortcut } from "../utils/objectVisibility"
|
||||||
import { labelTypeMap } from "../utils/constants"
|
import { labelTypeMap } from "../utils/constants"
|
||||||
|
|
||||||
interface RightObjectToolsComponentProps {
|
interface RightObjectToolsComponentProps {
|
||||||
@@ -379,33 +380,6 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTagVisibility = (tagCategory?: string) => {
|
|
||||||
const { showTags, showTagsConfigs } = useTopToolsStore.getState()
|
|
||||||
if (!showTags) return false
|
|
||||||
if (tagCategory === "mask") return showTagsConfigs.includes("外框")
|
|
||||||
if (tagCategory === "point_text") return showTagsConfigs.includes("点ID")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncObjectItemsVisibility = useCallback(
|
|
||||||
(objectId: number, hide: boolean) => {
|
|
||||||
const sameIdItems = getItemsById(objectId)
|
|
||||||
if (!sameIdItems?.length) return
|
|
||||||
sameIdItems.forEach((item) => {
|
|
||||||
if (hide) {
|
|
||||||
item.visible = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.data?.type === "tag") {
|
|
||||||
item.visible = getTagVisibility(item.data?.tag_category)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item.visible = true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[getItemsById]
|
|
||||||
)
|
|
||||||
|
|
||||||
const operationHide = useCallback(
|
const operationHide = useCallback(
|
||||||
(operationId: string, hide: boolean) => {
|
(operationId: string, hide: boolean) => {
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
||||||
@@ -414,51 +388,17 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
.get(activeImage)
|
.get(activeImage)
|
||||||
?.get(operationId)
|
?.get(operationId)
|
||||||
?.map((child) => {
|
?.map((child) => {
|
||||||
updatePathStatus(activeImage + child[0], "HIDE", hide)
|
toggleObjectHideByShortcut(activeImage, operationId, child[0], hide)
|
||||||
syncObjectItemsVisibility(child[0], hide)
|
|
||||||
// 隐藏时清空选中状态
|
|
||||||
if (hide) setSelectedItemFalse(child[0])
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[
|
[activeImage, storeLabel, updateOperationStatus]
|
||||||
activeImage,
|
|
||||||
storeLabel,
|
|
||||||
syncObjectItemsVisibility,
|
|
||||||
updateOperationStatus,
|
|
||||||
updatePathStatus,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const objectHide = useCallback(
|
const objectHide = useCallback(
|
||||||
(operationId: string, objectId: number, hide: boolean) => {
|
(operationId: string, objectId: number, hide: boolean) => {
|
||||||
if (hide) {
|
toggleObjectHideByShortcut(activeImage, operationId, objectId, hide)
|
||||||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
|
||||||
syncObjectItemsVisibility(objectId, hide)
|
|
||||||
// 隐藏时清空选中状态
|
|
||||||
setSelectedItemFalse(objectId)
|
|
||||||
let childrenHideStatus = storeLabel
|
|
||||||
.get(activeImage)
|
|
||||||
?.get(operationId)
|
|
||||||
?.map((child) => {
|
|
||||||
return getItemById(child[0])!.visible
|
|
||||||
})
|
|
||||||
if (childrenHideStatus?.findIndex((bool) => bool === true) === -1) {
|
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
|
||||||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
|
||||||
syncObjectItemsVisibility(objectId, hide)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[activeImage]
|
||||||
activeImage,
|
|
||||||
getItemById,
|
|
||||||
storeLabel,
|
|
||||||
syncObjectItemsVisibility,
|
|
||||||
updateOperationStatus,
|
|
||||||
updatePathStatus,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// const renderObjectName = useCallback((name: string, input: string) => {
|
// const renderObjectName = useCallback((name: string, input: string) => {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
import { useBackUrlStore, usePermissionStore } from "../store/auth"
|
import { useBackUrlStore, usePermissionStore } from "../store/auth"
|
||||||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
import { useDescToolsStore } from "../useDescToolsStore"
|
import { useDescToolsStore } from "../useDescToolsStore"
|
||||||
|
import { useKeyboardStore } from "../useKeyBoardStore"
|
||||||
import { usePaperStore } from "../usePaperStore"
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useRightToolsStore } from "../useRightToolsStore"
|
import { useRightToolsStore } from "../useRightToolsStore"
|
||||||
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
||||||
@@ -213,6 +214,10 @@ const TopTools = (
|
|||||||
setImageFilter,
|
setImageFilter,
|
||||||
crosshairStatus,
|
crosshairStatus,
|
||||||
setCrosshairStatus,
|
setCrosshairStatus,
|
||||||
|
assistToolEnabled,
|
||||||
|
setAssistToolEnabled,
|
||||||
|
assistToolSize,
|
||||||
|
setAssistToolSize,
|
||||||
needBackup,
|
needBackup,
|
||||||
setNeedBackup,
|
setNeedBackup,
|
||||||
checkSize,
|
checkSize,
|
||||||
@@ -1531,6 +1536,7 @@ const TopTools = (
|
|||||||
}, [activeOperation, mode])
|
}, [activeOperation, mode])
|
||||||
|
|
||||||
const operationSchema = getOperationSchema(activeOperation)
|
const operationSchema = getOperationSchema(activeOperation)
|
||||||
|
const drawAction = useKeyboardStore((state) => state.drawAction)
|
||||||
|
|
||||||
const subAttrForm = (
|
const subAttrForm = (
|
||||||
<Box w={256}>
|
<Box w={256}>
|
||||||
@@ -1638,12 +1644,19 @@ const TopTools = (
|
|||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleSave,
|
handleSave,
|
||||||
handleBackup,
|
handleBackup,
|
||||||
|
commitTaskByActionKey,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const renderEditIcon = useMemo(() => {
|
const renderEditIcon = useMemo(() => {
|
||||||
const iconStyle = { width: 20, height: 20 }
|
const iconStyle = { width: 20, height: 20 }
|
||||||
if (mode === "support") return <Paintbrush style={iconStyle} />
|
if (mode === "support") return <Paintbrush style={iconStyle} />
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
|
if (pressA && drawAction === "subtract") {
|
||||||
|
return <CopyMinus style={iconStyle} />
|
||||||
|
}
|
||||||
|
if (pressA && drawAction === "add") {
|
||||||
|
return <CopyPlus style={iconStyle} />
|
||||||
|
}
|
||||||
return pressA ? (
|
return pressA ? (
|
||||||
<CirclePlus style={iconStyle} />
|
<CirclePlus style={iconStyle} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1652,7 +1665,7 @@ const TopTools = (
|
|||||||
} else {
|
} else {
|
||||||
return <MousePointer style={iconStyle} />
|
return <MousePointer style={iconStyle} />
|
||||||
}
|
}
|
||||||
}, [mode, editMode, pressA])
|
}, [mode, editMode, pressA, drawAction])
|
||||||
|
|
||||||
const showTagConfigContainer = useMemo(() => {
|
const showTagConfigContainer = useMemo(() => {
|
||||||
const configs = ["ID", "类别", "外框", "点ID", "子属性", "属性值"]
|
const configs = ["ID", "类别", "外框", "点ID", "子属性", "属性值"]
|
||||||
@@ -2021,6 +2034,36 @@ const TopTools = (
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex justify="space-between" align="center" p={8} gap={4}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
c={assistToolEnabled ? "blue" : "var(--mantine-color-text)"}
|
||||||
|
onClick={() => {
|
||||||
|
setAssistToolEnabled(!assistToolEnabled)
|
||||||
|
}}>
|
||||||
|
<Eclipse size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
<NumberInput
|
||||||
|
value={assistToolSize}
|
||||||
|
w={58}
|
||||||
|
min={1}
|
||||||
|
size="xs"
|
||||||
|
hideControls
|
||||||
|
onChange={(v) => {
|
||||||
|
const next = Number(v)
|
||||||
|
if (Number.isFinite(next) && next >= 1) {
|
||||||
|
setAssistToolSize(next)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
useKeyEventStore.getState().setFocusInput(true)
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
useKeyEventStore.getState().setFocusInput(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
<Flex justify="center" align="center">
|
<Flex justify="center" align="center">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ interface KeyboardStore {
|
|||||||
setCtrl: (v: boolean) => void
|
setCtrl: (v: boolean) => void
|
||||||
shift: boolean
|
shift: boolean
|
||||||
setShift: (v: boolean) => void
|
setShift: (v: boolean) => void
|
||||||
|
drawAction: "none" | "add" | "subtract"
|
||||||
|
setDrawAction: (mode: "none" | "add" | "subtract") => void
|
||||||
copyDataIds: number[] // ctrl + c 复制数据
|
copyDataIds: number[] // ctrl + c 复制数据
|
||||||
setCopyDataIds: (ids: number[]) => void
|
setCopyDataIds: (ids: number[]) => void
|
||||||
}
|
}
|
||||||
@@ -12,8 +14,10 @@ interface KeyboardStore {
|
|||||||
export const useKeyboardStore = create<KeyboardStore>((set) => ({
|
export const useKeyboardStore = create<KeyboardStore>((set) => ({
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
|
drawAction: "none",
|
||||||
copyDataIds: [],
|
copyDataIds: [],
|
||||||
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
||||||
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
||||||
|
setDrawAction: (mode) => set((state) => ({ ...state, drawAction: mode })),
|
||||||
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Comment } from "./api/label/typing"
|
import { Comment } from "./api/label/typing"
|
||||||
import { Project } from "./api/project/typing"
|
import { Project } from "./api/project/typing"
|
||||||
|
import { notifications } from "@mantine/notifications"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { DispatchWithoutAction } from "react"
|
import { DispatchWithoutAction } from "react"
|
||||||
@@ -12,12 +13,19 @@ import {
|
|||||||
useObjectStore,
|
useObjectStore,
|
||||||
} from "./store"
|
} from "./store"
|
||||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||||
|
import { useKeyboardStore } from "./useKeyBoardStore"
|
||||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||||
import { usePaperSupportStore } from "./usePaperSupportStore"
|
import { usePaperSupportStore } from "./usePaperSupportStore"
|
||||||
import { useRightToolsStore } from "./useRightToolsStore"
|
import { useRightToolsStore } from "./useRightToolsStore"
|
||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
|
|
||||||
|
interface ContinuePolygonTarget {
|
||||||
|
imageId: string
|
||||||
|
operationId: string
|
||||||
|
objectId: number
|
||||||
|
}
|
||||||
|
|
||||||
interface PaperState {
|
interface PaperState {
|
||||||
paperScope: paper.PaperScope | null
|
paperScope: paper.PaperScope | null
|
||||||
group: paper.Group | null
|
group: paper.Group | null
|
||||||
@@ -35,6 +43,7 @@ interface PaperState {
|
|||||||
rasterScale: { [x: string]: number }
|
rasterScale: { [x: string]: number }
|
||||||
rasterSize: { [x: string]: [number, number] }
|
rasterSize: { [x: string]: [number, number] }
|
||||||
reciprocalRasterScale: { [x: string]: number }
|
reciprocalRasterScale: { [x: string]: number }
|
||||||
|
continuePolygonTarget: ContinuePolygonTarget | null
|
||||||
loadingData: boolean
|
loadingData: boolean
|
||||||
init: (
|
init: (
|
||||||
initEl: HTMLCanvasElement,
|
initEl: HTMLCanvasElement,
|
||||||
@@ -169,6 +178,7 @@ interface PaperState {
|
|||||||
getAll: () => paper.Path[]
|
getAll: () => paper.Path[]
|
||||||
getItemById: (id: number) => paper.Path | null
|
getItemById: (id: number) => paper.Path | null
|
||||||
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
|
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
|
||||||
|
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
|
||||||
setLoadingData: (flag: boolean) => void
|
setLoadingData: (flag: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +529,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
rasterScale: initialPaperState.rasterScale,
|
rasterScale: initialPaperState.rasterScale,
|
||||||
rasterSize: initialPaperState.rasterSize,
|
rasterSize: initialPaperState.rasterSize,
|
||||||
reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
|
reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
|
||||||
|
continuePolygonTarget: null,
|
||||||
currentMousePosition: [0, 0],
|
currentMousePosition: [0, 0],
|
||||||
loadingData: false,
|
loadingData: false,
|
||||||
init: (
|
init: (
|
||||||
@@ -566,6 +577,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
...state,
|
...state,
|
||||||
loadingData: flag,
|
loadingData: flag,
|
||||||
})),
|
})),
|
||||||
|
setContinuePolygonTarget: (target) =>
|
||||||
|
set((state: PaperState) => ({
|
||||||
|
...state,
|
||||||
|
continuePolygonTarget: target,
|
||||||
|
})),
|
||||||
setCurrentMousePosition: (point: [number, number]) =>
|
setCurrentMousePosition: (point: [number, number]) =>
|
||||||
set((state: PaperState) => ({
|
set((state: PaperState) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -1409,26 +1425,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
})
|
})
|
||||||
// 标识位为true时:选中路径隐藏tags; 没选中显示tags
|
// 标识位为true时:选中路径隐藏tags; 没选中显示tags
|
||||||
const configs = useTopToolsStore.getState().showTagsConfigs
|
const configs = useTopToolsStore.getState().showTagsConfigs
|
||||||
if (useTopToolsStore.getState().showTags)
|
if (useTopToolsStore.getState().showTags) {
|
||||||
if (panMode) {
|
const setTagVisibleByConfig = (tag: any) => {
|
||||||
lastTags.forEach((tag) => {
|
|
||||||
if (tag.data.tag_category === "mask")
|
if (tag.data.tag_category === "mask")
|
||||||
tag.visible = configs.includes("外框")
|
tag.visible = configs.includes("外框")
|
||||||
else if (tag.data.tag_category === "point_text")
|
else if (tag.data.tag_category === "point_text")
|
||||||
tag.visible = configs.includes("点ID")
|
tag.visible = configs.includes("点ID")
|
||||||
else tag.visible = true
|
else tag.visible = true
|
||||||
})
|
}
|
||||||
tags.forEach((tag) => {
|
lastTags.forEach(setTagVisibleByConfig)
|
||||||
tag.visible = false
|
tags.forEach(setTagVisibleByConfig)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
if (tag.data.tag_category === "mask")
|
|
||||||
tag.visible = configs.includes("外框")
|
|
||||||
else if (tag.data.tag_category === "point_text")
|
|
||||||
tag.visible = configs.includes("点ID")
|
|
||||||
else tag.visible = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("鼠标按下 选中路径", activePath, "当前模式", panMode)
|
console.log("鼠标按下 选中路径", activePath, "当前模式", panMode)
|
||||||
@@ -2678,6 +2684,9 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
let polygonTool = paperPolygonTool
|
let polygonTool = paperPolygonTool
|
||||||
let currentMagnetPoint: paper.Point | null = null
|
let currentMagnetPoint: paper.Point | null = null
|
||||||
let draftId: number | null = null
|
let draftId: number | null = null
|
||||||
|
let continueSourceItems: Array<paper.Path | paper.CompoundPath> = []
|
||||||
|
let continueTargetContourIndex: number | null = null
|
||||||
|
let continueTargetContext: ContinuePolygonTarget | null = null
|
||||||
|
|
||||||
const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
|
const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
|
||||||
// 更新选中的点
|
// 更新选中的点
|
||||||
@@ -2723,6 +2732,193 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
currentMagnetPoint = null
|
currentMagnetPoint = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPointFromSegment = (segment: any): paper.Point | null => {
|
||||||
|
if (segment instanceof paper.Point) return segment
|
||||||
|
if (segment instanceof paper.Segment)
|
||||||
|
return new paper.Point(segment.point)
|
||||||
|
if (
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
segment.length >= 2 &&
|
||||||
|
typeof segment[0] === "number" &&
|
||||||
|
typeof segment[1] === "number"
|
||||||
|
) {
|
||||||
|
return new paper.Point(segment[0], segment[1])
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
Array.isArray(segment[0]) &&
|
||||||
|
segment[0].length >= 2 &&
|
||||||
|
typeof segment[0][0] === "number" &&
|
||||||
|
typeof segment[0][1] === "number"
|
||||||
|
) {
|
||||||
|
return new paper.Point(segment[0][0], segment[0][1])
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
segment?.point &&
|
||||||
|
typeof segment.point.x === "number" &&
|
||||||
|
typeof segment.point.y === "number"
|
||||||
|
) {
|
||||||
|
return new paper.Point(segment.point.x, segment.point.y)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
segment?.point &&
|
||||||
|
Array.isArray(segment.point) &&
|
||||||
|
segment.point.length >= 2 &&
|
||||||
|
typeof segment.point[0] === "number" &&
|
||||||
|
typeof segment.point[1] === "number"
|
||||||
|
) {
|
||||||
|
return new paper.Point(segment.point[0], segment.point[1])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearContinueSourceItems = (restoreVisible: boolean) => {
|
||||||
|
continueSourceItems.forEach((item) => {
|
||||||
|
if (restoreVisible) {
|
||||||
|
item.visible = true
|
||||||
|
} else {
|
||||||
|
item.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
continueSourceItems = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearContinuePolygonContext = (restoreVisible: boolean) => {
|
||||||
|
clearContinueSourceItems(restoreVisible)
|
||||||
|
continueTargetContourIndex = null
|
||||||
|
continueTargetContext = null
|
||||||
|
usePaperStore.getState().setContinuePolygonTarget(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startContinuePolygonDraft = () => {
|
||||||
|
const target = usePaperStore.getState().continuePolygonTarget
|
||||||
|
if (!target) return false
|
||||||
|
|
||||||
|
const imageLabel = useLabelStore.getState().label.get(target.imageId)
|
||||||
|
const operationPaths = imageLabel?.get(target.operationId) || []
|
||||||
|
const targetTuple = operationPaths.find(
|
||||||
|
(path) => path[0] === target.objectId
|
||||||
|
)
|
||||||
|
if (!targetTuple) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "未找到可继续标注的对象",
|
||||||
|
})
|
||||||
|
clearContinuePolygonContext(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPoints = safeClone(targetTuple[1] || [])
|
||||||
|
let contourIndex = -1
|
||||||
|
for (let i = nextPoints.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (nextPoints[i]?.length > 3) {
|
||||||
|
contourIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contourIndex === -1) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前对象没有可继续标注的区域",
|
||||||
|
})
|
||||||
|
clearContinuePolygonContext(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const contourSegments = safeClone(nextPoints[contourIndex] || [])
|
||||||
|
const continueSegments = contourSegments.slice().reverse()
|
||||||
|
continueSegments.pop()
|
||||||
|
const contourPoints = continueSegments
|
||||||
|
.map((segment: any) => getPointFromSegment(segment))
|
||||||
|
.filter((point: paper.Point | null): point is paper.Point => !!point)
|
||||||
|
|
||||||
|
if (contourPoints.length < 3) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前区域至少保留3个节点,无法继续撤回",
|
||||||
|
})
|
||||||
|
clearContinuePolygonContext(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
clearContinueSourceItems(true)
|
||||||
|
continueSourceItems = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(target.objectId)
|
||||||
|
.filter((item) => item.data?.type === "polygon")
|
||||||
|
continueSourceItems.forEach((item) => {
|
||||||
|
item.visible = false
|
||||||
|
})
|
||||||
|
|
||||||
|
continueTargetContourIndex = contourIndex
|
||||||
|
continueTargetContext = target
|
||||||
|
points = contourPoints
|
||||||
|
draftId = target.objectId
|
||||||
|
polygon?.remove()
|
||||||
|
polygon = usePaperStore.getState().addPolygon(
|
||||||
|
renderPath,
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
...usePaperStore.getState().toolOption,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageId: target.imageId,
|
||||||
|
operationId: target.operationId,
|
||||||
|
id: target.objectId,
|
||||||
|
type: "polygon",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
handleCircles(polygon)
|
||||||
|
lastPoint = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.group!.localToGlobal(points[points.length - 1])
|
||||||
|
lastTime = Date.now()
|
||||||
|
clearPolygonMagnet()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveContinuePolygonDraft = (draftPath: paper.Path) => {
|
||||||
|
const target =
|
||||||
|
continueTargetContext || usePaperStore.getState().continuePolygonTarget
|
||||||
|
if (!target) return false
|
||||||
|
|
||||||
|
const imageLabel = useLabelStore.getState().label.get(target.imageId)
|
||||||
|
const operationPaths = imageLabel?.get(target.operationId) || []
|
||||||
|
const targetTuple = operationPaths.find(
|
||||||
|
(path) => path[0] === target.objectId
|
||||||
|
)
|
||||||
|
if (!targetTuple) return false
|
||||||
|
|
||||||
|
const pathData = JSON.parse(draftPath.exportJSON({ precision: 20 }))
|
||||||
|
const nextContour = pathData?.[1]?.segments
|
||||||
|
if (!nextContour?.length) return false
|
||||||
|
|
||||||
|
const nextPoints = safeClone(targetTuple[1] || [])
|
||||||
|
const contourIndex =
|
||||||
|
continueTargetContourIndex !== null &&
|
||||||
|
continueTargetContourIndex >= 0 &&
|
||||||
|
continueTargetContourIndex < nextPoints.length
|
||||||
|
? continueTargetContourIndex
|
||||||
|
: nextPoints.length - 1
|
||||||
|
if (contourIndex < 0) return false
|
||||||
|
|
||||||
|
nextPoints[contourIndex] = nextContour
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.setOperation(target.imageId, target.operationId, [
|
||||||
|
target.objectId,
|
||||||
|
nextPoints,
|
||||||
|
safeClone(targetTuple[2] || initialDetail),
|
||||||
|
safeClone(targetTuple[3] || []),
|
||||||
|
])
|
||||||
|
useObjectStore
|
||||||
|
.getState()
|
||||||
|
.updateSelectedPath(target.imageId, "ADD", target.objectId)
|
||||||
|
clearContinuePolygonContext(false)
|
||||||
|
forceUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const rebuildPolygonDraft = () => {
|
const rebuildPolygonDraft = () => {
|
||||||
polygon?.remove()
|
polygon?.remove()
|
||||||
polygon = null
|
polygon = null
|
||||||
@@ -2855,6 +3051,230 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paper path data里可能带函数(例如事件回调), 不能直接structuredClone
|
||||||
|
const getSafePathData = (data: any) => {
|
||||||
|
const nextData: Record<string, any> = {}
|
||||||
|
Object.entries(data || {}).forEach(([key, value]) => {
|
||||||
|
if (typeof value === "function") return
|
||||||
|
nextData[key] = value
|
||||||
|
})
|
||||||
|
return nextData
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSegmentPoint = (segment: any): [number, number] | null => {
|
||||||
|
if (
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
segment.length >= 2 &&
|
||||||
|
typeof segment[0] === "number" &&
|
||||||
|
typeof segment[1] === "number"
|
||||||
|
) {
|
||||||
|
return [segment[0], segment[1]]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
Array.isArray(segment[0]) &&
|
||||||
|
segment[0].length >= 2 &&
|
||||||
|
typeof segment[0][0] === "number" &&
|
||||||
|
typeof segment[0][1] === "number"
|
||||||
|
) {
|
||||||
|
return [segment[0][0], segment[0][1]]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
segment?.point &&
|
||||||
|
typeof segment.point.x === "number" &&
|
||||||
|
typeof segment.point.y === "number"
|
||||||
|
) {
|
||||||
|
return [segment.point.x, segment.point.y]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
segment?.point &&
|
||||||
|
Array.isArray(segment.point) &&
|
||||||
|
segment.point.length >= 2 &&
|
||||||
|
typeof segment.point[0] === "number" &&
|
||||||
|
typeof segment.point[1] === "number"
|
||||||
|
) {
|
||||||
|
return [segment.point[0], segment.point[1]]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePolygonFromSegments = (segments: any[]) => {
|
||||||
|
const polygon = (segments || [])
|
||||||
|
.map((segment) => getSegmentPoint(segment))
|
||||||
|
.filter((point): point is [number, number] => !!point)
|
||||||
|
|
||||||
|
if (polygon.length > 1) {
|
||||||
|
const [firstX, firstY] = polygon[0]
|
||||||
|
const [lastX, lastY] = polygon[polygon.length - 1]
|
||||||
|
if (
|
||||||
|
Math.abs(firstX - lastX) < 0.000001 &&
|
||||||
|
Math.abs(firstY - lastY) < 0.000001
|
||||||
|
) {
|
||||||
|
polygon.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPointInPolygon = (
|
||||||
|
point: [number, number],
|
||||||
|
polygon: [number, number][]
|
||||||
|
) => {
|
||||||
|
if (polygon.length < 3) return false
|
||||||
|
const [x, y] = point
|
||||||
|
let inside = false
|
||||||
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
|
const [xi, yi] = polygon[i]
|
||||||
|
const [xj, yj] = polygon[j]
|
||||||
|
const dy = yj - yi
|
||||||
|
if (yi > y !== yj > y) {
|
||||||
|
const crossX = ((xj - xi) * (y - yi)) / (dy || 0.000001) + xi
|
||||||
|
if (x < crossX) inside = !inside
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHollowFlagsByContainment = (allSegments: any[][]) => {
|
||||||
|
const polygons = allSegments.map((segments) =>
|
||||||
|
normalizePolygonFromSegments(segments)
|
||||||
|
)
|
||||||
|
return polygons.map((polygon, index) => {
|
||||||
|
if (polygon.length < 3) return false
|
||||||
|
const samplePoint = polygon[0]
|
||||||
|
let containDepth = 0
|
||||||
|
polygons.forEach((otherPolygon, otherIndex) => {
|
||||||
|
if (otherIndex === index || otherPolygon.length < 3) return
|
||||||
|
if (isPointInPolygon(samplePoint, otherPolygon)) containDepth += 1
|
||||||
|
})
|
||||||
|
return containDepth % 2 === 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectedPolygonSubtract = (draftPath: paper.Path) => {
|
||||||
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
|
const selectedIds =
|
||||||
|
useObjectStore.getState().selectedPath[activeImage] || []
|
||||||
|
if (selectedIds.length !== 1) return false
|
||||||
|
|
||||||
|
const selectedId = selectedIds[0]
|
||||||
|
const targetShapes = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(selectedId)
|
||||||
|
.filter((item) => item.data?.type === "polygon")
|
||||||
|
|
||||||
|
const targetShape =
|
||||||
|
targetShapes.find((item) => item instanceof paper.CompoundPath) ||
|
||||||
|
targetShapes.find(
|
||||||
|
(item) => item instanceof paper.Path && !item.data?.isHollowPolygon
|
||||||
|
) ||
|
||||||
|
targetShapes.find((item) => item instanceof paper.Path)
|
||||||
|
|
||||||
|
if (!targetShape) return false
|
||||||
|
|
||||||
|
let clippedDraft: paper.Path | paper.CompoundPath | null =
|
||||||
|
draftPath.intersect(usePaperStore.getState().rasterPath!, {
|
||||||
|
insert: false,
|
||||||
|
trace: false,
|
||||||
|
}) as paper.Path | paper.CompoundPath
|
||||||
|
|
||||||
|
draftPath.remove()
|
||||||
|
|
||||||
|
const draftHasSegments =
|
||||||
|
clippedDraft instanceof paper.CompoundPath
|
||||||
|
? (clippedDraft.children as paper.Path[]).some(
|
||||||
|
(child) => child.segments?.length
|
||||||
|
)
|
||||||
|
: clippedDraft.segments?.length
|
||||||
|
if (!draftHasSegments) {
|
||||||
|
clippedDraft?.remove()
|
||||||
|
clippedDraft = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetData = getSafePathData(targetShape.data || {})
|
||||||
|
let nextPolygon = (
|
||||||
|
targetShape as paper.Path | paper.CompoundPath
|
||||||
|
).subtract(clippedDraft) as paper.Path | paper.CompoundPath
|
||||||
|
clippedDraft?.remove()
|
||||||
|
clippedDraft = null
|
||||||
|
targetShape.remove()
|
||||||
|
|
||||||
|
const points: any[] = []
|
||||||
|
const hollowPoints: any[] = []
|
||||||
|
const nextPaths =
|
||||||
|
nextPolygon instanceof paper.CompoundPath
|
||||||
|
? (nextPolygon.children as paper.Path[])
|
||||||
|
: ([nextPolygon] as paper.Path[])
|
||||||
|
const nextPathSegments = nextPaths.map((path) => {
|
||||||
|
if (!path.segments?.length) return null
|
||||||
|
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||||||
|
return pathData?.[1]?.segments || null
|
||||||
|
})
|
||||||
|
const hollowFlags = getHollowFlagsByContainment(
|
||||||
|
nextPathSegments.map((segments) => segments || [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (nextPolygon instanceof paper.CompoundPath) {
|
||||||
|
nextPolygon.data = Object.assign({}, targetData)
|
||||||
|
}
|
||||||
|
nextPaths.forEach((path, index) => {
|
||||||
|
const segments = nextPathSegments[index]
|
||||||
|
if (!segments?.length) return
|
||||||
|
const isHollowPolygon = !!hollowFlags[index]
|
||||||
|
path.data = Object.assign({}, targetData, {
|
||||||
|
isHollowPolygon,
|
||||||
|
})
|
||||||
|
if (isHollowPolygon) {
|
||||||
|
hollowPoints.push(segments)
|
||||||
|
} else {
|
||||||
|
points.push(segments)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const operationId = (
|
||||||
|
targetData.operationId ||
|
||||||
|
useTopToolsStore.getState().activeOperation ||
|
||||||
|
""
|
||||||
|
).toString()
|
||||||
|
if (!operationId) {
|
||||||
|
nextPolygon.remove()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const prevDetail = safeClone(
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.getLabelDetailDataByKeys(activeImage, operationId, selectedId) ||
|
||||||
|
initialDetail
|
||||||
|
)
|
||||||
|
|
||||||
|
if (points.length) {
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.setOperation(activeImage, operationId, [
|
||||||
|
selectedId,
|
||||||
|
points,
|
||||||
|
prevDetail,
|
||||||
|
hollowPoints,
|
||||||
|
])
|
||||||
|
useObjectStore
|
||||||
|
.getState()
|
||||||
|
.updateSelectedPath(activeImage, "ADD", selectedId)
|
||||||
|
} else {
|
||||||
|
nextPolygon.remove()
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.deleteOperation(activeImage, operationId, selectedId)
|
||||||
|
useObjectStore.getState().updateSelectedPath(activeImage, "ADD")
|
||||||
|
const nextLabel = safeClone(useLabelStore.getState().label)
|
||||||
|
useLabelStore.getState().setLabel(nextLabel)
|
||||||
|
useLabelStore.getState().pushStateStack(nextLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
forceUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
polygonTool.onMouseDown = (e: {
|
polygonTool.onMouseDown = (e: {
|
||||||
event: MouseEvent
|
event: MouseEvent
|
||||||
point: paper.Point
|
point: paper.Point
|
||||||
@@ -2874,6 +3294,14 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
usePaperSupportStore.getState().clearAll()
|
usePaperSupportStore.getState().clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!polygon &&
|
||||||
|
!points.length &&
|
||||||
|
usePaperStore.getState().continuePolygonTarget
|
||||||
|
) {
|
||||||
|
if (!startContinuePolygonDraft()) return
|
||||||
|
}
|
||||||
|
|
||||||
// 先判断是否存在吸附点
|
// 先判断是否存在吸附点
|
||||||
let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point
|
let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point
|
||||||
// doubleclick
|
// doubleclick
|
||||||
@@ -2897,35 +3325,107 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
// 初始化 compoundPolygon
|
// 初始化 compoundPolygon
|
||||||
compoundPolygon = new paper.Path()
|
compoundPolygon = new paper.Path()
|
||||||
|
|
||||||
|
if (usePaperStore.getState().continuePolygonTarget) {
|
||||||
|
;(polygon as paper.Path).closePath()
|
||||||
|
const continueSaved = saveContinuePolygonDraft(
|
||||||
|
polygon as paper.Path
|
||||||
|
)
|
||||||
|
if (!continueSaved) {
|
||||||
|
polygon.remove()
|
||||||
|
clearContinuePolygonContext(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCircles.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
selectedCircles = []
|
||||||
|
clearPolygonMagnet()
|
||||||
|
useTopToolsStore.getState().setPressA(false)
|
||||||
|
draftId = null
|
||||||
|
lastPoint = null
|
||||||
|
lastTime = 0
|
||||||
|
points = []
|
||||||
|
polygon = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useKeyboardStore.getState().drawAction === "subtract") {
|
||||||
|
;(polygon as paper.Path).closePath()
|
||||||
|
handleSelectedPolygonSubtract(polygon as paper.Path)
|
||||||
|
|
||||||
|
selectedCircles.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
selectedCircles = []
|
||||||
|
clearPolygonMagnet()
|
||||||
|
useTopToolsStore.getState().setPressA(false)
|
||||||
|
draftId = null
|
||||||
|
lastPoint = null
|
||||||
|
lastTime = 0
|
||||||
|
points = []
|
||||||
|
polygon = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// pressA 模式下添加多边形
|
// pressA 模式下添加多边形
|
||||||
if (useTopToolsStore.getState().pressA) {
|
if (useTopToolsStore.getState().pressA) {
|
||||||
let ids =
|
let ids =
|
||||||
useObjectStore.getState().selectedPath[
|
useObjectStore.getState().selectedPath[
|
||||||
useBottomToolsStore.getState().activeImage
|
useBottomToolsStore.getState().activeImage
|
||||||
]
|
]
|
||||||
|
const selectedId = ids?.[0]
|
||||||
|
if (!selectedId) {
|
||||||
|
resetPolygonDraft()
|
||||||
|
return
|
||||||
|
}
|
||||||
let paths = usePaperStore.getState().getItemsById(ids[0])
|
let paths = usePaperStore.getState().getItemsById(ids[0])
|
||||||
let oldPolygon = polygon
|
let oldPolygon = polygon
|
||||||
// 闭合绘制路径
|
// 闭合绘制路径
|
||||||
oldPolygon.closePath()
|
oldPolygon.closePath()
|
||||||
|
const polygonPaths = paths.filter(
|
||||||
|
(path) => path.data?.type === "polygon"
|
||||||
|
)
|
||||||
|
const prevCompoundPath =
|
||||||
|
(polygonPaths.find(
|
||||||
|
(path) => path instanceof paper.CompoundPath
|
||||||
|
) as paper.CompoundPath | undefined) ||
|
||||||
|
((polygonPaths.find(
|
||||||
|
(path) => path.parent instanceof paper.CompoundPath
|
||||||
|
)?.parent as paper.CompoundPath | undefined) ??
|
||||||
|
undefined)
|
||||||
|
|
||||||
if (paths.length === 1) {
|
const basePathData = getSafePathData(
|
||||||
const prevPath = paths[0]
|
polygonPaths.find((path) => !!path.data)?.data || {}
|
||||||
|
)
|
||||||
|
oldPolygon.data = Object.assign({}, basePathData, oldPolygon.data, {
|
||||||
|
id: selectedId,
|
||||||
|
type: "polygon",
|
||||||
|
isHollowPolygon: !oldPolygon.clockwise,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (prevCompoundPath) {
|
||||||
|
// 已是多区域对象, 直接追加 child
|
||||||
|
prevCompoundPath.addChild(oldPolygon)
|
||||||
|
prevCompoundPath.data = Object.assign(
|
||||||
|
{},
|
||||||
|
getSafePathData(prevCompoundPath.data || {}),
|
||||||
|
basePathData,
|
||||||
|
{ id: selectedId, type: "polygon" }
|
||||||
|
)
|
||||||
|
polygon = prevCompoundPath
|
||||||
|
} else {
|
||||||
|
const prevPaths = polygonPaths.filter(
|
||||||
|
(path): path is paper.Path => path instanceof paper.Path
|
||||||
|
)
|
||||||
polygon = usePaperStore.getState().addCompoundPath(
|
polygon = usePaperStore.getState().addCompoundPath(
|
||||||
renderCompoundPath,
|
renderCompoundPath,
|
||||||
[prevPath, oldPolygon] as paper.Path[],
|
[...prevPaths, oldPolygon] as paper.Path[],
|
||||||
{
|
{
|
||||||
// option
|
// option
|
||||||
...usePaperStore.getState().toolOption,
|
...usePaperStore.getState().toolOption,
|
||||||
},
|
},
|
||||||
{ ...oldPolygon.data, id: prevPath?.data.id }
|
{ ...basePathData, ...oldPolygon.data, id: selectedId }
|
||||||
)
|
)
|
||||||
} else if (paths.length > 1) {
|
|
||||||
const prevPath = paths.filter(
|
|
||||||
(path) => path instanceof paper.CompoundPath
|
|
||||||
)[0]
|
|
||||||
// 直接将当前路径添加到原有父路径
|
|
||||||
prevPath.addChild(oldPolygon)
|
|
||||||
polygon = prevPath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3271,8 +3771,13 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
polygonTool.onKeyDown = (e: paper.KeyEvent) => {
|
polygonTool.onKeyDown = (e: paper.KeyEvent) => {
|
||||||
if (e.key === "escape" || e.key === "alt") {
|
if (e.key === "escape" || e.key === "alt") {
|
||||||
if (polygon || points.length) {
|
if (
|
||||||
|
polygon ||
|
||||||
|
points.length ||
|
||||||
|
usePaperStore.getState().continuePolygonTarget
|
||||||
|
) {
|
||||||
resetPolygonDraft()
|
resetPolygonDraft()
|
||||||
|
clearContinuePolygonContext(true)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(null)
|
usePaperSupportStore.getState().handleBufferPaths(null)
|
||||||
}
|
}
|
||||||
} else if (e.key === "delete") {
|
} else if (e.key === "delete") {
|
||||||
@@ -4817,6 +5322,14 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
circles.forEach((circle) => {
|
circles.forEach((circle) => {
|
||||||
circle.scale(1 / newScale)
|
circle.scale(1 / newScale)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tagItems = usePaperStore.getState().group!.getItems({
|
||||||
|
data: { type: "tag" },
|
||||||
|
}) as paper.Item[]
|
||||||
|
tagItems.forEach((item) => {
|
||||||
|
if (item.data?.tag_category === "mask") return
|
||||||
|
item.scale(1 / newScale)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAll: () => {
|
getAll: () => {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const useRenderTag = () => {
|
|||||||
parent: group,
|
parent: group,
|
||||||
visible,
|
visible,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 文本框
|
// 文本框
|
||||||
const textMask = new paper.Path.Rectangle(text.bounds)
|
const textMask = new paper.Path.Rectangle(text.bounds)
|
||||||
textMask.set({
|
textMask.set({
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ interface TopToolsState {
|
|||||||
) => void
|
) => void
|
||||||
crosshairStatus: "hidden" | "line" | "carve"
|
crosshairStatus: "hidden" | "line" | "carve"
|
||||||
setCrosshairStatus: (val: "hidden" | "line" | "carve") => void
|
setCrosshairStatus: (val: "hidden" | "line" | "carve") => void
|
||||||
|
assistToolEnabled: boolean
|
||||||
|
setAssistToolEnabled: (val: boolean) => void
|
||||||
|
assistToolSize: number
|
||||||
|
setAssistToolSize: (val: number) => void
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
setShowTags: (val: boolean) => void
|
setShowTags: (val: boolean) => void
|
||||||
showTagsConfigs: string[]
|
showTagsConfigs: string[]
|
||||||
@@ -85,6 +89,8 @@ const initialTopToolsState = {
|
|||||||
brightness: 0,
|
brightness: 0,
|
||||||
},
|
},
|
||||||
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
||||||
|
assistToolEnabled: false,
|
||||||
|
assistToolSize: 10,
|
||||||
drawOption: "default" as "default" | "intersect" | "unite",
|
drawOption: "default" as "default" | "intersect" | "unite",
|
||||||
saveCurrentScale: false,
|
saveCurrentScale: false,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
@@ -136,7 +142,7 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
}
|
}
|
||||||
let path = usePaperStore.getState().getItemById(ids[0])
|
let path = usePaperStore.getState().getItemById(ids[0])
|
||||||
if (path && path.data.type !== "rectangle") {
|
if (path && path.data.type !== "rectangle") {
|
||||||
let flag = val && usePaperStore.getState().mode !== "rectangle"
|
let flag = val
|
||||||
|
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -156,6 +162,18 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
...state,
|
...state,
|
||||||
crosshairStatus: val,
|
crosshairStatus: val,
|
||||||
})),
|
})),
|
||||||
|
assistToolEnabled: initialTopToolsState.assistToolEnabled,
|
||||||
|
setAssistToolEnabled: (val) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
assistToolEnabled: val,
|
||||||
|
})),
|
||||||
|
assistToolSize: initialTopToolsState.assistToolSize,
|
||||||
|
setAssistToolSize: (val) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
assistToolSize: Math.max(1, val),
|
||||||
|
})),
|
||||||
showTags: false,
|
showTags: false,
|
||||||
setShowTags: (val) =>
|
setShowTags: (val) =>
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
|
|||||||
71
components/label/utils/objectVisibility.ts
Normal file
71
components/label/utils/objectVisibility.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import paper from "paper"
|
||||||
|
import { useLabelStore, useObjectStore } from "../store"
|
||||||
|
import { usePaperStore } from "../usePaperStore"
|
||||||
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
|
||||||
|
const getTagVisibility = (tagCategory?: string) => {
|
||||||
|
const { showTags, showTagsConfigs } = useTopToolsStore.getState()
|
||||||
|
if (!showTags) return false
|
||||||
|
if (tagCategory === "mask") return showTagsConfigs.includes("外框")
|
||||||
|
if (tagCategory === "point_text") return showTagsConfigs.includes("点ID")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelectedItemFalse = (id: number) => {
|
||||||
|
if (!id) return
|
||||||
|
const sameIdItems = usePaperStore.getState().getItemsById(id)
|
||||||
|
sameIdItems.forEach((item) => {
|
||||||
|
if (item.data.type === "mask" || item.data.type === "text") {
|
||||||
|
item.remove()
|
||||||
|
}
|
||||||
|
if (item.data.selected) item.data.selected = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncObjectItemsVisibility = (objectId: number, hide: boolean) => {
|
||||||
|
const sameIdItems = usePaperStore.getState().getItemsById(objectId)
|
||||||
|
if (!sameIdItems?.length) return
|
||||||
|
sameIdItems.forEach((item) => {
|
||||||
|
if (hide) {
|
||||||
|
item.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.data?.type === "tag") {
|
||||||
|
item.visible = getTagVisibility(item.data?.tag_category)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.visible = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleObjectHideByShortcut = (
|
||||||
|
imageId: string,
|
||||||
|
operationId: string,
|
||||||
|
objectId: number,
|
||||||
|
hide: boolean
|
||||||
|
) => {
|
||||||
|
const { updatePathStatus, updateOperationStatus } = useObjectStore.getState()
|
||||||
|
const storeLabel = useLabelStore.getState().label
|
||||||
|
|
||||||
|
if (hide) {
|
||||||
|
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||||
|
syncObjectItemsVisibility(objectId, hide)
|
||||||
|
setSelectedItemFalse(objectId)
|
||||||
|
|
||||||
|
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
|
||||||
|
const hasVisibleChild = operationChildren.some((child) => {
|
||||||
|
const item = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemById(child[0]) as paper.Item | null
|
||||||
|
return !!item?.visible
|
||||||
|
})
|
||||||
|
if (!hasVisibleChild) {
|
||||||
|
updateOperationStatus(imageId + operationId, "HIDE", hide)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOperationStatus(imageId + operationId, "HIDE", hide)
|
||||||
|
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||||
|
syncObjectItemsVisibility(objectId, hide)
|
||||||
|
}
|
||||||
128
components/setting/PageSurface.module.css
Normal file
128
components/setting/PageSurface.module.css
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 56px);
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border-color: #e6ebf2;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-InputWrapper-label) {
|
||||||
|
color: #1f2329;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 22px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Input-input),
|
||||||
|
.filterPanel :global(.mantine-Select-input),
|
||||||
|
.filterPanel :global(.mantine-NumberInput-input),
|
||||||
|
.filterPanel :global(.mantine-PillsInput-input),
|
||||||
|
.filterPanel :global(.mantine-Textarea-input) {
|
||||||
|
min-height: 36px;
|
||||||
|
border-color: #d0d5dd;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #31373d;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Textarea-input) {
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Input-input::placeholder),
|
||||||
|
.filterPanel :global(.mantine-Select-input::placeholder),
|
||||||
|
.filterPanel :global(.mantine-NumberInput-input::placeholder),
|
||||||
|
.filterPanel :global(.mantine-PillsInput-input::placeholder),
|
||||||
|
.filterPanel :global(.mantine-Textarea-input::placeholder) {
|
||||||
|
color: #98a2b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Input-input:focus),
|
||||||
|
.filterPanel :global(.mantine-Select-input:focus),
|
||||||
|
.filterPanel :global(.mantine-NumberInput-input:focus),
|
||||||
|
.filterPanel :global(.mantine-PillsInput-input:focus-within),
|
||||||
|
.filterPanel :global(.mantine-Textarea-input:focus) {
|
||||||
|
border-color: #69c0ff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(105, 192, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Button-root[data-variant="transparent"]) {
|
||||||
|
color: #168de8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Button-root:not([data-variant="transparent"])),
|
||||||
|
.headerActions :global(.mantine-Button-root) {
|
||||||
|
min-height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-ActionIcon-root),
|
||||||
|
.headerActions :global(.mantine-ActionIcon-root) {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPanel :global(.mantine-Pill-root) {
|
||||||
|
background: #eef7ff;
|
||||||
|
color: #168de8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPanel {
|
||||||
|
border-color: #e6ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterActions,
|
||||||
|
.headerActions {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #eef2f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionEyebrow {
|
||||||
|
color: #168de8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
color: #1f2329;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
312
components/setting/PageSurface.tsx
Normal file
312
components/setting/PageSurface.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
type GroupProps,
|
||||||
|
type PaperProps,
|
||||||
|
type StackProps,
|
||||||
|
} from "@mantine/core"
|
||||||
|
import type { PropsWithChildren, ReactNode } from "react"
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
type DataTableDefaultColumnProps,
|
||||||
|
type DataTableProps,
|
||||||
|
} from "mantine-datatable"
|
||||||
|
import classes from "./PageSurface.module.css"
|
||||||
|
|
||||||
|
function mergeClassName(...classNames: Array<string | undefined>) {
|
||||||
|
return classNames.filter(Boolean).join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingPage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<StackProps>) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
w="100%"
|
||||||
|
h="calc(100vh - 56px)"
|
||||||
|
p="md"
|
||||||
|
gap="md"
|
||||||
|
className={mergeClassName(classes.page, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingPanel({
|
||||||
|
className,
|
||||||
|
withBorder = true,
|
||||||
|
radius = "lg",
|
||||||
|
p = "md",
|
||||||
|
shadow = "xs",
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<PaperProps>) {
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
withBorder={withBorder}
|
||||||
|
radius={radius}
|
||||||
|
p={p}
|
||||||
|
shadow={shadow}
|
||||||
|
className={mergeClassName(classes.panel, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingFilterPanel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<PaperProps>) {
|
||||||
|
return (
|
||||||
|
<SettingPanel
|
||||||
|
className={mergeClassName(classes.filterPanel, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingContentPanel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<PaperProps>) {
|
||||||
|
return (
|
||||||
|
<SettingPanel
|
||||||
|
className={mergeClassName(classes.contentPanel, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingSectionHeaderProps = GroupProps & {
|
||||||
|
eyebrow?: string
|
||||||
|
title: ReactNode
|
||||||
|
description?: ReactNode
|
||||||
|
actions?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingSectionHeader({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SettingSectionHeaderProps) {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
gap="sm"
|
||||||
|
wrap="wrap"
|
||||||
|
className={mergeClassName(classes.sectionHeader, className)}
|
||||||
|
{...props}>
|
||||||
|
<Stack gap={2}>
|
||||||
|
{eyebrow ? (
|
||||||
|
<Text className={classes.sectionEyebrow}>{eyebrow}</Text>
|
||||||
|
) : null}
|
||||||
|
<Text className={classes.sectionTitle}>{title}</Text>
|
||||||
|
{description ? (
|
||||||
|
<Text className={classes.sectionDescription}>{description}</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
{actions}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingListHeaderProps = {
|
||||||
|
title: ReactNode
|
||||||
|
count?: ReactNode
|
||||||
|
actions?: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingListHeader({
|
||||||
|
title,
|
||||||
|
count,
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
}: SettingListHeaderProps) {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
wrap="wrap"
|
||||||
|
gap="sm"
|
||||||
|
className={mergeClassName(classes.listHeader, className)}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text fw={700}>{title}</Text>
|
||||||
|
{count !== undefined ? (
|
||||||
|
<Badge variant="light" color="gray">
|
||||||
|
{count}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
{actions}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingHeaderActions({
|
||||||
|
className,
|
||||||
|
gap = "sm",
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<GroupProps>) {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
gap={gap}
|
||||||
|
wrap="wrap"
|
||||||
|
className={mergeClassName(classes.headerActions, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingFilterActions({
|
||||||
|
className,
|
||||||
|
gap = "sm",
|
||||||
|
justify = "flex-end",
|
||||||
|
mt = "md",
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<GroupProps>) {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={justify}
|
||||||
|
gap={gap}
|
||||||
|
wrap="wrap"
|
||||||
|
mt={mt}
|
||||||
|
className={mergeClassName(classes.filterActions, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingDataTableStyles = {
|
||||||
|
header: {
|
||||||
|
backgroundColor: "#F8F9FB",
|
||||||
|
borderBottom: "1px solid #EEF2F6",
|
||||||
|
color: "#56606A",
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
height: "100%",
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#31373D",
|
||||||
|
borderBottom: "1px solid #E8EDF3",
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
padding: "14px 16px 18px",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const settingDataTableDefaultColumnProps: DataTableDefaultColumnProps<
|
||||||
|
Record<string, unknown>
|
||||||
|
> = {
|
||||||
|
titleStyle: {
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingDataTable<T = Record<string, unknown>>(
|
||||||
|
props: DataTableProps<T>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
styles,
|
||||||
|
defaultColumnProps,
|
||||||
|
withTableBorder,
|
||||||
|
withRowBorders,
|
||||||
|
striped,
|
||||||
|
highlightOnHover,
|
||||||
|
loaderBackgroundBlur,
|
||||||
|
loadingText,
|
||||||
|
noRecordsText,
|
||||||
|
minHeight,
|
||||||
|
page,
|
||||||
|
onPageChange,
|
||||||
|
totalRecords,
|
||||||
|
recordsPerPage,
|
||||||
|
recordsPerPageLabel,
|
||||||
|
paginationText,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const paginationProps =
|
||||||
|
page !== undefined &&
|
||||||
|
onPageChange !== undefined &&
|
||||||
|
recordsPerPage !== undefined
|
||||||
|
? {
|
||||||
|
page,
|
||||||
|
onPageChange,
|
||||||
|
totalRecords,
|
||||||
|
recordsPerPage,
|
||||||
|
loadingText: loadingText ?? "正在加载数据...",
|
||||||
|
recordsPerPageLabel: recordsPerPageLabel ?? "每页条数",
|
||||||
|
paginationText:
|
||||||
|
paginationText ??
|
||||||
|
(({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
totalRecords,
|
||||||
|
}: {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
totalRecords: number
|
||||||
|
}) => `${from}-${to} / ${totalRecords}`),
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const tableProps = {
|
||||||
|
withTableBorder: withTableBorder ?? true,
|
||||||
|
withRowBorders: withRowBorders ?? true,
|
||||||
|
striped: striped ?? true,
|
||||||
|
highlightOnHover: highlightOnHover ?? true,
|
||||||
|
defaultColumnProps:
|
||||||
|
defaultColumnProps ??
|
||||||
|
(settingDataTableDefaultColumnProps as DataTableDefaultColumnProps<T>),
|
||||||
|
loaderBackgroundBlur: loaderBackgroundBlur ?? 1,
|
||||||
|
noRecordsText: noRecordsText ?? "暂无数据",
|
||||||
|
minHeight: minHeight ?? 360,
|
||||||
|
styles: styles
|
||||||
|
? { ...settingDataTableStyles, ...styles }
|
||||||
|
: settingDataTableStyles,
|
||||||
|
...paginationProps,
|
||||||
|
...rest,
|
||||||
|
} as DataTableProps<T>
|
||||||
|
|
||||||
|
return <DataTable {...tableProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingTabsStyles = {
|
||||||
|
list: {
|
||||||
|
padding: 4,
|
||||||
|
backgroundColor: "#F8F9FB",
|
||||||
|
border: "1px solid #EEF2F6",
|
||||||
|
borderRadius: 10,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
minHeight: 36,
|
||||||
|
borderRadius: 8,
|
||||||
|
color: "#56606A",
|
||||||
|
fontWeight: 600,
|
||||||
|
"&[data-active]": {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
color: "#1F2329",
|
||||||
|
boxShadow: "0 6px 18px rgba(15, 23, 42, 0.08)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
} as const
|
||||||
Reference in New Issue
Block a user