Merge commit '062242a63ccb2d57868033730d9edbbf68c44896' into zh
This commit is contained in:
282
app/person/dashboard/components/Icon.tsx
Normal file
282
app/person/dashboard/components/Icon.tsx
Normal 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>
|
||||
)
|
||||
205
app/person/dashboard/components/PersonalProjectTable.tsx
Normal file
205
app/person/dashboard/components/PersonalProjectTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
225
app/person/dashboard/components/PersonalTaskTable.tsx
Normal file
225
app/person/dashboard/components/PersonalTaskTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
BIN
app/person/dashboard/resource/images/data.png
Normal file
BIN
app/person/dashboard/resource/images/data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
app/person/dashboard/resource/images/data_logo.png
Normal file
BIN
app/person/dashboard/resource/images/data_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app/person/dashboard/resource/images/hour.png
Normal file
BIN
app/person/dashboard/resource/images/hour.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
app/person/dashboard/resource/images/hour_logo.png
Normal file
BIN
app/person/dashboard/resource/images/hour_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
BIN
app/person/dashboard/resource/images/review.png
Normal file
BIN
app/person/dashboard/resource/images/review.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
app/person/dashboard/resource/images/review_logo.png
Normal file
BIN
app/person/dashboard/resource/images/review_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
541
app/person/report/components/DailyReportModal.tsx
Normal file
541
app/person/report/components/DailyReportModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
68
app/person/report/config.ts
Normal file
68
app/person/report/config.ts
Normal 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, "动态"],
|
||||
])
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
4
components/label/api/const.ts
Normal file
4
components/label/api/const.ts
Normal 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"
|
||||
49
components/label/api/daily/index.ts
Normal file
49
components/label/api/daily/index.ts
Normal 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 }),
|
||||
})
|
||||
}
|
||||
84
components/label/api/daily/typing.ts
Normal file
84
components/label/api/daily/typing.ts
Normal 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
|
||||
}
|
||||
}
|
||||
171
components/label/api/label/typing.ts
Normal file
171
components/label/api/label/typing.ts
Normal 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
|
||||
}
|
||||
198
components/label/api/project/index.ts
Normal file
198
components/label/api/project/index.ts
Normal 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",
|
||||
})
|
||||
}
|
||||
474
components/label/api/project/typing.ts
Normal file
474
components/label/api/project/typing.ts
Normal 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
|
||||
}
|
||||
}
|
||||
77
components/label/api/task/index.ts
Normal file
77
components/label/api/task/index.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
179
components/label/api/task/typing.ts
Normal file
179
components/label/api/task/typing.ts
Normal 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
605
components/label/store.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
)
|
||||
49
components/label/utils/clone.ts
Normal file
49
components/label/utils/clone.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
248
components/label/utils/constants.ts
Normal file
248
components/label/utils/constants.ts
Normal 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: "无法标注",
|
||||
}
|
||||
Reference in New Issue
Block a user