Merge commit '062242a63ccb2d57868033730d9edbbf68c44896' into zh

This commit is contained in:
2026-03-16 15:49:34 +08:00
24 changed files with 4448 additions and 4 deletions

View File

@@ -0,0 +1,282 @@
"use client"
export const DataIcon = () => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0.1 4C0.1 1.84609 1.84609 0.1 4 0.1H28C30.1539 0.1 31.9 1.84609 31.9 4V28C31.9 30.1539 30.1539 31.9 28 31.9H4C1.84609 31.9 0.1 30.1539 0.1 28V4Z"
fill="url(#paint0_linear_726_9281)"
stroke="url(#paint1_linear_726_9281)"
strokeWidth="0.2"
strokeLinejoin="round"
/>
<path
d="M8.67848 12.5511L9.89873 11.2781L10.6614 12.0738L9.44114 13.3468L10.2038 14.1424L11.424 12.7898L12.1867 13.5855L10.9665 14.8585L11.1953 15.495L15.2373 11.3577L12.4918 8.25471C12.0342 7.8569 11.1953 7.93646 10.8139 8.41384L8.22088 10.9599C7.83955 11.3577 7.99209 11.9146 8.37341 12.3124L8.67848 12.5511ZM8.37341 23.0535C8.29715 23.3717 8.52595 23.6104 8.83101 23.5309L13.4832 22.0192L9.89873 18.3593L8.37341 23.0535ZM23.5503 11.9942C23.9316 11.5964 23.9316 11.0394 23.5503 10.6416L21.3386 8.41384C20.9573 8.01602 20.3472 8.01602 19.9658 8.41384L18.3642 10.0051L21.9487 13.5855L23.5503 11.9942ZM17.8304 10.5621L10.4326 17.8819L14.0171 21.4622L21.4149 14.0628L17.8304 10.5621ZM23.7791 19.3936L20.4234 16.1315L16.3051 20.1892L17.0677 20.9848L18.3642 19.7118L19.1269 20.5075L17.8304 21.7805L18.593 22.5761L19.8896 21.3031L20.6522 22.0987L19.3557 23.3717L19.5845 23.6104C19.9658 24.0083 20.5759 24.1674 21.0335 23.7696L23.6266 21.2235C24.0079 20.6666 24.1604 19.7914 23.7791 19.3936Z"
fill="white"
/>
<defs>
<linearGradient
id="paint0_linear_726_9281"
x1="-4"
y1="-9.5"
x2="29"
y2="32"
gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.26" />
<stop offset="1" stopColor="#FFCBCE" />
</linearGradient>
<linearGradient
id="paint1_linear_726_9281"
x1="1.67638e-07"
y1="32"
x2="32"
y2="1.67638e-07"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FBACB3" />
<stop offset="1" stopColor="white" />
</linearGradient>
</defs>
</svg>
)
export const HourIcon = () => (
<svg
width="38"
height="37"
viewBox="0 0 38 37"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dd_726_9271)">
<rect
x="2"
y="1"
width="32"
height="32"
rx="3.84"
fill="url(#paint0_linear_726_9271)"
/>
</g>
<rect
x="2"
y="1"
width="32"
height="32"
rx="3.84"
fill="white"
fillOpacity="0.1"
/>
<path
d="M15.1422 18.626L16.6317 17.5382L20.6829 11.3447L21.6931 9.79101C21.7472 9.70607 21.774 9.60659 21.7699 9.50597C21.7656 9.42473 21.7414 9.34579 21.6995 9.27612C21.6575 9.20644 21.599 9.14817 21.5291 9.10646L20.6829 8.61169C20.5676 8.54209 20.43 8.51976 20.2987 8.54935C20.1674 8.57894 20.0526 8.65817 19.9784 8.77046L19.2562 9.87151L18.9667 10.3242L15.0322 16.7614L15.1429 18.626H15.1422ZM23.0216 9.77994H25.2333C25.7243 9.77994 26.1231 10.2193 26.1231 10.6956V21.9084C26.1231 22.3847 25.7243 22.7709 25.2333 22.7709H10.7668C10.2757 22.7709 9.87695 22.3847 9.87695 21.9084V10.6956C9.87695 10.2193 10.1362 9.77994 10.6272 9.77994H17.8893L13.8034 16.4165L14.0692 20.4736L17.8782 17.903L23.0216 9.77994ZM18.6115 19.4685C18.4533 19.4685 18.3017 19.5314 18.1898 19.6432C18.078 19.755 18.0152 19.9067 18.0152 20.0649C18.0152 20.223 18.078 20.3747 18.1898 20.4865C18.3017 20.5983 18.4533 20.6612 18.6115 20.6612H24.4896C24.6478 20.6612 24.7995 20.5983 24.9113 20.4865C25.0231 20.3747 25.0859 20.223 25.0859 20.0649C25.0859 19.9067 25.0231 19.755 24.9113 19.6432C24.7995 19.5314 24.6478 19.4685 24.4896 19.4685H18.6115ZM25.1963 24.0854H10.8052C10.3606 24.0854 10.0003 24.3941 10.0003 24.7744C10.0003 25.1547 10.3606 25.4634 10.8052 25.4634H25.1963C25.6416 25.4634 26.0013 25.1547 26.0013 24.7744C25.999 24.397 25.6394 24.0854 25.1963 24.0854Z"
fill="white"
/>
<defs>
<filter
id="filter0_dd_726_9271"
x="0.56"
y="0.0399997"
width="37.28"
height="36.8"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="0.48"
operator="erode"
in="SourceAlpha"
result="effect1_dropShadow_726_9271"
/>
<feOffset dx="1.44" dy="1.44" />
<feGaussianBlur stdDeviation="1.44" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0627451 0 0 0 0 0.760784 0 0 0 0 0.423529 0 0 0 0.15 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_726_9271"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="0.48"
operator="erode"
in="SourceAlpha"
result="effect2_dropShadow_726_9271"
/>
<feOffset dx="-0.48" />
<feGaussianBlur stdDeviation="0.72" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0627451 0 0 0 0 0.760784 0 0 0 0 0.423529 0 0 0 0.33 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_726_9271"
result="effect2_dropShadow_726_9271"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_726_9271"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_726_9271"
x1="-13"
y1="-27"
x2="27"
y2="31.5"
gradientUnits="userSpaceOnUse">
<stop stopColor="#E7F9F0" />
<stop offset="1" stopColor="#4FD796" />
</linearGradient>
</defs>
</svg>
)
export const ReviewIcon = () => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="3.84" fill="url(#paint0_linear_0_1)" />
<g filter="url(#filter0_ii_0_1)">
<rect width="32" height="32" rx="3.84" fill="white" fillOpacity="0.1" />
</g>
<g filter="url(#filter1_ii_0_1)">
<rect width="32" height="32" rx="3.84" fill="white" fillOpacity="0.1" />
</g>
<path
d="M23.227 18.008H20.5869C20.1405 18.008 19.7365 17.7447 19.5553 17.3347L18.4511 14.8444C18.2574 14.4063 18.3641 13.9006 18.7028 13.5623C19.4724 12.7923 19.9188 11.6954 19.8142 10.4966C19.6526 8.6318 18.1352 7.13848 16.2771 7.00945C14.0429 6.85439 12.1827 8.62868 12.1827 10.8411C12.1827 11.9265 12.6312 12.9057 13.351 13.604C13.6794 13.9224 13.7912 14.4032 13.6286 14.8309L12.6975 17.2785C12.6157 17.4931 12.471 17.6778 12.2826 17.808C12.0941 17.9383 11.8708 18.008 11.642 18.008H8.77399C8.06967 18.008 7.5 18.5813 7.5 19.2879V21.3859C7.5 22.0935 8.07071 22.6658 8.77399 22.6658H23.226C23.9303 22.6658 24.5 22.0925 24.5 21.3859V19.2879C24.501 18.5813 23.9303 18.008 23.227 18.008ZM23.139 25H8.86203C8.54819 25 8.2934 24.744 8.2934 24.4287C8.2934 24.1134 8.54819 23.8574 8.86203 23.8574H23.14C23.4539 23.8574 23.7087 24.1134 23.7087 24.4287C23.7086 24.5038 23.6938 24.5781 23.6652 24.6475C23.6365 24.7168 23.5945 24.7798 23.5416 24.8329C23.4887 24.8859 23.4259 24.928 23.3568 24.9567C23.2878 24.9853 23.2137 25.0001 23.139 25Z"
fill="white"
/>
<defs>
<filter
id="filter0_ii_0_1"
x="-0.48"
y="-0.48"
width="32.96"
height="32.96"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="0.48" dy="-0.48" />
<feGaussianBlur stdDeviation="0.48" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.760784 0 0 0 0 0.470588 0 0 0 0 0.0627451 0 0 0 0.15 0"
/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_1" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="-0.48" dy="0.48" />
<feGaussianBlur stdDeviation="0.74" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_0_1"
result="effect2_innerShadow_0_1"
/>
</filter>
<filter
id="filter1_ii_0_1"
x="-0.48"
y="-0.48"
width="32.96"
height="32.96"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="0.48" dy="-0.48" />
<feGaussianBlur stdDeviation="0.48" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.760784 0 0 0 0 0.470588 0 0 0 0 0.0627451 0 0 0 0.15 0"
/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_1" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="-0.48" dy="0.48" />
<feGaussianBlur stdDeviation="0.74" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_0_1"
result="effect2_innerShadow_0_1"
/>
</filter>
<linearGradient
id="paint0_linear_0_1"
x1="-15"
y1="-28"
x2="25"
y2="30.5"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FEF5E7" />
<stop offset="1" stopColor="#FFB84D" />
</linearGradient>
</defs>
</svg>
)

View File

