diff --git a/app/person/dashboard/components/Icon.tsx b/app/person/dashboard/components/Icon.tsx new file mode 100644 index 0000000..f3bf826 --- /dev/null +++ b/app/person/dashboard/components/Icon.tsx @@ -0,0 +1,282 @@ +"use client" + +export const DataIcon = () => ( + + + + + + + + + + + + + + +) + +export const HourIcon = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export const ReviewIcon = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) diff --git a/app/person/dashboard/components/PersonalProjectTable.tsx b/app/person/dashboard/components/PersonalProjectTable.tsx new file mode 100644 index 0000000..97e8c36 --- /dev/null +++ b/app/person/dashboard/components/PersonalProjectTable.tsx @@ -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(0) + + const originColumn = useMemo(() => { + let column: DataTableColumn[] = + [ + { + accessor: "project_id", + title: "项目ID", + width: 150, + render: (record: Project.PersonProjectDashboardResponseItem) => { + return ( + { + 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} + + ) + }, + }, + { + 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 ( + + {record.project_type ?? "-"} + + ) + }, + }, + { + 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 ( + + + + 参与项目 + + }> + 更新 + + + + + 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="当前页数" + /> + + + ) +} diff --git a/app/person/dashboard/components/PersonalTaskTable.tsx b/app/person/dashboard/components/PersonalTaskTable.tsx new file mode 100644 index 0000000..06779bd --- /dev/null +++ b/app/person/dashboard/components/PersonalTaskTable.tsx @@ -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([]) + 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(0) + + const originColumn = useMemo(() => { + let column: DataTableColumn[] = [ + { + accessor: "project_name", + title: "项目名称", + width: 150, + render: (record: Task.SimpleTaskItem) => { + return {record.project_name} + }, + }, + { + accessor: "id", + title: "任务ID", + width: 150, + render: (record: Task.SimpleTaskItem) => { + return ( + { + 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} + + ) + }, + }, + { + accessor: "label_status", + title: "状态", + width: 80, + render: (record: Task.SimpleTaskItem) => { + return ( + + {TaskStatusEnum.get(record.label_status)} + + ) + }, + }, + { + accessor: "label_username", + title: "标注员", + width: 80, + }, + { + accessor: "review1_username", + title: "审核员", + width: 80, + }, + { + accessor: "review2_username", + title: "复审员", + width: 80, + }, + ] + return column + }, [setBackProps]) + + return ( + + + + 我的任务 + + }> + 更新 + + + + + 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="当前页数" + /> + + + ) +} diff --git a/app/person/dashboard/page.tsx b/app/person/dashboard/page.tsx index 5010763..0da1889 100644 --- a/app/person/dashboard/page.tsx +++ b/app/person/dashboard/page.tsx @@ -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 ( + + + + + + + + + + + {title} + + + {timeText} + + + + + {sizeTitle} + + + {sizeValue} + + + + + + ) +} + +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 ( + + + + + 今日 + + + + + {dayjs().format("DD")} + + {monthText(dayjs().format("MM"))} + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/app/person/dashboard/resource/images/data.png b/app/person/dashboard/resource/images/data.png new file mode 100644 index 0000000..cc8e522 Binary files /dev/null and b/app/person/dashboard/resource/images/data.png differ diff --git a/app/person/dashboard/resource/images/data_logo.png b/app/person/dashboard/resource/images/data_logo.png new file mode 100644 index 0000000..0288cd9 Binary files /dev/null and b/app/person/dashboard/resource/images/data_logo.png differ diff --git a/app/person/dashboard/resource/images/hour.png b/app/person/dashboard/resource/images/hour.png new file mode 100644 index 0000000..b4c1a42 Binary files /dev/null and b/app/person/dashboard/resource/images/hour.png differ diff --git a/app/person/dashboard/resource/images/hour_logo.png b/app/person/dashboard/resource/images/hour_logo.png new file mode 100644 index 0000000..195906e Binary files /dev/null and b/app/person/dashboard/resource/images/hour_logo.png differ diff --git a/app/person/dashboard/resource/images/review.png b/app/person/dashboard/resource/images/review.png new file mode 100644 index 0000000..e432d30 Binary files /dev/null and b/app/person/dashboard/resource/images/review.png differ diff --git a/app/person/dashboard/resource/images/review_logo.png b/app/person/dashboard/resource/images/review_logo.png new file mode 100644 index 0000000..76b3fec Binary files /dev/null and b/app/person/dashboard/resource/images/review_logo.png differ diff --git a/app/person/report/components/DailyReportModal.tsx b/app/person/report/components/DailyReportModal.tsx new file mode 100644 index 0000000..7bab3ff --- /dev/null +++ b/app/person/report/components/DailyReportModal.tsx @@ -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 ( + + 小时(H) + + ) +} + +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 ( + { + form.reset() + onCloseAction() + }} + title={record ? "编辑日报" : "填写日报"} + size={840} + centered + closeOnClickOutside={false}> + + + + + + + + + + + + + + + + + + + + + + + + } + rightSectionWidth={78} + {...form.getInputProps("work_time")} + /> + + + + + + + + + } + rightSectionWidth={78} + /> + + + + + + + { + form.reset() + onCloseAction() + }}> + 取消 + + + 提交 + + + + + + ) +} diff --git a/app/person/report/config.ts b/app/person/report/config.ts new file mode 100644 index 0000000..8763169 --- /dev/null +++ b/app/person/report/config.ts @@ -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([ + [true, "是"], + [false, "否"], +]) + +export const objTypeTextMap = new Map([ + [0, "静态"], + [1, "动态"], +]) diff --git a/app/person/report/page.tsx b/app/person/report/page.tsx index c344805..1c4aefd 100644 --- a/app/person/report/page.tsx +++ b/app/person/report/page.tsx @@ -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([]) + const [summary, setSummary] = useState(null) + + const [sortStatus, setSortStatus] = useState({ + columnAccessor: "id", + direction: "desc", + }) + + const [modalOpened, setModalOpened] = useState(false) + const [editingRecord, setEditingRecord] = + useState(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[] = [ + { 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) => ( + + {record.project_name ?? "-"} + + ), + }, + { + accessor: "performance_accounting", + title: "绩效核算", + width: 110, + render: (record) => { + if (record.performance_accounting === true) + return {performanceTextMap.get(true)} + if (record.performance_accounting === false) + return {performanceTextMap.get(false)} + 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) => ( + + {record.remarks ?? "-"} + + ), + }, + { + accessor: "operation", + title: "操作", + width: 90, + textAlign: "center", + render: (record) => { + const disabled = !!record.lock_status + return ( + + { + if (disabled) return + setEditingRecord(record) + setModalOpened(true) + }}> + + + { + if (disabled || !record.id) return + modals.openConfirmModal({ + title: "删除日报", + centered: true, + children: ( + + 删除后不可恢复,你确定删除该日报吗? + + ), + labels: { confirm: "删除", cancel: "取消" }, + confirmProps: { color: "red" }, + onConfirm: async () => { + await deleteDailyWorkData(record.id as number) + notifications.show({ + color: "green", + title: "操作成功", + message: "已删除日报", + }) + load() + }, + }) + }}> + + + + ) + }, + }, + ] + 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 ( + + + + + + 搜索条件 + + ) : ( + + ) + } + onClick={() => setSearchExpanded((v) => !v)}> + {searchExpanded ? "收起" : "展开"} + + + + + } + onClick={() => { + setFilters(defaultFilters) + setAppliedFilters(defaultFilters) + setPage(1) + setQueryTrigger((v) => v + 1) + }}> + 重置 + + } + onClick={() => { + setAppliedFilters(filters) + setPage(1) + setQueryTrigger((v) => v + 1) + }}> + 查询 + + + + + + + setFilters((s) => ({ + ...s, + date_start: e.target.value, + })) + } + /> + { + setFilters((s) => ({ ...s, date_end: e.target.value })) + }} + /> + setFilters((s) => ({ ...s, project_id: v }))} + /> + setFilters((s) => ({ ...s, leader_uid: v }))} + /> + + setFilters((s) => ({ ...s, set_type: v[0] ?? "" })) + } + /> + + setFilters((s) => ({ ...s, actual_type: v[0] ?? "" })) + } + /> + + setFilters((s) => ({ ...s, work_type: v[0] ?? "" })) + } + /> + + setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" })) + } + /> + + setFilters((s) => ({ + ...s, + performance_accounting: v[0] ?? "", + })) + } + /> + + setFilters((s) => ({ ...s, obj_type: v[0] ?? "" })) + } + /> + + + + + + + + + } + size="xs" + radius={"xs"} + onClick={() => { + setEditingRecord(null) + setModalOpened(true) + }}> + 填写日报 + + + + + + + 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) + }} + /> + + + + 当前页总计 + + + 工时:{summaryText.total_work_time} + 标准量:{summaryText.total_standard_size} + + 完成量:{summaryText.total_completed_size} + + 余量:{summaryText.total_residual_size} + + 余量工时:{summaryText.total_residual_work_time} + + + + + + + + { + setModalOpened(false) + setEditingRecord(null) + if (refresh) load() + }} + /> + + ) } diff --git a/components/label/api/const.ts b/components/label/api/const.ts new file mode 100644 index 0000000..25355ce --- /dev/null +++ b/components/label/api/const.ts @@ -0,0 +1,4 @@ +export const BASE_LABEL_API = + process.env.NEXT_PUBLIC_ENV === "production" + ? "https://label.cowarobot.com" + : "https://label.softtest.cowarobot.com" diff --git a/components/label/api/daily/index.ts b/components/label/api/daily/index.ts new file mode 100644 index 0000000..b3011f9 --- /dev/null +++ b/components/label/api/daily/index.ts @@ -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({ + 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({ + 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 }), + }) +} diff --git a/components/label/api/daily/typing.ts b/components/label/api/daily/typing.ts new file mode 100644 index 0000000..df920ae --- /dev/null +++ b/components/label/api/daily/typing.ts @@ -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 + } +} diff --git a/components/label/api/label/typing.ts b/components/label/api/label/typing.ts new file mode 100644 index 0000000..5d41646 --- /dev/null +++ b/components/label/api/label/typing.ts @@ -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> + id: number + image_id: number + key_points?: KeyPoint[] + last_modified_timestamp: number + last_modified_uid: number + line_segment?: Array> + prelabel: boolean + rect?: number[] + segmentation?: Array> + 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 +} diff --git a/components/label/api/project/index.ts b/components/label/api/project/index.ts new file mode 100644 index 0000000..efdb526 --- /dev/null +++ b/components/label/api/project/index.ts @@ -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({ + url: BASE_LABEL_API + "/api/v1/label_server/project/list", + method: "POST", + body: JSON.stringify(data), + }) +} + +// 获取项目详情 +export const getProjectDetailById = (id: number) => { + return httpFetch({ + url: BASE_LABEL_API + `/api/v1/label_server/project/detail/${id}`, + method: "GET", + }) +} + +// 校验项目源数据 +export const checkLabelDataSource = (data: Project.CheckRequest) => { + return httpFetch({ + 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({ + 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({ + 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>({ + 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({ + url: BASE_LABEL_API + "/api/v1/label_server/project/type_list", + method: "GET", + }) +} + +// 获取项目管理员列表 +export const getProjectAdminList = () => { + return httpFetch>({ + url: BASE_LABEL_API + "/api/v1/label_server/project/admin_list", + method: "GET", + }) +} + +// 获取项目审核员 +export const getProjectReviewers = () => { + return httpFetch({ + url: BASE_LABEL_API + "/api/v1/label_server/project/reviewer_map", + method: "GET", + }) +} + +// 获取用户有权限的项目 +export const getSelfProjectList = () => { + return httpFetch>({ + url: BASE_LABEL_API + "/api/v1/label_server/project/self", + method: "GET", + }) +} diff --git a/components/label/api/project/typing.ts b/components/label/api/project/typing.ts new file mode 100644 index 0000000..a3b8e42 --- /dev/null +++ b/components/label/api/project/typing.ts @@ -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 + } +} diff --git a/components/label/api/task/index.ts b/components/label/api/task/index.ts new file mode 100644 index 0000000..a02c752 --- /dev/null +++ b/components/label/api/task/index.ts @@ -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({ + url: BASE_LABEL_API + "/api/v1/label_server/task/list", + method: "POST", + body: JSON.stringify(data), + }) +} + +// 领取任务 +export const taskAcquire = (data: Task.AcquireReqProps) => { + return httpFetch({ + 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({ + 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({ + 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({ + url: BASE_LABEL_API + `/api/v1/label_server/task/list_by_user`, + method: "GET", + data: params, + }) +} diff --git a/components/label/api/task/typing.ts b/components/label/api/task/typing.ts new file mode 100644 index 0000000..3ca89f2 --- /dev/null +++ b/components/label/api/task/typing.ts @@ -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 + } +} diff --git a/components/label/store.ts b/components/label/store.ts new file mode 100644 index 0000000..a479563 --- /dev/null +++ b/components/label/store.ts @@ -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> +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 + ) => 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 = { + 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): Promise => { + // console.log(name, "with value", value, "has been saved"); + await set(name, superjson.stringify(value)) + }, + removeItem: async (name: string): Promise => { + console.log(name, "has been deleted") + await del(name) + }, +} + +// store path data +export const useLabelStore = create( + persist( + (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( + (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((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 + setImages: (value: any) => void +} + +interface VideoFrameState { + videoFrames: Map + setVideoFrames: (key: string, value: string[]) => void + removeVideoFrames: (key: string) => void +} + +export const useImagesStore = create( + persist( + (storeSet) => ({ + images: new Map(), + setImages: (value: any) => + storeSet((state) => { + return { + ...state, + images: value, + } + }), + }), + { + name: "image-data-storage", + storage: storage, + } + ) +) + +export const useVideoFrameStore = create( + persist( + (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, + } + ) +) diff --git a/components/label/utils/clone.ts b/components/label/utils/clone.ts new file mode 100644 index 0000000..ca8d4c9 --- /dev/null +++ b/components/label/utils/clone.ts @@ -0,0 +1,49 @@ +const deepCloneFallback = (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 = {} + 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 = (value: T): T => { + try { + return structuredClone(value) + } catch (error) { + console.warn("structuredClone failed, fallback to deep clone", error) + return deepCloneFallback(value) + } +} diff --git a/components/label/utils/constants.ts b/components/label/utils/constants.ts new file mode 100644 index 0000000..4a8e7e7 --- /dev/null +++ b/components/label/utils/constants.ts @@ -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: "无法标注", +}