@@ -0,0 +1,205 @@
"use client"
import { getPersonProjectDashBoard } from "@/components/label/api/project"
import { Project } from "@/components/label/api/project/typing"
import { Button, Group, Text, UnstyledButton } from "@mantine/core"
import { IconRefresh } from "@tabler/icons-react"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import { DataTable, DataTableColumn } from "mantine-datatable"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
dayjs.extend(duration)
function formatDurationSeconds(seconds?: number | null) {
const value = seconds ?? 0
if (!value) return "-"
const durationObj = dayjs.duration(value, "seconds")
const hours = durationObj.hours()
const minutes = durationObj.minutes()
if (hours) return `${hours}小时${minutes}分钟`
if (minutes) return `${minutes}分钟`
return "-"
}
export default function PersonalProjectTable() {
const router = useRouter()
const [records, setRecords] = useState<
Project.PersonProjectDashboardResponseItem[]
>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(50)
const [totalItems, setTotalItems] = useState(0)
const totalPages = useMemo(() => {
const totalItems = records.length
return Math.max(1, Math.ceil(totalItems / pageSize))
}, [pageSize, records.length])
const pageRecords = useMemo(() => {
const start = (page - 1) * pageSize
return records.slice(start, start + pageSize)
}, [page, pageSize, records])
const load = useCallback(async () => {
setLoading(true)
try {
const res = await getPersonProjectDashBoard()
setRecords(res ?? [])
setTotalItems(res?.length ?? 0)
setPage(1)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
load()
}, [load])
useEffect(() => {
if (page > totalPages) setPage(totalPages)
}, [page, totalPages])
const [currentRowId, setCurrentRowId] = useState<number>(0)
const originColumn = useMemo(() => {
let column: DataTableColumn<Project.PersonProjectDashboardResponseItem>[] =
[
{
accessor: "project_id",
title: "项目ID",
width: 150,
render: (record: Project.PersonProjectDashboardResponseItem) => {
return (
<UnstyledButton
px={6}
variant="transparent"
onClick={async () => {
setCurrentRowId(record.project_id)
router.push(`/project/info/detail?id=${record.project_id}`)
}}
title={record.project_id.toString()}
c={"brand"}
style={{
fontSize: "14px",
maxWidth: "100%",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{record.project_id}
</UnstyledButton>
)
},
},
{
accessor: "project_name",
title: "项目名称",
width: 160,
render: (record: Project.PersonProjectDashboardResponseItem) => {
return record.project_name ?? "-"
},
},
{
accessor: "project_type",
title: "所属业务",
width: 160,
render: (record: Project.PersonProjectDashboardResponseItem) => {
return (
<Text
title={record.project_id.toString()}
style={{
fontSize: "14px",
maxWidth: "100%",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{record.project_type ?? "-"}
</Text>
)
},
},
{
accessor: "work_time",
title: "任务耗时",
width: 140,
render: (record: Project.PersonProjectDashboardResponseItem) => {
return formatDurationSeconds(record.work_time)
},
},
{
accessor: "project_label_type",
title: "标注类别",
width: 160,
render: (record: Project.PersonProjectDashboardResponseItem) => {
return record.project_label_type ?? "-"
},
},
]
return column
}, [router])
return (
<Group
gap="sm"
h="100%"
align="stretch"
style={{ flexDirection: "column" }}>
<Group justify="space-between" align="center">
<Text fw={700} fz="lg" style={{ marginBottom: 8 }}>
</Text>
<Button
size={"xs"}
fz={"sm"}
fw={500}
radius="sm"
onClick={load}
loading={loading}
leftSection={<IconRefresh size={16} />}>
</Button>
</Group>
<Group
gap={0}
align="stretch"
style={{ flex: 1, minHeight: 0, width: "100%" }}>
<DataTable<Project.PersonProjectDashboardResponseItem>
width="100%"
style={{ width: "100%" }}
styles={{
header: {
color: "var(--mantine-color-grey-text)",
backgroundColor: "var(--mantine-datatable-striped-color)",
},
}}
withTableBorder
withRowBorders
pinFirstColumn
rowBackgroundColor={(record) => {
if (record.project_id === currentRowId)
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
}}
idAccessor="project_id"
scrollAreaProps={{ type: "auto" }}
records={pageRecords}
onRecordsPerPageChange={setPageSize}
recordsPerPageOptions={[10, 15, 20]}
columns={originColumn}
totalRecords={totalItems}
recordsPerPage={pageSize}
page={page}
onPageChange={setPage}
noRecordsText="暂无数据"
recordsPerPageLabel="当前页数"
/>
</Group>
</Group>
)
}

View File

@@ -0,0 +1,225 @@
"use client"
import { getUserTaskList } from "@/components/label/api/task"
import { Task } from "@/components/label/api/task/typing"
import {
useBackUrlStore,
usePermissionStore,
} from "@/components/label/store/auth"
import { TaskStatusEnum } from "@/components/label/utils/constants"
import { Badge, Button, Group, Text, UnstyledButton } from "@mantine/core"
import { IconRefresh } from "@tabler/icons-react"
import { DataTable, DataTableColumn } from "mantine-datatable"
import { useCallback, useEffect, useMemo, useState } from "react"
function getStatusColor(status?: number | null) {
switch (status) {
case 2:
return "blue"
case 4:
return "yellow"
case 6:
return "orange"
case 7:
return "green"
case 8:
return "red"
default:
return "gray"
}
}
export default function PersonalTaskTable() {
const user_name = usePermissionStore((s) => s.user_name)
const setBackProps = useBackUrlStore((s) => s.setBackProps)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(50)
const [totalItems, setTotalItems] = useState(0)
const [records, setRecords] = useState<Task.SimpleTaskItem[]>([])
const [loading, setLoading] = useState(false)
const totalPages = useMemo(() => {
return Math.max(1, Math.ceil(totalItems / pageSize))
}, [pageSize, totalItems])
const load = useCallback(async () => {
if (!user_name) return
setLoading(true)
try {
const res = await getUserTaskList({
page_number: page,
page_size: pageSize,
})
setRecords(res?.simple_task_list ?? [])
setTotalItems(res?.total_items ?? 0)
} finally {
setLoading(false)
}
}, [page, pageSize, user_name])
useEffect(() => {
load()
}, [load])
useEffect(() => {
if (page > totalPages) setPage(totalPages)
}, [page, totalPages])
const resetData = () => {
if (page === 1) load()
else setPage(1)
}
const [currentRowId, setCurrentRowId] = useState<number>(0)
const originColumn = useMemo(() => {
let column: DataTableColumn<Task.SimpleTaskItem>[] = [
{
accessor: "project_name",
title: "项目名称",
width: 150,
render: (record: Task.SimpleTaskItem) => {
return <Text>{record.project_name}</Text>
},
},
{
accessor: "id",
title: "任务ID",
width: 150,
render: (record: Task.SimpleTaskItem) => {
return (
<UnstyledButton
px={6}
variant="transparent"
onClick={async () => {
setCurrentRowId(record.id)
if ([4, 5, 6, 7].includes(record.label_type)) {
setBackProps(`/person/dashboard`)
if ([4, 5, 6].includes(record.label_type)) {
// router.replace(
// `/image/label?project_id=${record.project_id}&task_id=${record.id}`
// )
window.location.href = `/image/label?project_id=${record.project_id}&task_id=${record.id}`
} else if ([7].includes(record.label_type)) {
// router.replace(
// `/video/label?project_id=${record.project_id}&task_id=${record.id}`
// )
window.location.href = `/video/label?project_id=${record.project_id}&task_id=${record.id}`
}
} else {
// await runCommand(
// record.id.toString(),
// user_name,
// user_password
// )
}
}}
title={record.id.toString()}
c={"brand"}
style={{
fontSize: "14px",
maxWidth: "100%",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{record.id}
</UnstyledButton>
)
},
},
{
accessor: "label_status",
title: "状态",
width: 80,
render: (record: Task.SimpleTaskItem) => {
return (
<Badge
color={getStatusColor(record.label_status)}
style={{
fontSize: "12px",
padding: "2px 6px",
}}>
{TaskStatusEnum.get(record.label_status)}
</Badge>
)
},
},
{
accessor: "label_username",
title: "标注员",
width: 80,
},
{
accessor: "review1_username",
title: "审核员",
width: 80,
},
{
accessor: "review2_username",
title: "复审员",
width: 80,
},
]
return column
}, [setBackProps])
return (
<Group
gap="sm"
h="100%"
align="stretch"
style={{ flexDirection: "column" }}>
<Group justify="space-between" align="center">
<Text fw={700} fz="lg" style={{ marginBottom: 8 }}>
</Text>
<Button
size={"xs"}
fz={"sm"}
fw={500}
radius="sm"
onClick={resetData}
loading={loading}
leftSection={<IconRefresh size={16} />}>
</Button>
</Group>
<Group
gap={0}
align="stretch"
style={{ flex: 1, minHeight: 0, width: "100%" }}>
<DataTable<Task.SimpleTaskItem>
width="100%"
style={{ width: "100%" }}
styles={{
header: {
color: "var(--mantine-color-grey-text)",
backgroundColor: "var(--mantine-datatable-striped-color)",
},
}}
withTableBorder
withRowBorders
pinFirstColumn
pinLastColumn
rowBackgroundColor={({ id }) => {
if (id === currentRowId)
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
}}
scrollAreaProps={{ type: "auto" }}
records={records}
onRecordsPerPageChange={setPageSize}
recordsPerPageOptions={[10, 15, 20]}
columns={originColumn}
totalRecords={totalItems}
recordsPerPage={pageSize}
page={page}
onPageChange={setPage}
noRecordsText="暂无数据"
recordsPerPageLabel="当前页数"
/>
</Group>
</Group>
)
}

View File

@@ -1,5 +1,285 @@
"use client"
export default function DashboardPage() {
return <>Person Dashboard</>
import { getCurrentPersonData } from "@/components/label/api/project"
import {
BackgroundImage,
Center,
Group,
Paper,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import { JSX, useEffect, useMemo, useState } from "react"
import { DataIcon, HourIcon, ReviewIcon } from "./components/Icon"
import PersonalProjectTable from "./components/PersonalProjectTable"
import PersonalTaskTable from "./components/PersonalTaskTable"
import DataImage from "./resource/images/data.png"
import DataLogoImage from "./resource/images/data_logo.png"
import HourImage from "./resource/images/hour.png"
import HourLogoImage from "./resource/images/hour_logo.png"
import ReviewImage from "./resource/images/review.png"
import ReviewLogoImage from "./resource/images/review_logo.png"
dayjs.extend(duration)
function monthText(month: string) {
const list = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
]
const idx = Number(month) - 1
return list[idx] ?? ""
}
function formatDurationSeconds(seconds?: number | null) {
const value = seconds ?? 0
if (!value) return "-"
const durationObj = dayjs.duration(value, "seconds")
const hours = durationObj.hours()
const minutes = durationObj.minutes()
if (hours) return `${hours}小时${minutes}分钟`
if (minutes) return `${minutes}分钟`
return "-"
}
function StatCard(props: {
title: string
timeText: string
sizeTitle: string
sizeValue: number
sizeColor: string
background: string
backgroundLogo: string
Icon: () => JSX.Element
}) {
const {
title,
timeText,
sizeTitle,
sizeValue,
sizeColor,
background,
backgroundLogo,
Icon,
} = props
return (
<Paper
shadow="xs"
p="md"
withBorder
radius={"md"}
pos="relative"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
overflow: "hidden",
display: "flex",
}}>
<BackgroundImage
src={background}
w="100%"
h="100%"
style={{ position: "absolute", top: 0, left: 0, zIndex: 0 }}
/>
<BackgroundImage
src={backgroundLogo}
w={108}
h={108}
style={{
position: "absolute",
bottom: 0,
right: 8,
zIndex: 0,
backgroundRepeat: "no-repeat",
}}
/>
<Group gap="md" align="flex-start" style={{ flex: 1, zIndex: 1 }}>
<div style={{ width: 32 }}>
<Icon />
</div>
<Stack gap={6} style={{ flex: 1 }}>
<div>
<Text fw={700} fz="lg" lh={1.4}>
{title}
</Text>
<Title order={3} style={{ marginTop: 6 }}>
{timeText}
</Title>
</div>
<Stack gap={6}>
<Text fw={700} fz="lg">
{sizeTitle}
</Text>
<Paper
shadow="xs"
px={8}
py={4}
radius="sm"
style={{
background: "#fff",
width: "fit-content",
color: sizeColor,
fontWeight: 700,
fontSize: 16,
}}>
{sizeValue}
</Paper>
</Stack>
</Stack>
</Group>
</Paper>
)
}
export default function PersonDashboardPage() {
const [data, setData] = useState<{
label_data_size: number
label_work_time: number
review1_data_size: number
review1_work_time: number
review2_data_size: number
review2_work_time: number
} | null>(null)
useEffect(() => {
const run = async () => {
const res = await getCurrentPersonData({
date: dayjs().format("YYYY-MM-DD"),
})
setData(res)
}
run()
}, [])
const dataText = useMemo(() => {
return {
label: formatDurationSeconds(data?.label_work_time ?? 0),
review1: formatDurationSeconds(data?.review1_work_time ?? 0),
review2: formatDurationSeconds(data?.review2_work_time ?? 0),
}
}, [data])
return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
<Group align="stretch" gap="md" wrap="nowrap" h={180}>
<Paper
shadow="xs"
withBorder
p="md"
style={{
width: 160,
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
display: "flex",
flexDirection: "column",
gap: 16,
}}>
<Text fz="lg" fw={700}>
</Text>
<Center>
<Paper
radius="md"
p="sm"
style={{
width: 72,
height: 72,
background: "rgba(0, 161, 255, 0.08)",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}>
<Text fz={24} fw={700} lh={1}>
{dayjs().format("DD")}
</Text>
<Text fz={16}>{monthText(dayjs().format("MM"))}</Text>
</Paper>
</Center>
</Paper>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" style={{ flex: 1 }}>
<StatCard
title="标注工时"
timeText={dataText.label}
sizeTitle="标注数据量"
sizeValue={data?.label_data_size ?? 0}
sizeColor="#E03131"
background={DataImage.src}
backgroundLogo={DataLogoImage.src}
Icon={DataIcon}
/>
<StatCard
title="审核工时"
timeText={dataText.review1}
sizeTitle="审核数据量"
sizeValue={data?.review1_data_size ?? 0}
sizeColor="#2F9E44"
background={HourImage.src}
backgroundLogo={HourLogoImage.src}
Icon={HourIcon}
/>
<StatCard
title="复审工时"
timeText={dataText.review2}
sizeTitle="复审数据量"
sizeValue={data?.review2_data_size ?? 0}
sizeColor="#F08C00"
background={ReviewImage.src}
backgroundLogo={ReviewLogoImage.src}
Icon={ReviewIcon}
/>
</SimpleGrid>
</Group>
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md" style={{ flex: 1 }}>
<Paper
shadow="xs"
withBorder
p="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
display: "flex",
flexDirection: "column",
minHeight: 0,
}}>
<div style={{ flex: 1, minHeight: 0 }}>
<PersonalTaskTable />
</div>
</Paper>
<Paper
shadow="xs"
withBorder
p="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
display: "flex",
flexDirection: "column",
minHeight: 0,
}}>
<div style={{ flex: 1, minHeight: 0 }}>
<PersonalProjectTable />
</div>
</Paper>
</SimpleGrid>
</Stack>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,541 @@
"use client"
import {
addDailyWorkData,
editDailyWorkData,
} from "@/components/label/api/daily"
import { Daily } from "@/components/label/api/daily/typing"
import {
useAllUserStore,
usePermissionStore,
} from "@/components/label/store/auth"
import {
Button,
Group,
Modal,
NumberInput,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { notifications } from "@mantine/notifications"
import { useEffect, useMemo } from "react"
import {
accountTypeOpts,
actualTypeOpts,
objTypeOpts,
performanceOpts,
setTypeOpts,
workTypeOpts,
} from "../config"
function normalizeTimeValue(value?: string | null) {
if (!value) return ""
return value.length >= 5 ? value.slice(0, 5) : value
}
function toTimeWithSeconds(value?: string) {
if (!value) return undefined
return value.length === 5 ? `${value}:00` : value
}
type LeaderTreeNode = {
title?: string
value?: string | number
children?: LeaderTreeNode[]
}
function flattenLeaderTree(
nodes: LeaderTreeNode[],
prefixTitle: string[] = []
): Array<{ label: string; value: string }> {
const result: Array<{ label: string; value: string }> = []
nodes.forEach((node) => {
const title = node.title ?? ""
const nextPrefix = title ? [...prefixTitle, title] : prefixTitle
const children = node.children ?? []
const hasChildren = children.length > 0
const valueRaw = node.value
if (hasChildren) {
result.push(...flattenLeaderTree(children, nextPrefix))
return
}
if (valueRaw === undefined || valueRaw === null) return
if (typeof valueRaw === "string" && valueRaw.includes("-")) return
result.push({
label: nextPrefix.join(" / "),
value: String(valueRaw),
})
})
return result
}
function HourRightSection() {
return (
<Text size="xs" c="dimmed">
(H)
</Text>
)
}
export default function DailyReportModal(props: {
opened: boolean
record: Daily.DailyWorkList | null
projectOptions: Array<{ label: string; value: string }>
onCloseAction: (refresh?: boolean) => void
}) {
const { opened, record, onCloseAction, projectOptions } = props
const user_id = usePermissionStore((s) => s.user_id)
const user_name = usePermissionStore((s) => s.user_name)
const treeData = useAllUserStore((s) => s.treeData)
const leaderOptions = useMemo(() => {
return flattenLeaderTree(treeData ?? [])
}, [treeData])
const form = useForm({
initialValues: {
user_name: user_name ?? "",
date: "",
start_time: "",
end_time: "",
set_type: "",
actual_type: "",
project_id: "",
leader_uid: "",
work_type: "",
accounting_type: "",
performance_accounting: "",
obj_type: "",
work_time: undefined as number | undefined,
rate: undefined as number | undefined,
standard_size: undefined as number | undefined,
completed_size: undefined as number | undefined,
residual_size: undefined as number | undefined,
residual_work_time: undefined as number | undefined,
residual_pm: "",
remarks: "",
},
validate: {
date: (v) => (v ? null : "请输入日期"),
// set_type: (v) => (v ? null : "请选择规定类型"),
actual_type: (v) => (v ? null : "请选择出勤类型"),
},
validateInputOnChange: true,
validateInputOnBlur: true,
clearInputErrorOnChange: true,
})
useEffect(() => {
if (!opened) return
if (!record) {
form.setValues({
user_name: user_name ?? "",
date: new Date().toISOString().slice(0, 10),
start_time: "",
end_time: "",
set_type: "",
actual_type: "",
project_id: "",
leader_uid: "",
work_type: "",
accounting_type: "",
performance_accounting: "",
obj_type: "",
work_time: undefined,
rate: undefined,
standard_size: undefined,
completed_size: undefined,
residual_size: undefined,
residual_work_time: undefined,
residual_pm: "",
remarks: "",
})
form.resetDirty()
return
}
form.setValues({
user_name: user_name ?? "",
date: record.date ?? "",
start_time: normalizeTimeValue(record.start_time),
end_time: normalizeTimeValue(record.end_time),
set_type: record.set_type ?? "",
actual_type: record.actual_type ?? "",
project_id:
record.project_id === undefined || record.project_id === null
? ""
: String(record.project_id),
leader_uid:
record.leader_uid === undefined || record.leader_uid === null
? ""
: String(record.leader_uid),
work_type: record.work_type ?? "",
accounting_type: record.accounting_type ?? "",
performance_accounting:
record.performance_accounting === true
? "true"
: record.performance_accounting === false
? "false"
: "",
obj_type:
record.obj_type === undefined || record.obj_type === null
? ""
: String(record.obj_type),
work_time: record.work_time ?? undefined,
rate: record.rate ?? undefined,
standard_size: record.standard_size ?? undefined,
completed_size: record.completed_size ?? undefined,
residual_size: record.residual_size ?? undefined,
residual_work_time: record.residual_work_time ?? undefined,
residual_pm:
record.residual_pm === true
? "正"
: record.residual_pm === false
? "负"
: "",
remarks: record.remarks ?? "",
})
form.resetDirty()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [opened, record, user_name])
useEffect(() => {
const workTime = form.values.work_time
const rate = form.values.rate
if (workTime !== undefined && rate !== undefined) {
form.setFieldValue("standard_size", Number((workTime * rate).toFixed(1)))
} else {
form.setFieldValue("standard_size", undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const standardSize = form.values.standard_size
const completedSize = form.values.completed_size
const workTime = form.values.work_time
if (
standardSize !== undefined &&
completedSize !== undefined &&
workTime !== undefined &&
standardSize !== 0
) {
const residualSize = completedSize - standardSize
const residualWorkTime = Number(
((residualSize / standardSize) * workTime).toFixed(1)
)
form.setFieldValue("residual_size", Number(residualSize.toFixed(1)))
form.setFieldValue("residual_work_time", residualWorkTime)
form.setFieldValue("residual_pm", residualSize >= 0 ? "正" : "负")
} else {
form.setFieldValue("residual_size", undefined)
form.setFieldValue("residual_work_time", undefined)
form.setFieldValue("residual_pm", "")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleSubmit = form.onSubmit(async (values) => {
const uid = typeof user_id === "number" ? user_id : Number(user_id ?? 0)
if (!uid) {
notifications.show({
color: "red",
title: "提交失败",
message: "未获取到用户信息",
})
return
}
const params: Daily.CreateProps = {
uid,
date: values.date,
start_time: toTimeWithSeconds(values.start_time),
end_time: toTimeWithSeconds(values.end_time),
set_type: values.set_type,
actual_type: values.actual_type,
project_id: values.project_id ? Number(values.project_id) : undefined,
leader_uid: values.leader_uid ? Number(values.leader_uid) : undefined,
work_type: values.work_type || undefined,
accounting_type: values.accounting_type || undefined,
performance_accounting:
values.performance_accounting === "true"
? true
: values.performance_accounting === "false"
? false
: undefined,
obj_type: values.obj_type ? Number(values.obj_type) : undefined,
work_time: values.work_time,
rate: values.rate,
standard_size: values.standard_size,
completed_size: values.completed_size,
residual_size: values.residual_size,
residual_work_time: values.residual_work_time,
residual_pm:
values.residual_pm === "正"
? true
: values.residual_pm === "负"
? false
: undefined,
remarks: values.remarks || undefined,
}
try {
if (record?.id) {
await editDailyWorkData(params, record.id)
notifications.show({
color: "green",
title: "操作成功",
message: "已更新日报",
})
} else {
await addDailyWorkData(params)
notifications.show({
color: "green",
title: "操作成功",
message: "已提交日报",
})
}
onCloseAction(true)
form.reset()
} catch (e) {
notifications.show({
color: "red",
title: "操作失败",
message: e instanceof Error ? e.message : "请求失败",
})
}
})
return (
<Modal
opened={opened}
onClose={() => {
form.reset()
onCloseAction()
}}
title={record ? "编辑日报" : "填写日报"}
size={840}
centered
closeOnClickOutside={false}>
<form onSubmit={handleSubmit}>
<Stack gap="sm">
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
<TextInput
label="姓名"
size="xs"
radius="xs"
value={form.values.user_name}
readOnly
/>
<TextInput
label="日期"
size="xs"
radius="xs"
type="date"
withAsterisk
{...form.getInputProps("date")}
/>
<div />
<TextInput
label="开始时间"
size="xs"
radius="xs"
type="time"
{...form.getInputProps("start_time")}
/>
<TextInput
label="结束时间"
size="xs"
radius="xs"
type="time"
{...form.getInputProps("end_time")}
/>
<div />
<Select
label="规定类型"
size="xs"
radius="xs"
withAsterisk
data={setTypeOpts}
searchable
clearable
{...form.getInputProps("set_type")}
/>
<Select
label="出勤类型"
size="xs"
radius="xs"
withAsterisk
data={actualTypeOpts}
searchable
clearable
{...form.getInputProps("actual_type")}
/>
<div />
<Select
label="项目名称"
size="xs"
radius="xs"
data={projectOptions}
searchable
clearable
{...form.getInputProps("project_id")}
/>
<Select
label="任务组长"
size="xs"
radius="xs"
data={leaderOptions}
searchable
clearable
{...form.getInputProps("leader_uid")}
/>
<Select
label="工作类别"
size="xs"
radius="xs"
data={workTypeOpts}
searchable
clearable
{...form.getInputProps("work_type")}
/>
<Select
label="对象类型"
size="xs"
radius="xs"
data={objTypeOpts}
searchable
clearable
{...form.getInputProps("obj_type")}
/>
<Select
label="绩效核算"
size="xs"
radius="xs"
data={performanceOpts}
searchable
clearable
{...form.getInputProps("performance_accounting")}
/>
<Select
label="核算方式"
size="xs"
radius="xs"
data={accountTypeOpts}
searchable
clearable
{...form.getInputProps("accounting_type")}
/>
<NumberInput
label="工时"
size="xs"
radius="xs"
min={0}
decimalScale={1}
allowNegative={false}
hideControls
rightSection={<HourRightSection />}
rightSectionWidth={78}
{...form.getInputProps("work_time")}
/>
<NumberInput
label="额定倍速"
size="xs"
radius="xs"
min={0}
allowNegative={false}
hideControls
{...form.getInputProps("rate")}
/>
<div />
<NumberInput
label="标准量"
size="xs"
radius="xs"
value={form.values.standard_size}
readOnly
hideControls
/>
<NumberInput
label="完成量"
size="xs"
radius="xs"
min={0}
allowNegative={false}
hideControls
{...form.getInputProps("completed_size")}
/>
<div />
<NumberInput
label="余量"
size="xs"
radius="xs"
value={form.values.residual_size}
readOnly
hideControls
/>
<NumberInput
label="余量工时"
size="xs"
radius="xs"
value={form.values.residual_work_time}
readOnly
hideControls
rightSection={<HourRightSection />}
rightSectionWidth={78}
/>
<TextInput
label="余量正负"
size="xs"
radius="xs"
value={form.values.residual_pm}
readOnly
/>
</SimpleGrid>
<TextInput
label="备注"
size="xs"
radius="xs"
{...form.getInputProps("remarks")}
/>
<Group justify="flex-end" gap="sm">
<Button
variant="default"
size="xs"
radius="xs"
onClick={() => {
form.reset()
onCloseAction()
}}>
</Button>
<Button size="xs" radius="xs" type="submit">
</Button>
</Group>
</Stack>
</form>
</Modal>
)
}

View File

@@ -0,0 +1,68 @@
export const setTypeOpts = [
{ label: "出勤", value: "出勤" },
{ label: "周末休", value: "周末休" },
{ label: "节假休", value: "节假休" },
]
export const actualTypeOpts = [
{ label: "出勤", value: "出勤" },
{ label: "周末休", value: "周末休" },
{ label: "节假休", value: "节假休" },
{ label: "调休", value: "调休" },
{ label: "补勤", value: "补勤" },
{ label: "增勤", value: "增勤" },
{ label: "年假", value: "年假" },
{ label: "事假", value: "事假" },
{ label: "病假", value: "病假" },
{ label: "婚假", value: "婚假" },
{ label: "产假", value: "产假" },
{ label: "陪产假", value: "陪产假" },
{ label: "丧假", value: "丧假" },
{ label: "其他", value: "其他" },
]
export const workTypeOpts = [
{ label: "标注", value: "标注" },
{ label: "审核", value: "审核" },
{ label: "复审", value: "复审" },
{ label: "标注返工", value: "标注返工" },
{ label: "审核返工", value: "审核返工" },
{ label: "复审返工", value: "复审返工" },
{ label: "管理", value: "管理" },
]
export const accountTypeOpts = [
{ label: "对象核算", value: "对象核算" },
{ label: "素材核算", value: "素材核算" },
{ label: "会议", value: "会议" },
{ label: "培训", value: "培训" },
{ label: "项目管理", value: "项目管理" },
{ label: "出差", value: "出差" },
{ label: "断电", value: "断电" },
{ label: "网络故障", value: "网络故障" },
{ label: "平台故障", value: "平台故障" },
{ label: "支援行政", value: "支援行政" },
{ label: "公司活动", value: "公司活动" },
{ label: "请假", value: "请假" },
{ label: "其他", value: "其他" },
]
export const performanceOpts = [
{ label: "是", value: "true" },
{ label: "否", value: "false" },
]
export const objTypeOpts = [
{ label: "静态", value: "0" },
{ label: "动态", value: "1" },
]
export const performanceTextMap = new Map<boolean, string>([
[true, "是"],
[false, "否"],
])
export const objTypeTextMap = new Map<number, string>([
[0, "静态"],
[1, "动态"],
])

View File

@@ -1,5 +1,710 @@
"use client"
export default function ReportPage() {
return <>Person Report</>
import {
deleteDailyWorkData,
getSelfDailyWorkList,
} from "@/components/label/api/daily"
import { Daily } from "@/components/label/api/daily/typing"
import { getSelfProjectList } from "@/components/label/api/project"
import { useAllUserStore } from "@/components/label/store/auth"
import {
ActionIcon,
Badge,
Button,
Collapse,
Flex,
Group,
MultiSelect,
Paper,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core"
import { modals } from "@mantine/modals"
import { notifications } from "@mantine/notifications"
import {
IconChevronDown,
IconChevronUp,
IconPencil,
IconPlus,
IconRefresh,
IconSearch,
IconTrash,
} from "@tabler/icons-react"
import dayjs from "dayjs"
import {
DataTable,
DataTableColumn,
DataTableSortStatus,
} from "mantine-datatable"
import { useCallback, useEffect, useMemo, useState } from "react"
import DailyReportModal from "./components/DailyReportModal"
import {
accountTypeOpts,
actualTypeOpts,
objTypeOpts,
objTypeTextMap,
performanceOpts,
performanceTextMap,
setTypeOpts,
workTypeOpts,
} from "./config"
type LeaderTreeNode = {
title?: string
value?: string | number
children?: LeaderTreeNode[]
}
function flattenLeaderTree(
nodes: LeaderTreeNode[],
prefixTitle: string[] = []
): Array<{ label: string; value: string }> {
const result: Array<{ label: string; value: string }> = []
nodes.forEach((node) => {
const title = node.title ?? ""
const nextPrefix = title ? [...prefixTitle, title] : prefixTitle
const children = node.children ?? []
const hasChildren = children.length > 0
const valueRaw = node.value
if (hasChildren) {
result.push(...flattenLeaderTree(children, nextPrefix))
return
}
if (valueRaw === undefined || valueRaw === null) return
if (typeof valueRaw === "string" && valueRaw.includes("-")) return
result.push({
label: nextPrefix.join(" / "),
value: String(valueRaw),
})
})
return result
}
function weekDayText(day: number) {
const list = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
return list[day] ?? "-"
}
function toBoolean(value?: string) {
if (value === "true") return true
if (value === "false") return false
return undefined
}
function toNumber(value?: string) {
if (!value) return undefined
const n = Number(value)
return Number.isFinite(n) ? n : undefined
}
export default function PersonReportPage() {
const treeData = useAllUserStore((s) => s.treeData)
const leaderOptions = useMemo(() => {
return flattenLeaderTree(treeData ?? [])
}, [treeData])
const defaultFilters = useMemo(() => {
return {
date_start: dayjs().subtract(6, "day").format("YYYY-MM-DD"),
date_end: dayjs().format("YYYY-MM-DD"),
set_type: "",
actual_type: "",
leader_uid: [] as string[],
work_type: "",
accounting_type: "",
project_id: [] as string[],
performance_accounting: "",
obj_type: "",
}
}, [])
const [projectOptions, setProjectOptions] = useState<
Array<{ label: string; value: string }>
>([])
const [filters, setFilters] = useState(defaultFilters)
const [appliedFilters, setAppliedFilters] = useState(defaultFilters)
const [queryTrigger, setQueryTrigger] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [totalItems, setTotalItems] = useState(0)
const [loading, setLoading] = useState(false)
const [records, setRecords] = useState<Daily.DailyWorkList[]>([])
const [summary, setSummary] = useState<Daily.Summary | null>(null)
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: "id",
direction: "desc",
})
const [modalOpened, setModalOpened] = useState(false)
const [editingRecord, setEditingRecord] =
useState<Daily.DailyWorkList | null>(null)
const [searchExpanded, setSearchExpanded] = useState(false)
const queryParams = useMemo(() => {
const params: Daily.Request = {
page_number: page,
page_size: pageSize,
flag: 0,
date_start: appliedFilters.date_start || undefined,
date_end: appliedFilters.date_end || undefined,
set_type: appliedFilters.set_type || undefined,
actual_type: appliedFilters.actual_type || undefined,
leader_uid: appliedFilters.leader_uid.length
? appliedFilters.leader_uid
.map((v) => Number(v))
.filter((v) => Number.isFinite(v))
: undefined,
work_type: appliedFilters.work_type || undefined,
accounting_type: appliedFilters.accounting_type || undefined,
project_id: appliedFilters.project_id.length
? appliedFilters.project_id
.map((v) => Number(v))
.filter((v) => Number.isFinite(v))
: undefined,
performance_accounting: toBoolean(appliedFilters.performance_accounting),
obj_type: toNumber(appliedFilters.obj_type),
order_by: `${sortStatus.columnAccessor} ${sortStatus.direction}`,
}
Object.keys(params).forEach((k) => {
const key = k as keyof Daily.Request
const value = params[key]
if (value === "" || value === null) delete params[key]
if (Array.isArray(value) && value.length === 0) delete params[key]
if (value !== 0 && value !== false && value !== "" && !value)
delete params[key]
})
return params
}, [
appliedFilters,
page,
pageSize,
sortStatus.columnAccessor,
sortStatus.direction,
])
const load = useCallback(async () => {
if (!queryTrigger) return
setLoading(true)
try {
const res = await getSelfDailyWorkList(queryParams)
setRecords(res?.daily_work_list ?? [])
setTotalItems(res?.total_items ?? 0)
setSummary(res?.total_summary ?? null)
} catch (e) {
setRecords([])
setTotalItems(0)
setSummary(null)
notifications.show({
color: "red",
title: "加载失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setLoading(false)
}
}, [queryParams, queryTrigger])
useEffect(() => {
load()
}, [load])
useEffect(() => {
queueMicrotask(() => {
setQueryTrigger((v) => v + 1)
})
}, [])
const columns = useMemo(() => {
const cols: DataTableColumn<Daily.DailyWorkList>[] = [
{ accessor: "id", title: "ID", width: 80, sortable: true },
{
accessor: "date",
title: "日期",
width: 120,
sortable: true,
render: (record) => record.date ?? "-",
},
{
accessor: "week",
title: "星期",
width: 90,
render: (record) =>
record.date ? weekDayText(dayjs(record.date).day()) : "-",
},
{
accessor: "set_type",
title: "规定类型",
width: 120,
render: (record) => record.set_type ?? "-",
},
{
accessor: "actual_type",
title: "实际类型",
width: 120,
render: (record) => record.actual_type ?? "-",
},
{ accessor: "leader_name", title: "任务组长", width: 120 },
{ accessor: "work_type", title: "工作类别", width: 140 },
{ accessor: "accounting_type", title: "核算方式", width: 140 },
{
accessor: "project_name",
title: "项目名称",
width: 220,
render: (record) => (
<Text
size="sm"
style={{
maxWidth: 220,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{record.project_name ?? "-"}
</Text>
),
},
{
accessor: "performance_accounting",
title: "绩效核算",
width: 110,
render: (record) => {
if (record.performance_accounting === true)
return <Badge color="green">{performanceTextMap.get(true)}</Badge>
if (record.performance_accounting === false)
return <Badge color="gray">{performanceTextMap.get(false)}</Badge>
return "-"
},
},
{
accessor: "obj_type",
title: "对象类型",
width: 100,
render: (record) => {
if (record.obj_type === 0 || record.obj_type === 1)
return objTypeTextMap.get(record.obj_type) ?? "-"
return "-"
},
},
{ accessor: "start_time", title: "开始时间", width: 120 },
{ accessor: "end_time", title: "结束时间", width: 120 },
{
accessor: "work_time",
title: "工时",
width: 90,
sortable: true,
},
{ accessor: "rate", title: "额定倍率", width: 90 },
{
accessor: "standard_size",
title: "标准量",
width: 90,
sortable: true,
},
{
accessor: "completed_size",
title: "完成量",
width: 90,
sortable: true,
},
{
accessor: "residual_size",
title: "余量",
width: 90,
sortable: true,
},
{
accessor: "residual_work_time",
title: "余量工时",
width: 110,
sortable: true,
},
{
accessor: "remarks",
title: "备注",
width: 200,
render: (record) => (
<Text
size="sm"
style={{
maxWidth: 200,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{record.remarks ?? "-"}
</Text>
),
},
{
accessor: "operation",
title: "操作",
width: 90,
textAlign: "center",
render: (record) => {
const disabled = !!record.lock_status
return (
<Group gap={6} justify="center" wrap="nowrap">
<ActionIcon
variant="subtle"
color="blue"
disabled={disabled}
onClick={() => {
if (disabled) return
setEditingRecord(record)
setModalOpened(true)
}}>
<IconPencil size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
disabled={disabled}
onClick={() => {
if (disabled || !record.id) return
modals.openConfirmModal({
title: "删除日报",
centered: true,
children: (
<Text size="sm">
</Text>
),
labels: { confirm: "删除", cancel: "取消" },
confirmProps: { color: "red" },
onConfirm: async () => {
await deleteDailyWorkData(record.id as number)
notifications.show({
color: "green",
title: "操作成功",
message: "已删除日报",
})
load()
},
})
}}>
<IconTrash size={16} />
</ActionIcon>
</Group>
)
},
},
]
return cols
}, [load])
const summaryText = useMemo(() => {
return {
total_work_time: summary?.total_work_time ?? 0,
total_standard_size: summary?.total_standard_size ?? 0,
total_completed_size: summary?.total_completed_size ?? 0,
total_residual_size: summary?.total_residual_size ?? 0,
total_residual_work_time: summary?.total_residual_work_time ?? 0,
}
}, [summary])
const asyncGetSelfProjectList = useCallback(async () => {
const res = await getSelfProjectList()
setProjectOptions(
(res ?? []).map((r) => ({ label: r.label, value: String(r.value) }))
)
}, [])
useEffect(() => {
queueMicrotask(() => asyncGetSelfProjectList())
}, [asyncGetSelfProjectList])
return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Stack gap="sm">
<Group justify="space-between" wrap="wrap" gap="sm">
<Group gap="xs">
<Text fw={700}></Text>
<Button
size="xs"
variant="transparent"
rightSection={
searchExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)
}
onClick={() => setSearchExpanded((v) => !v)}>
{searchExpanded ? "收起" : "展开"}
</Button>
</Group>
<Group gap="sm">
<Button
variant="default"
size="xs"
radius="xs"
leftSection={<IconRefresh size={16} />}
onClick={() => {
setFilters(defaultFilters)
setAppliedFilters(defaultFilters)
setPage(1)
setQueryTrigger((v) => v + 1)
}}>
</Button>
<Button
size="xs"
radius="xs"
leftSection={<IconSearch size={16} />}
onClick={() => {
setAppliedFilters(filters)
setPage(1)
setQueryTrigger((v) => v + 1)
}}>
</Button>
</Group>
</Group>
<Collapse
in={searchExpanded}
transitionDuration={150}
transitionTimingFunction="ease"
animateOpacity>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
<TextInput
label="开始日期"
size="xs"
radius="xs"
type="date"
value={filters.date_start}
onChange={(e) =>
setFilters((s) => ({
...s,
date_start: e.target.value,
}))
}
/>
<TextInput
label="结束日期"
size="xs"
radius="xs"
type="date"
value={filters.date_end}
onChange={(e) => {
setFilters((s) => ({ ...s, date_end: e.target.value }))
}}
/>
<MultiSelect
label="项目名称"
size="xs"
radius="xs"
data={projectOptions}
searchable
clearable
value={filters.project_id}
onChange={(v) => setFilters((s) => ({ ...s, project_id: v }))}
/>
<MultiSelect
label="任务组长"
size="xs"
radius="xs"
data={leaderOptions}
searchable
clearable
value={filters.leader_uid}
onChange={(v) => setFilters((s) => ({ ...s, leader_uid: v }))}
/>
<MultiSelect
label="规定类型"
size="xs"
radius="xs"
data={setTypeOpts}
searchable
clearable
value={filters.set_type ? [filters.set_type] : []}
onChange={(v) =>
setFilters((s) => ({ ...s, set_type: v[0] ?? "" }))
}
/>
<MultiSelect
label="出勤类型"
size="xs"
radius="xs"
data={actualTypeOpts}
searchable
clearable
value={filters.actual_type ? [filters.actual_type] : []}
onChange={(v) =>
setFilters((s) => ({ ...s, actual_type: v[0] ?? "" }))
}
/>
<MultiSelect
label="工作类别"
size="xs"
radius="xs"
data={workTypeOpts}
searchable
clearable
value={filters.work_type ? [filters.work_type] : []}
onChange={(v) =>
setFilters((s) => ({ ...s, work_type: v[0] ?? "" }))
}
/>
<MultiSelect
label="核算方式"
size="xs"
radius="xs"
data={accountTypeOpts}
searchable
clearable
value={filters.accounting_type ? [filters.accounting_type] : []}
onChange={(v) =>
setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" }))
}
/>
<MultiSelect
label="绩效核算"
size="xs"
radius="xs"
data={performanceOpts}
searchable
clearable
value={
filters.performance_accounting
? [filters.performance_accounting]
: []
}
onChange={(v) =>
setFilters((s) => ({
...s,
performance_accounting: v[0] ?? "",
}))
}
/>
<MultiSelect
label="对象类型"
size="xs"
radius="xs"
data={objTypeOpts}
searchable
clearable
value={filters.obj_type ? [filters.obj_type] : []}
onChange={(v) =>
setFilters((s) => ({ ...s, obj_type: v[0] ?? "" }))
}
/>
</SimpleGrid>
</Collapse>
</Stack>
</Paper>
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
gap: 12,
}}>
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
<Flex justify={"space-between"} align="center" w="100%">
<Button
leftSection={<IconPlus size={12} />}
size="xs"
radius={"xs"}
onClick={() => {
setEditingRecord(null)
setModalOpened(true)
}}>
</Button>
<ActionIcon onClick={load} variant="transparent">
<IconRefresh size={16} />
</ActionIcon>
</Flex>
<DataTable<Daily.DailyWorkList>
width="100%"
style={{ width: "100%" }}
withTableBorder
withRowBorders
pinFirstColumn
pinLastColumn
scrollAreaProps={{ type: "auto" }}
fetching={loading}
records={records}
columns={columns}
totalRecords={totalItems}
recordsPerPage={pageSize}
page={page}
onPageChange={setPage}
onRecordsPerPageChange={(v) => {
setPageSize(v)
setPage(1)
}}
recordsPerPageOptions={[10, 15, 20, 50]}
noRecordsText="暂无数据"
recordsPerPageLabel="当前页数"
sortStatus={sortStatus}
onSortStatusChange={(s) => {
setSortStatus(s)
setPage(1)
}}
/>
<Paper withBorder radius="xs" p="sm">
<Group justify="space-between" wrap="wrap">
<Text size="sm" fw={700}>
</Text>
<Group gap="md">
<Text size="sm">{summaryText.total_work_time}</Text>
<Text size="sm">{summaryText.total_standard_size}</Text>
<Text size="sm">
{summaryText.total_completed_size}
</Text>
<Text size="sm">{summaryText.total_residual_size}</Text>
<Text size="sm">
{summaryText.total_residual_work_time}
</Text>
</Group>
</Group>
</Paper>
</Stack>
</Paper>
<DailyReportModal
opened={modalOpened}
record={editingRecord}
projectOptions={projectOptions}
onCloseAction={(refresh) => {
setModalOpened(false)
setEditingRecord(null)
if (refresh) load()
}}
/>
</Stack>
)
}

View File

@@ -0,0 +1,4 @@
export const BASE_LABEL_API =
process.env.NEXT_PUBLIC_ENV === "production"
? "https://label.cowarobot.com"
: "https://label.softtest.cowarobot.com"

View File

@@ -0,0 +1,49 @@
import httpFetch from "@/api/fetch"
import { Daily } from "./typing"
import { BASE_LABEL_API } from "../const"
export const getSelfDailyWorkList = (data: Daily.Request) => {
return httpFetch<Daily.Response>({
url: BASE_LABEL_API + "/api/v1/label_server/daily_work/list/self",
method: "POST",
body: JSON.stringify(data),
})
}
export const getTeamDailyWorkList = (data: Daily.Request) => {
return httpFetch<Daily.Response>({
url: BASE_LABEL_API + "/api/v1/label_server/daily_work/list/team",
method: "POST",
body: JSON.stringify(data),
})
}
export const addDailyWorkData = (data: Daily.CreateProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/daily_work/add",
method: "POST",
body: JSON.stringify(data),
})
}
export const editDailyWorkData = (data: Daily.CreateProps, id: number) => {
return httpFetch({
url: BASE_LABEL_API + `/api/v1/label_server/daily_work/edit/${id}`,
method: "put",
body: JSON.stringify(data),
})
}
export const deleteDailyWorkData = (id: number) => {
return httpFetch({
url: BASE_LABEL_API + `/api/v1/label_server/daily_work/delete/${id}`,
method: "delete",
})
}
export const lockDailyWorkData = (lock_status: boolean, id: number) => {
return httpFetch({
url: BASE_LABEL_API + `/api/v1/label_server/daily_work/lock/${id}`,
method: "put",
body: JSON.stringify({ lock_status }),
})
}

View File

@@ -0,0 +1,84 @@
export namespace Daily {
export interface Request {
date_end?: string
date_start?: string
/**
* 0-个人日报 1-团队日报
*/
flag?: number
name?: string
order_by?: string
page_number?: number
page_size?: number
[property: string]: any
}
export interface Summary {
total_work_time: number
total_standard_size: number
total_completed_size: number
total_residual_size: number
total_residual_work_time: number
}
export interface Response {
daily_work_list: DailyWorkList[]
total_items: number
total_pages: number
total_summary: Summary
[property: string]: any
}
export interface DailyWorkList {
accounting_type?: string | null
actual_type?: string
completed_size?: number
date?: string
end_time?: string
id?: number
leader_name?: string | null
leader_uid?: number | null
lock_status?: boolean
obj_type?: number | null
performance_accounting?: boolean | null
project_id?: number | null
project_name?: string | null
rate?: number
remarks?: string | null
residual_pm?: boolean | null
residual_size?: number
residual_work_time?: number
set_type?: string
standard_size?: number
start_time?: string
uid?: number
username?: string
work_time?: number
work_type?: string | null
[property: string]: any
}
export interface CreateProps {
accounting_type?: string
actual_type: string
completed_size?: number
date: string
end_time?: string
leader_uid?: number
obj_type?: number
performance_accounting?: boolean
project_id?: number
rate?: number
remarks?: string
residual_pm?: boolean
residual_size?: number
residual_work_time?: number
set_type: string
standard_size?: number
start_time?: string
uid: number
work_time?: number
work_type?: string
[property: string]: any
}
}

View File

@@ -0,0 +1,171 @@
export interface LabelResult {
data_name: string
image_objects?: ImageObjects
video_objects?: {
[key: string]: ImageObjects
}
pc_box_objects?: { [key: string]: any }[]
pc_segment_objects?: { [key: string]: any }[]
pc_vegetation_objects?: { [key: string]: any }[]
}
export interface ImageObjects {
annotations: Annotation[]
categories: Category[]
desc?: Desc
image_groups: ImageGroups
images: Images
key_frame?: boolean
label1_ts?: number
label1_uid?: number
review1_ts?: number
review1_uid?: number
review2_ts?: number
review2_uid?: number
}
export interface Annotation {
bbox?: number[]
category_id: number
comment?: Comment[]
create_timestamp: number
first_modified_timestamp: number
first_modified_uid: number
hollow_segmentation?: Array<Array<number[]>>
id: number
image_id: number
key_points?: KeyPoint[]
last_modified_timestamp: number
last_modified_uid: number
line_segment?: Array<Array<number[]>>
prelabel: boolean
rect?: number[]
segmentation?: Array<Array<number[]>>
sub_attributes?: { [key: string]: any }
uid: number
}
export interface Comment {
check_box_comments: string[]
comment_type: number
date: number
scene_info: SceneInfo
text_comments: string[]
times: number
turn: number
uid: number
}
export interface SceneInfo {
phi?: number
point_idx?: number[]
theta?: number
type: number
vfov_factor?: number
x?: number
x_offset?: number
y?: number
y_offset?: number
z?: number
zoom?: number
}
export interface KeyPoint {
attributes?: { [key: string]: any }
point: [number, number]
}
export interface Category {
id: number
name: string
supercategory: string
}
export interface Desc {
meta_operation: string[]
other_desc: string
}
export interface ImageGroups {
// "01J4GZMQ2G8KKQVF52S6EXBGRA": string;
}
export interface Images {
file_name: string
height: number
id: number
width: number
}
export interface RequestLabelResult {
label_object_size: { [key: string]: any }
results: LabelResult[]
task_id: number
uid: number
work_load: WorkLoad
work_time: { [key: string]: any }
}
export interface WorkLoad {
comment_size: number
dynamic_size: { [key: number]: number }
key_frame_size: { [key: number]: number }
object_size: { [key: number]: number }
prelabel_modify_size: { [key: number]: number }
question_size: { [key: number]: number }
review_dynamic_size: { [key: number]: number }
review_key_frame_size: { [key: number]: number }
review_question_size: { [key: number]: number }
review_size: number
review_static_size: { [key: number]: number }
static_size: { [key: number]: number }
work_time: number
}
export interface ServerResponse {
data_list?: DataList[]
proxy?: string
[property: string]: any
}
export interface DataList {
data_name: string
data_type: string
image_data?: string
video_data?: string
pointcloud_data?: PointcloudDatum[]
[property: string]: any
}
export interface PointcloudDatum {
intensity: number
time_offset: number
x: number
y: number
z: number
[property: string]: any
}
export interface AuxiliaryParams {
project_id: number
file_name: string
image_hw: [number, number]
points_and_box: Array<[number, number]>
tags: number[]
clear_previous_state?: boolean
}
interface UserInfo {
uid: number
name: string
all_projects: { [x: number]: number[] }
collect_projects: number[]
group_name: string
work_status: number
base_city: string
}
export interface LoginInfo extends UserInfo {
token: string
refresh_token: string
}

View File

@@ -0,0 +1,198 @@
import httpFetch from "@/api/fetch"
import { Project, ProjectDetail, ReviewUsers } from "./typing"
import { BASE_LABEL_API } from "../const"
// 获取项目列表
export const getProjectList = (data: Project.ListRequest) => {
return httpFetch<Project.ListResponse>({
url: BASE_LABEL_API + "/api/v1/label_server/project/list",
method: "POST",
body: JSON.stringify(data),
})
}
// 获取项目详情
export const getProjectDetailById = (id: number) => {
return httpFetch<ProjectDetail.DataProps>({
url: BASE_LABEL_API + `/api/v1/label_server/project/detail/${id}`,
method: "GET",
})
}
// 校验项目源数据
export const checkLabelDataSource = (data: Project.CheckRequest) => {
return httpFetch<Project.CheckResponse>({
url: BASE_LABEL_API + "/api/v1/label_server/project/check_data",
method: "POST",
body: JSON.stringify(data),
})
}
// 创建项目
export const projectAdd = (data: Project.CreateProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/add",
method: "POST",
body: JSON.stringify(data),
})
}
// 审核项目
export const projectReview = (data: Project.CreateProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/review",
method: "POST",
body: JSON.stringify(data),
})
}
// 配置项目
export const projectConfig = (data: Project.ConfigProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/config",
method: "POST",
body: JSON.stringify(data),
})
}
// 编辑项目
export const projectEditById = (data: any, id: number) => {
return httpFetch({
url: BASE_LABEL_API + `/api/v1/label_server/project/edit/${id}`,
method: "POST",
body: JSON.stringify(data),
})
}
// 审核试标项目
export const projectPreLabelReview = (data: Project.PreLabelProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/prototype_review",
method: "POST",
body: JSON.stringify(data),
})
}
// 提交验收
export const projectDeliver = (data: { project_id: number }) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/deliver",
method: "POST",
body: JSON.stringify(data),
})
}
// 完成项目
export const projectAccept = (data: { project_id: number; pass: boolean }) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/accept",
method: "POST",
body: JSON.stringify(data),
})
}
// 收藏项目
export const projectCollect = (data: {
project_id: number
is_collect: boolean
}) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/project/collect",
method: "POST",
body: JSON.stringify(data),
})
}
// 获取项目抽检比例
export const getReviewScaleList = (params: { project_id: number }) => {
return httpFetch<any[]>({
url: BASE_LABEL_API + "/api/v1/label_server/review_scale/list",
method: "GET",
data: params,
})
}
// 保存抽检比例
export const saveUserReviewScale = (data: Project.ReviewScaleProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/review_scale/save",
method: "POST",
body: JSON.stringify(data),
})
}
export const getProjectDetail = (id: number) => {
return httpFetch<Project.DetailResponse>({
url: BASE_LABEL_API + `/api/v1/label_server/project/detail/${id}`,
method: "GET",
})
}
// 重新生成Embedding
export const regenerateProjectEmbedding = (data: { project_id: number }) => {
return httpFetch({
url: BASE_LABEL_API + `/api/v1/label_server/project/regenerate_embedding`,
method: "POST",
body: JSON.stringify(data),
})
}
// 获取个人参与项目
export const getPersonProjectDashBoard = () => {
return httpFetch<Array<Project.PersonProjectDashboardResponseItem>>({
url:
BASE_LABEL_API + `/api/v1/label_server/data_statistics/personal_project`,
method: "GET",
})
}
// 获取个人看板数据
export const getCurrentPersonData = (params: { date: string }) => {
return httpFetch<{
label_data_size: number
label_work_time: number
review1_data_size: number
review1_work_time: number
review2_data_size: number
review2_work_time: number
}>({
url:
BASE_LABEL_API +
"/api/v1/label_server/data_statistics/personal_dashboard",
method: "GET",
data: params,
})
}
/* ****************************** Options ****************************** */
// 获取项目所属业务列表
export const getProjectTypeList = () => {
return httpFetch<string[]>({
url: BASE_LABEL_API + "/api/v1/label_server/project/type_list",
method: "GET",
})
}
// 获取项目管理员列表
export const getProjectAdminList = () => {
return httpFetch<Array<{ label: string; value: number }>>({
url: BASE_LABEL_API + "/api/v1/label_server/project/admin_list",
method: "GET",
})
}
// 获取项目审核员
export const getProjectReviewers = () => {
return httpFetch<ReviewUsers>({
url: BASE_LABEL_API + "/api/v1/label_server/project/reviewer_map",
method: "GET",
})
}
// 获取用户有权限的项目
export const getSelfProjectList = () => {
return httpFetch<Array<{ label: string; value: number }>>({
url: BASE_LABEL_API + "/api/v1/label_server/project/self",
method: "GET",
})
}

View File

@@ -0,0 +1,474 @@
export namespace Project {
export interface DataProps {
key: React.Key
id: number // 项目ID
name: string // 项目名称
is_collect: boolean // 是否收藏
project_type?: string // 所属业务
label_schema_name?: string // 标注方案
label_schema_version?: string
status: number // 项目状态
labeled_scale?: number // 标注完成比例
reviewed_scale?: number // 审核完成比例
data_size?: number // 总数据量
rate_speed?: number // 额定倍速
create_user: string // 创建人
owner: string // 项目负责人
label_process?: string[] // 工序
admin_user: number[] // 标注管理员id
data_source?: string // 数据源
remarks?: string // 备注
// reviewer_user?: string; // 审核人
// reviewer_comment?: string; // 审核意见
create_at?: string // 创建时间
update_at?: string // 更新时间
modify?: number //0=可以发起变更,无变更 1=正在变更,无变更 2=可以发起变更,有变更 3=正在变更,有变更
}
export interface CheckRequest {
project_type: string
nfs_path: string
flag: number
}
export interface CheckResponse {
data_size: number
error_code: number
}
export interface CreateProps {
admin_user: string[]
create_user: string
data_size: number
data_source: string
label_schema_name: string
label_schema_version: string
project_name: string
project_type: string
related_task?: number[]
remarks?: string
[property: string]: any
}
export interface ReviewProps {
pass: boolean
project_id: number
project_name: string
review_comment?: string
review_user: string
test_proportion?: number
}
export interface ListRequest {
admin_user?: string
create_user?: string
end_date?: string
page_number: number
page_size: number
project_id?: number
project_name?: string
project_type?: string
review_user?: string
start_date?: string
status?: number[]
[property: string]: any
}
export interface ListResponse {
project_list: DataProps[]
total_pages: number
total_items: number
}
export interface LabelConfig {
acquire_mode: number
acquire_size: number
avg_remind_threshold: number
dynamic_label_speed: number
filter_pre_label: boolean
fix_remind_threshold: number
label_accuracy_rate: number
label_update_month: number
max_size: number
pre_label_confirm: boolean
static_label_speed: number
}
export interface Review1Config {
acquire_mode: number
acquire_size: number
dynamic_review1_speed: number
end_date?: string
filter_label_first_sub: boolean
filter_objs: boolean
filter_rework: boolean
filter_task_batch: boolean
filter_track_user: boolean
is_rework?: boolean
label_first_sub_end_date?: string
label_first_sub_start_date?: string
max_objs?: number
max_size: number
min_objs?: number
review1_accuracy_rate: number
review1_update_month: number
start_date?: string
static_review1_speed: number
track_user_list?: number[]
}
export interface Review2Config {
acquire_mode: number
acquire_size: number
dynamic_review2_speed: number
end_date?: string
filter_objs: boolean
filter_review1_first_sub: boolean
filter_review1_user: boolean
filter_rework: boolean
filter_task_batch: boolean
filter_track_user: boolean
is_rework?: boolean
max_objs?: number
max_size: number
min_objs?: number
review1_first_sub_end_date?: string
review1_first_sub_start_date?: string
review1_user_list?: number[]
review2_update_month: number
start_date?: string
static_review2_speed: number
track_user_list?: number[]
}
export interface ConfigProps {
admin_user?: number[]
check_frame?: boolean
check_task?: boolean
comment_rule?: { [key: string]: any }
default_pose?: string
is_lock_object?: boolean
label_config?: LabelConfig
label_users?: number[]
modifiable_types?: string[]
project_id: number
project_name?: string
project_remark?: string
review1_config?: Review1Config
review1_users?: number[]
review2_config?: Review2Config
review2_users?: number[]
user_grade?: { [key: string]: any }
}
// 试标审核
export interface PreLabelProps {
id: number
review_user: string
action: number // 1-试标完成2-试标通过3-试标不通过
detail?: string
}
export interface ReviewScaleProps {
frame_scale?: number
grade?: string
project_id: number
task_scale?: number
uids: number[]
}
/**
* DetailResponse
*/
export interface DetailResponse {
admin_user: string[]
alarm_threshold: AlarmThreshold
check_frame: boolean
check_task: boolean
comment_rule: CommentRule
create_at: string
create_user: string
data_size: number
default_pose: null
id: number
is_lock_object: boolean
label_process: LabelProcess
label_schema_list: LabelSchemaList[]
label_type: number
labeled_multiplier: LabeledMultiplier
modifiable_types: string[]
name: string
project_progress: ProjectProgress
remarks: string
rpath: string
status: number
task_size: number
user_grade: UserGrade
}
export interface AlarmThreshold {
avg_remind_threshold: number
fix_remind_threshold: number
}
export interface CommentRule {
check_box_rules: {
create_date: string
create_user: string
text: string
}[]
text_rules: string[]
}
export interface LabelProcess {
filter_pre_label: boolean
filter_task_info: string[]
pre_label_confirm: boolean
step_detail: StepDetail[]
}
export interface StepDetail {
acquire_mode: number
acquire_size: number
action: number
max_size: number
user_list: number[]
}
export interface LabelSchemaList {
bbox_range: number[]
bbox_size: number[]
bbox_type: number
category_id: number
color: [number, number, number]
label_class: string
label_type: number
radius: number
sensor_list: string[]
sub_attributes: string[]
sub_attributes_describe: SubAttributesDescribe[]
super_category: string
}
export interface SubAttributesDescribe {
chinese_name: string
english_name: string
isrequired: number
optional_item: string[]
round_id: number
select_mode: number
}
export interface LabeledMultiplier {
dynamic_label_speed: null
dynamic_review1_speed: null
dynamic_review2_speed: null
label_accuracy_rate: null
label_update_month: null
review1_accuracy_rate: null
review1_update_month: null
review2_update_month: null
static_label_speed: null
static_review1_speed: null
static_review2_speed: null
}
export interface ProjectProgress {
normal_labeled_task_size: number
normal_reviewed1_task_size: number
normal_reviewed2_task_size: number
rework_labeled_task_size: number
rework_reviewed1_task_size: number
rework_reviewed2_task_size: number
unlabeled_task_size: number
}
export interface UserGrade {
A: {
frame: number
task: number
}
B: {
frame: number
task: number
}
C: {
frame: number
task: number
}
}
/**
* DetailResponse
*/
export interface PersonProjectDashboardResponseItem {
project_id: number
project_label_type: string
project_name: string
project_type: string
work_time: number
}
}
export interface ReviewUsers {
reviewer1: string[]
reviewer2: string[]
reviewer3: string[]
}
export namespace ProjectDetail {
export interface DataProps {
admin_user: number[]
alarm_threshold: AlarmThreshold
check_frame: boolean
check_task: boolean
comment_rule: CommentRule
create_at: string
create_user: string
data_size: number
default_pose: null
id: number
is_lock_object: boolean
label_process: LabelProcess
label_schema_list: LabelSchemaList[]
label_type: number
labeled_multiplier: LabeledMultiplier
modifiable_types: string[]
name: string
project_progress: ProjectProgress
remarks: string
rpath: string
status: number
task_size: number
user_grade: UserGrade
[property: string]: any
}
export interface AlarmThreshold {
avg_remind_threshold: number
fix_remind_threshold: number
[property: string]: any
}
export interface CommentRule {
check_box_rules: string[]
text_rules: string[]
[property: string]: any
}
export interface LabelProcess {
filter_pre_label: boolean
filter_task_info: FilterTaskInfo[]
pre_label_confirm: boolean
step_detail: StepDetail[]
[property: string]: any
}
export interface FilterTaskInfo {
action: number
end_date: string
filter_label_first_sub: boolean
filter_objs: boolean
filter_review1_first_sub: boolean
filter_review1_user: boolean
filter_rework: boolean
filter_task_batch: boolean
filter_track_user: boolean
is_rework: boolean
label_first_sub_end_date: string
label_first_sub_start_date: string
max_objs: number
min_objs: number
review1_first_sub_end_date: string
review1_first_sub_start_date: string
review1_user_list: number[]
start_date: string
track_user_list: number[]
[property: string]: any
}
export interface StepDetail {
acquire_mode: number
acquire_size: number
action: number
max_size: number
user_list: number[]
[property: string]: any
}
export interface LabelSchemaList {
bbox_range: number[]
bbox_size: number[]
bbox_type: number
category_id: number
color: number[]
label_class: string
label_type: number
radius: number
sensor_list: string[]
sub_attributes: string[]
sub_attributes_describe: SubAttributesDescribe[]
super_category: string
[property: string]: any
}
export interface SubAttributesDescribe {
chinese_name: string
english_name: string
isrequired: number
optional_item: string[]
round_id: number
select_mode: number
[property: string]: any
}
export interface LabeledMultiplier {
dynamic_label_speed: number
dynamic_review1_speed: number
dynamic_review2_speed: number
label_accuracy_rate: number
label_update_month: number
review1_accuracy_rate: number
review1_update_month: number
review2_update_month: number
static_label_speed: number
static_review1_speed: number
static_review2_speed: number
[property: string]: any
}
export interface ProjectProgress {
normal_labeled_task_size: number
normal_reviewed1_task_size: number
normal_reviewed2_task_size: number
rework_labeled_task_size: number
rework_reviewed1_task_size: number
rework_reviewed2_task_size: number
unlabeled_task_size: number
[property: string]: any
}
export interface UserGrade {
A: A
B: B
C: C
[property: string]: any
}
export interface A {
frame: number
task: number
[property: string]: any
}
export interface B {
frame: number
task: number
[property: string]: any
}
export interface C {
frame: number
task: number
[property: string]: any
}
}

View File

@@ -0,0 +1,77 @@
import httpFetch from "@/api/fetch"
import { Task } from "./typing"
import { BASE_LABEL_API } from "../const"
// 获取任务列表
export const getTaskList = (data: Task.ListRequest) => {
return httpFetch<Task.ListResponse>({
url: BASE_LABEL_API + "/api/v1/label_server/task/list",
method: "POST",
body: JSON.stringify(data),
})
}
// 领取任务
export const taskAcquire = (data: Task.AcquireReqProps) => {
return httpFetch<Task.ListResponse>({
url: BASE_LABEL_API + "/api/v1/label_server/task/acquire",
method: "POST",
body: JSON.stringify(data),
})
}
// 调度任务
export const taskDispatch = (data: Task.DispatchReqProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/task/dispatch",
method: "POST",
body: JSON.stringify(data),
})
}
// 释放任务
export const taskRelease = (data: Task.ReleaseReqProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/task/release",
method: "POST",
body: JSON.stringify(data),
})
}
// 获取下一个任务
export const getNextTaskId = (data: Task.NextReqProps) => {
return httpFetch<Task.NextResProps>({
url: BASE_LABEL_API + "/api/v1/label_server/task/next",
method: "POST",
body: JSON.stringify(data),
})
}
// 提交任务
export const taskCommit = (data: Task.CommitReqProps) => {
return httpFetch({
url: BASE_LABEL_API + "/api/v1/label_server/task/commit",
method: "POST",
body: JSON.stringify(data),
})
}
// 获取任务的工作流程
export const getTaskWorkFlow = (id: number) => {
return httpFetch<any[]>({
url: BASE_LABEL_API + `/api/v1/label_server/task/workflow/${id}`,
method: "GET",
})
}
// 获取个人任务列表
export const getUserTaskList = (params: {
page_size: number
page_number: number
}) => {
return httpFetch<Task.ResGetUserTaskList>({
url: BASE_LABEL_API + `/api/v1/label_server/task/list_by_user`,
method: "GET",
data: params,
})
}

View File

@@ -0,0 +1,179 @@
export namespace Task {
export interface DataProps {
id: number
create_date: string
data_type?: number
data_name: {
name: [string]
related_images: []
}[]
project_id: number
project_name: string
project_uuid: string
label_status: number
current_uid?: number
current_username?: string
label_user?: number
label_username?: string
review_user1?: number
review1_username?: string
review_user2?: number
review2_username?: string
rejected: boolean
work_time: {
label_work_time: number
first_review_work_time: number
second_review_work_time: number
}
label_object_size: {
object_size: number
valid_size: number
first_comment_size: number
second_comment_size: number
new_size: number
prelabel_size: number
prelabel_modify_size: number
dynamic_size: number
static_size: number
review_size: number
review_dynamic_size: number
review_static_size: number
key_frame_size: number
question_size: number
total_qa_group_size: number
single_qa_group_size: number
multiple_qa_group_size: number
new_qa_group_size: number
new_single_qa_group_size: number
new_multiple_qa_group_size: number
review_question_size: number
comment_question_size: number
}
first_commit_label_time?: string
first_commit_review1_time?: string
reject_times: number
review_rate: {
review1_rate: number
review2_rate: number
}
label_first_work_time: number
review1_first_work_time: number
review2_first_work_time: number
first_reject_label_time?: string
}
export interface ListRequest {
data_type?: number
current_uid?: number[]
data_name?: string
first_commit_label_time_end?: string
first_commit_label_time_start?: string
first_commit_review1_time_end?: string
first_commit_review1_time_start?: string
get_data: boolean
id?: number[]
label_status?: number[]
label_user?: number[]
order_by?: string
page_number: number
page_size: number
project_id?: number[]
rejected?: number
review_user1?: number[]
review_user2?: number[]
}
export interface SizeProps {
object_size: number
valid_size: number
first_comment_size: number
second_comment_size: number
new_size: number
prelabel_size: number
prelabel_modify_size: number
dynamic_size: number
static_size: number
review_size: number
review_dynamic_size: number
review_static_size: number
key_frame_size: number
question_size: number
total_qa_group_size: number
single_qa_group_size: number
multiple_qa_group_size: number
new_qa_group_size: number
new_single_qa_group_size: number
new_multiple_qa_group_size: number
review_question_size: number
comment_question_size: number
}
export interface ListResponse {
task_list: DataProps[]
total_items: number
total_pages: number
total_size: SizeProps
}
export interface AcquireReqProps {
uid: number
project_id: number
label_action: 1 | 2 | 3 // 1-领取标注任务 2-领取审核任务 3-领取复审任务
}
export interface DispatchReqProps {
operation_uid: number
reject: boolean
task_ids: number[]
task_status_dst: number
task_status_src: number
uid_dst: number | null
uid_src: number | null
}
export interface ReleaseReqProps {
operation_uid: number
reject: boolean
task_ids: number[]
task_status: number
uid: number
}
export interface NextReqProps {
project_id: number
task_status: number
rejected: boolean
last_task_id: number
}
export interface NextResProps {
project_id: number
task_id: number | null
}
export interface CommitReqProps {
task_id: number
uid: number
commit_action: number //1-提交标注 2-提交审核1 3-提交审核2 4-驳回 5-无法标注
work_time: number
comment_question_size?: number
}
export interface ResGetUserTaskList {
simple_task_list: SimpleTaskItem[]
total_items: number
total_pages: number
}
export interface SimpleTaskItem {
id: number
label_status: number
label_type: number
label_username?: string
project_id: number
project_name: string
rejected: boolean
review1_username?: string
review2_username?: string
}
}

605
components/label/store.ts Normal file
View File

@@ -0,0 +1,605 @@
import { del, get, set } from "idb-keyval"
import superjson from "superjson"
import { create } from "zustand"
import { persist, PersistStorage, StorageValue } from "zustand/middleware"
import { Comment } from "./api/label/typing"
import { safeClone } from "./utils/clone"
type LabelData = Map<string, Map<string, [number, any[], any, any[]][]>>
type LabelPathTuple = [number, any[], any, any[]]
type LabelTime = {
label: number
review1: number
review2: number
}
const isPaperPointLike = (value: any) => {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
typeof value.x === "number" &&
typeof value.y === "number"
)
}
const isPaperSegmentLike = (value: any) => {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
value.point &&
isPaperPointLike(value.point)
)
}
const normalizePointCollection = (value: any): any => {
if (Array.isArray(value)) {
return value.map((item) => normalizePointCollection(item))
}
if (isPaperSegmentLike(value)) {
return [value.point.x, value.point.y]
}
if (isPaperPointLike(value)) {
return [value.x, value.y]
}
return value
}
const normalizeLabelPathTuple = (path: LabelPathTuple): LabelPathTuple => {
return [
path[0],
normalizePointCollection(path[1]),
path[2],
normalizePointCollection(path[3]),
]
}
interface LabelState {
// pathId pathPoints detail hollowPathPoints
label: LabelData
labelTime: LabelTime
labelDefaultComments: Comment[]
setLabel: (label: LabelData) => void
stateStack: LabelData[]
setStateStack: (data: LabelData[]) => void
popStateStack: () => LabelData | null
pushStateStack: (newData: LabelData) => void
getLabelDetailDataByKeys: (
imageId: string,
categoryId: string,
id: number
) => any
updateLabelDetailDataByKeys: (
imageId: string,
categoryId: string,
id: number,
newDetail: any
) => void
addDetail: (
imageId: string,
categoryId: string,
id: number,
obj: {
[keys: string]: any
}
) => void
setLabelTime: (
labelTime: LabelTime | ((prev: LabelTime) => LabelTime)
) => void
setLabelDefaultComments: (arr: Comment[]) => void
setImage: (
imageId: string,
operation: Map<string, [number, any[], any, any[]][]>
) => void
setOperation: (
imageId: string,
operationId: string,
path: [number, any[], any, any[]]
) => void
deleteOperation: (
imageId: string,
operationId: string,
pathId: number
) => void
resetLabel: () => void
}
interface ObjectState {
pathStatus: {
// key = imageId + pathId
[key: string]: {
["HIDE"]?: boolean
}
}
selectedPath: {
// key = imageId
[key: string]: number[]
}
operationStatus: {
// key = imageId + operationId
[key: string]: {
["HIDE"]?: boolean
["INPUT"]?: string
["COLLAPSE"]?: boolean
}
}
groupStatus: {
// key = imageId + groupId
[key: string]: {
["COLLAPSE"]?: boolean
}
}
imageStatus: {
// key = imageId
[key: string]: {
["INPUT"]?: string
["COLLAPSE"]?: boolean
["FILTERCOLOR"]?: {
[key: string]: boolean
}
}
}
updatePathStatus: (
id: string,
statusKey: "HIDE",
statusValue: boolean
) => void
updateSelectedPath: (
id: string,
type: "ADD" | "DELETE" | "ADDSOME",
pathValue?: number
) => void
resetPathAndOperationStatus: () => void
updateOperationStatus: (
id: string,
statusKey: "HIDE" | "COLLAPSE" | "INPUT",
statusValue: string | boolean
) => void
updateGroupStatus: (
id: string,
statusKey: "COLLAPSE",
statusValue: string | boolean
) => void
updateImageStatus: (
id: string,
statusKey: "INPUT" | "COLLAPSE" | "FILTERCOLOR",
statusValue:
| string
| boolean
| {
[key: string]: boolean
}
) => void
}
interface KeyEventState {
shift: boolean
focusInput: boolean
setShift: (value: boolean) => void
setFocusInput: (value: boolean) => void
}
const initialLabelState = {
label: new Map(),
labelTime: {
label: 0,
review1: 0,
review2: 0,
},
}
export const initialDetail = {
comment: [],
create_timestamp: 0,
first_modified_timestamp: 0,
first_modified_uid: 0,
last_modified_timestamp: 0,
last_modified_uid: 0,
sub_attributes: {},
prelabel: false,
}
// 自定义 storage 对象
export const storage: PersistStorage<any> = {
getItem: async (name: string) => {
// console.log(name, "has been retrieved");
let str = await get(name)
if (!str) return null
return superjson.parse(str) || null
},
setItem: async (name: string, value: StorageValue<any>): Promise<void> => {
// console.log(name, "with value", value, "has been saved");
await set(name, superjson.stringify(value))
},
removeItem: async (name: string): Promise<void> => {
console.log(name, "has been deleted")
await del(name)
},
}
// store path data
export const useLabelStore = create(
persist<LabelState>(
(storeSet) => ({
label: initialLabelState.label,
labelTime: initialLabelState.labelTime,
labelDefaultComments: [],
stateStack: [],
setLabel: (label) =>
storeSet((state) => {
return {
...state,
label,
}
}),
setStateStack: (data) =>
storeSet((state) => {
return {
...state,
stateStack: data,
}
}),
popStateStack: () => {
const arr: LabelData[] = useLabelStore.getState().stateStack
console.log("弹出栈", arr)
if (!arr.length) return null
if (arr.length === 1) {
return arr[0]
} else {
arr.pop()
storeSet((state) => {
return {
...state,
stateStack: arr,
}
})
return arr[arr.length - 1]
}
},
pushStateStack: (newData) => {
const arr: LabelData[] = useLabelStore.getState().stateStack
if (arr.length === 10) {
arr.shift()
}
arr.push(safeClone(newData))
console.log(arr)
return storeSet((state) => {
return {
...state,
stateStack: arr,
}
})
},
getLabelDetailDataByKeys: (
imageId: string,
categoryId: string,
id: number
) => {
const data = useLabelStore.getState().label
if (!data.has(imageId)) return null
const image = data.get(imageId)
if (!image?.has(categoryId)) return null
const category = image.get(categoryId)
let detail = null
category?.map((item) => {
if (item[0] === id) detail = item[2]
})
return detail
},
updateLabelDetailDataByKeys: (imageId, categoryId, id, newData) => {
const labelData = useLabelStore.getState().label
if (!labelData.has(imageId)) return
const image = labelData.get(imageId)
if (!image?.has(categoryId)) return
const category = image.get(categoryId)
category?.map((item) => {
if (item[0] === id) item[2] = newData
})
},
addDetail: (imageId, categoryId, id, obj) => {
const labelData = useLabelStore.getState().label
if (!labelData.has(imageId)) return
const image = labelData.get(imageId)
if (!image?.has(categoryId)) return
const category = image.get(categoryId)
category?.map((item) => {
if (item[0] === id) item[2] = Object.assign(item[2], obj)
})
},
setLabelTime: (labelTime) =>
storeSet((state) => {
return {
...state,
labelTime:
typeof labelTime === "function"
? labelTime(state.labelTime)
: labelTime,
}
}),
setLabelDefaultComments: (labelDefaultComments) =>
storeSet((state) => {
return {
...state,
labelDefaultComments,
}
}),
setImage: (imageId, operation) =>
storeSet((state) => {
state.label.set(imageId, operation)
return state
}),
setOperation: (imageId, operationId, path) =>
storeSet((state) => {
const normalizedPath = normalizeLabelPathTuple(path)
const labelData = useLabelStore.getState().label
if (!labelData.has(imageId)) {
labelData.set(imageId, new Map())
}
if (labelData.get(imageId)?.get(operationId)?.length) {
let result = [
...labelData.get(imageId)?.get(operationId)!,
normalizedPath,
]
.reduce((acc, curr) => {
const key = curr[0]
acc.set(key, curr)
return acc
}, new Map())
.values()
labelData.get(imageId)?.set(operationId, Array.from(result))
} else {
labelData.get(imageId)?.set(operationId, [normalizedPath])
}
console.log("绘制时", labelData)
useLabelStore.getState().pushStateStack(safeClone(labelData))
return { ...state, label: safeClone(labelData) }
}),
// 删除path
deleteOperation: (imageId, operationId, pathId) =>
storeSet((state) => {
if (state.label.get(imageId)?.get(operationId)?.length) {
let result =
state.label
.get(imageId)
?.get(operationId)
?.filter((item) => item[0] !== pathId) || []
state.label.get(imageId)?.set(operationId, result)
}
return state
}),
resetLabel: () =>
storeSet((state) => {
return {
...state,
label: initialLabelState.label,
labelTime: initialLabelState.labelTime,
}
}),
}),
{
name: "label-data-storage",
storage: storage,
}
)
)
const initialObjectState = {
pathStatus: {},
selectedPath: {},
operationStatus: {},
groupStatus: {},
imageStatus: {},
}
// store object list status
export const useObjectStore = create(
persist<ObjectState>(
(storeSet) => ({
pathStatus: initialObjectState.pathStatus,
selectedPath: initialObjectState.selectedPath,
operationStatus: initialObjectState.operationStatus,
groupStatus: initialObjectState.groupStatus,
imageStatus: initialObjectState.imageStatus,
updatePathStatus: (id: string, statusKey: "HIDE", statusValue: boolean) =>
storeSet((state) => {
let idStatus = state.pathStatus[id] || {}
idStatus = Object.assign(idStatus, {
[statusKey]: statusValue,
})
return {
...state,
pathStatus: {
...state.pathStatus,
[id]: idStatus,
},
}
}),
updateSelectedPath: (
id: string,
type: "ADD" | "DELETE" | "ADDSOME",
pathValue?: number
) =>
storeSet((state) => {
let idSelectedPath = state.selectedPath[id] || []
if (type === "ADD") {
if (useKeyEventStore.getState().shift) {
idSelectedPath = pathValue
? [...new Set([...idSelectedPath, pathValue])]
: idSelectedPath
} else {
idSelectedPath = pathValue ? [pathValue] : []
}
} else if (type === "ADDSOME") {
idSelectedPath = pathValue
? [...new Set([...idSelectedPath, pathValue])]
: idSelectedPath
} else if (type === "DELETE") {
idSelectedPath =
idSelectedPath?.filter((id) => id !== pathValue) || []
}
return {
...state,
selectedPath: {
...state.selectedPath,
[id]: idSelectedPath,
},
}
}),
resetPathAndOperationStatus: () =>
storeSet((state) => {
return {
...state,
selectedPath: {},
pathStatus: {},
operationStatus: {},
}
}),
updateOperationStatus: (
id: string,
statusKey: "HIDE" | "COLLAPSE" | "INPUT",
statusValue: string | boolean
) =>
storeSet((state) => {
let idStatus = state.operationStatus[id] || {}
idStatus = Object.assign(idStatus, {
[statusKey]: statusValue,
})
return {
...state,
operationStatus: {
...state.operationStatus,
[id]: idStatus,
},
}
}),
updateGroupStatus: (
id: string,
statusKey: "COLLAPSE",
statusValue: string | boolean
) =>
storeSet((state) => {
let idStatus = state.groupStatus[id] || {}
idStatus = Object.assign(idStatus, {
[statusKey]: statusValue,
})
return {
...state,
groupStatus: {
...state.groupStatus,
[id]: idStatus,
},
}
}),
updateImageStatus: (
id: string,
statusKey: "INPUT" | "COLLAPSE" | "FILTERCOLOR",
statusValue:
| string
| boolean
| {
[key: string]: boolean
}
) =>
storeSet((state) => {
let idStatus = state.imageStatus[id] || {}
idStatus = Object.assign(idStatus, {
[statusKey]: statusValue,
})
return {
...state,
imageStatus: {
...state.imageStatus,
[id]: idStatus,
},
}
}),
}),
{
name: "object-data-storage",
storage: storage,
partialize: (state: ObjectState) =>
Object.fromEntries(
Object.entries(state).filter(
([key]) => !["selectedPath"].includes(key)
)
) as any,
}
)
)
const initialKeyEventState = {
shift: false,
focusInput: false,
}
// store keyboard and mouse status
export const useKeyEventStore = create<KeyEventState>((set) => ({
shift: initialKeyEventState.shift,
setShift: (value: boolean) =>
set((state: KeyEventState) => ({ ...state, shift: value })),
focusInput: initialKeyEventState.focusInput,
setFocusInput: (value: boolean) =>
set((state: KeyEventState) => ({ ...state, focusInput: value })),
}))
interface ImageState {
images: Map<string, string>
setImages: (value: any) => void
}
interface VideoFrameState {
videoFrames: Map<string, string[]>
setVideoFrames: (key: string, value: string[]) => void
removeVideoFrames: (key: string) => void
}
export const useImagesStore = create(
persist<ImageState>(
(storeSet) => ({
images: new Map(),
setImages: (value: any) =>
storeSet((state) => {
return {
...state,
images: value,
}
}),
}),
{
name: "image-data-storage",
storage: storage,
}
)
)
export const useVideoFrameStore = create(
persist<VideoFrameState>(
(storeSet) => ({
videoFrames: new Map(),
setVideoFrames: (key: string, value: string[]) =>
storeSet((state) => {
const nextMap = new Map(state.videoFrames)
nextMap.set(key, value)
return {
...state,
videoFrames: nextMap,
}
}),
removeVideoFrames: (key: string) =>
storeSet((state) => {
const nextMap = new Map(state.videoFrames)
nextMap.delete(key)
return {
...state,
videoFrames: nextMap,
}
}),
}),
{
name: "video-frame-storage",
storage: storage,
}
)
)

View File

@@ -0,0 +1,49 @@
const deepCloneFallback = <T>(value: T, cache = new WeakMap()): T => {
if (value === null || typeof value !== "object") return value
if (cache.has(value as object)) return cache.get(value as object)
if (value instanceof Date) return new Date(value.getTime()) as T
if (value instanceof Map) {
const clonedMap = new Map()
cache.set(value as object, clonedMap)
value.forEach((mapValue, mapKey) => {
clonedMap.set(
deepCloneFallback(mapKey, cache),
deepCloneFallback(mapValue, cache)
)
})
return clonedMap as T
}
if (value instanceof Set) {
const clonedSet = new Set()
cache.set(value as object, clonedSet)
value.forEach((setValue) => {
clonedSet.add(deepCloneFallback(setValue, cache))
})
return clonedSet as T
}
if (Array.isArray(value)) {
const clonedArr: any[] = []
cache.set(value as object, clonedArr)
value.forEach((item, index) => {
clonedArr[index] = deepCloneFallback(item, cache)
})
return clonedArr as T
}
const clonedObj: Record<string, any> = {}
cache.set(value as object, clonedObj)
Object.keys(value as object).forEach((key) => {
clonedObj[key] = deepCloneFallback((value as any)[key], cache)
})
return clonedObj as T
}
export const safeClone = <T>(value: T): T => {
try {
return structuredClone(value)
} catch (error) {
console.warn("structuredClone failed, fallback to deep clone", error)
return deepCloneFallback(value)
}
}

View File

@@ -0,0 +1,248 @@
"use client"
export const dateFormat = "YYYY-MM-DD HH:mm:ss"
export const weekDayText = (data: number) => {
switch (data) {
case 0:
return "星期日"
case 1:
return "星期一"
case 2:
return "星期二"
case 3:
return "星期三"
case 4:
return "星期四"
case 5:
return "星期五"
case 6:
return "星期六"
default:
return "-"
}
}
// 标注任务工序
export const processOpts = [
{ label: "标注", value: "标注" },
{ label: "审核1", value: "审核1" },
{ label: "审核2", value: "审核2" },
]
// 状态码
export const STATUS_CODE = new Map([
[1, "已创建"],
[101, "已创建"], // 审核
[102, "已创建"], // 复审
[103, "已创建"], // 审核3
[180, "数据同步中"],
[190, "Embedding生成中"],
[191, "Embedding生成失败"],
[199, "审核失败"],
[201, "试标阶段"], // 试标中
[202, "试标阶段"], // 试标完成
[203, "试标阶段"], // 试标确认
[500, "标注中"],
[580, "待验收"],
[600, "已完成"],
[700, "已终止"],
])
// 状态码
export const projectStatusOpts = [
{ label: "试标阶段", value: "201,202,203" },
{ label: "标注中", value: 500 },
{ label: "待验收", value: 580 },
{ label: "已完成", value: 600 },
// { label: "审核失败", value: 199 },
{ label: "已终止", value: 700 },
]
export const CREATED_CODE = [1, 101, 102, 103, 190, 191, 199] // 已创建的状态码数组
export const ALL_CODE = [201, 202, 203, 500, 580, 600, 700] // 其他的状态码数组
export const PRELABEL_CODE = [201, 202, 203]
export const LABEL_CODE = [500, 580]
export const projectLabelTypeOpts = [
{ label: "点云框", value: 1 },
{ label: "点云分割", value: 2 },
{ label: "点云分类", value: 3 },
{ label: "图片", value: 4 },
{ label: "大语言基础", value: 5 },
{ label: "大语言QA", value: 6 },
{ label: "视频", value: 7 },
]
// 项目列表中状态码对应值及style
export const statusStyles = {
1: {
label: "已创建",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
101: {
label: "已创建",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
102: {
label: "已创建",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
103: {
label: "已创建",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
190: {
label: "Embedding生成中",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
191: {
label: "Embedding生成失败",
styleText: "text-[#D9333E] bg-[#FBEBEC]",
},
199: {
label: "审核失败",
styleText: "text-[#D9333E] bg-[#FBEBEC]",
},
201: {
label: "试标阶段",
styleText: "text-[#21D3ED] bg-[#EEFDFF]",
},
202: {
label: "试标阶段",
styleText: "text-[#21D3ED] bg-[#EEFDFF]",
},
203: {
label: "试标阶段",
styleText: "text-[#21D3ED] bg-[#EEFDFF]",
},
500: {
label: "标注中",
styleText: "text-[#1874FF] bg-[#E8F0FC]",
},
580: {
label: "待验收",
styleText: "text-[#FFB84D] bg-[#FEF5E7]",
},
600: {
label: "已完成",
styleText: "text-[#4FD796] bg-[#E7F9F0]",
},
700: {
label: "已终止",
styleText: "text-[#34425F] bg-[#F7F8F9]",
},
}
export const staticsColor = [
{ "8_151_156": 1 },
{ "9_109_217": 2 },
{ "15_15_118": 3 },
{ "28_27_175": 4 },
{ "35_84_116": 5 },
{ "45_69_235": 6 },
{ "53_127_127": 7 },
{ "54_124_171": 8 },
{ "54_207_201": 9 },
{ "56_158_13": 10 },
{ "63_18_119": 11 },
{ "69_151_223": 12 },
{ "81_188_187": 13 },
{ "83_29_171": 14 },
{ "84_119_48": 15 },
{ "95_33_176": 16 },
{ "100_16_23": 17 },
{ "113_249_251": 18 },
{ "116_78_41": 19 },
{ "125_179_75": 20 },
{ "127_48_236": 21 },
{ "127_127_54": 22 },
{ "132_133_239": 23 },
{ "147_210_239": 24 },
{ "151_29_36": 25 },
{ "155_125_62": 26 },
{ "156_239_102": 27 },
{ "161_98_48": 28 },
{ "178_253_253": 29 },
{ "188_136_242": 30 },
{ "189_191_84": 31 },
{ "191_49_47": 32 },
{ "196_29_127": 33 },
{ "202_247_138": 34 },
{ "203_164_81": 35 },
{ "207_19_34": 36 },
{ "212_107_8": 37 },
{ "212_177_6": 38 },
{ "223_134_144": 39 },
{ "233_157_84": 40 },
{ "244_206_159": 41 },
{ "252_202_0": 42 },
{ "252_250_147": 43 },
{ "253_247_87": 44 },
{ "255_191_120": 45 },
{ "255_236_61": 46 },
]
export const labelTypeOpts = [
{ label: "点云框", value: 1 },
{ label: "点云分割", value: 2 },
// { label: '点云分类', value: '点云分类' },
{ label: "2D框", value: 101 },
{ label: "多边形", value: 102 },
{ label: "关键点", value: 103 },
{ label: "多线段", value: 104 },
{ label: "文本描述", value: 201 },
]
export const labelTypeMap = new Map([
[1, "点云框"],
[2, "点云分割"],
[101, "2D框"],
[102, "多边形"],
[103, "关键点"],
[104, "多线段"],
[201, "文本描述"],
])
export const selectModeOpts = [
{ label: "输入", value: 0 },
{ label: "单选", value: 1 },
{ label: "多选", value: 2 },
]
export const selectModeMap = new Map([
[0, "输入"],
[1, "单选"],
[2, "多选"],
])
// 标注任务状态码
export const TaskStatusEnum = new Map([
// [0, "None"],
[1, "待领取"],
[2, "标注中"],
[3, "标注完成"],
[4, "审核中"],
[5, "审核完成"],
[6, "复审中"],
[7, "复审完成"],
[8, "无法标注"],
])
export const OwnTaskStatusOpts = {
0: "全部",
2: "标注中",
4: "审核中",
6: "复审中",
}
export const TaskStatusOpts = {
0: "全部",
1: "待领取",
2: "标注中",
3: "标注完成",
4: "审核中",
5: "审核完成",
6: "复审中",
7: "复审完成",
8: "无法标注",
}