diff --git a/app/management/person/collection/config.ts b/app/management/person/collection/config.ts new file mode 100644 index 0000000..3f78958 --- /dev/null +++ b/app/management/person/collection/config.ts @@ -0,0 +1,60 @@ +export const dispatchOpts: Record< + number, + { label: string; opts: Array<{ label: string; value: number }> } +> = { + 1: { + label: "待领取", + opts: [{ label: "标注中", value: 2 }], + }, + 2: { + label: "标注中", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + ], + }, + 3: { + label: "标注完成", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + ], + }, + 4: { + label: "审核中", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + { label: "复审中", value: 6 }, + ], + }, + 5: { + label: "审核完成", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + { label: "复审中", value: 6 }, + { label: "复审完成", value: 7 }, + ], + }, + 6: { + label: "复审中", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + { label: "复审中", value: 6 }, + ], + }, + 7: { + label: "复审完成", + opts: [ + { label: "标注中", value: 2 }, + { label: "审核中", value: 4 }, + { label: "复审中", value: 6 }, + ], + }, + 8: { + label: "无法标注", + opts: [{ label: "标注中", value: 2 }], + }, +} diff --git a/app/management/person/collection/detail/InfoSettingContainer.tsx b/app/management/person/collection/detail/InfoSettingContainer.tsx new file mode 100644 index 0000000..0fd9b31 --- /dev/null +++ b/app/management/person/collection/detail/InfoSettingContainer.tsx @@ -0,0 +1,173 @@ +"use client" + +import { projectConfig } from "@/components/label/api/project" +import { ProjectDetail } from "@/components/label/api/project/typing" +import { useAllUserStore } from "@/components/label/store/auth" +import { Button, Group, Paper, Stack, Switch, Text } from "@mantine/core" +import { notifications } from "@mantine/notifications" +import { useMemo, useState } from "react" +import AdminUserModal from "./components/AdminUserModal" +import LabelSchemeContainer from "./components/LabelSchemeContainer" +import NoteRulesTable from "./components/NoteRulesTable" +import SamplingPlanContainer from "./components/SamplingPlanContainer" +import TaskStageTabsContainer from "./components/TaskStageTabsContainer" + +export default function InfoSettingContainer(props: { + info: ProjectDetail.DataProps | undefined + userEnums: { + allEnum: Map + labelEnum: Map + review1Enum: Map + review2Enum: Map + } + update: () => void +}) { + const { info, userEnums, update } = props + const projectId = info?.id ?? -1 + + const { userOpts } = useAllUserStore() + const userOptions = useMemo(() => { + return userOpts.map((o) => ({ label: o.label, value: String(o.value) })) + }, [userOpts]) + + const [adminModalOpen, setAdminModalOpen] = useState(false) + const [lockingValue, setLockingValue] = useState( + !!info?.is_lock_object + ) + const [savingLock, setSavingLock] = useState(false) + + const adminText = useMemo(() => { + const adminUids = info?.admin_user ?? [] + const names = adminUids.map((uid) => { + const opt = userOpts.find((o) => o.value === uid) + return opt?.label ?? String(uid) + }) + return names.length ? names.join("、") : "-" + }, [info?.admin_user, userOpts]) + + const saveLock = async () => { + if (projectId === -1) return + setSavingLock(true) + try { + await projectConfig({ + project_id: projectId, + is_lock_object: lockingValue, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存配置", + }) + update() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } finally { + setSavingLock(false) + } + } + + return ( + <> + + + + + 基础配置 + + + + + + + 项目管理员 + + + {adminText} + + + + + + + + + 图片对象锁定 + + + 开启后对象可被锁定,避免多人同时标注冲突 + + + + setLockingValue(e.currentTarget.checked)} + /> + + + + + + + + + + + + + + + + { + setAdminModalOpen(false) + if (!next || projectId === -1) return + try { + await projectConfig({ project_id: projectId, admin_user: next }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存项目管理员", + }) + update() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + }} + /> + + ) +} diff --git a/app/management/person/collection/detail/OwnTaskTableContainer.tsx b/app/management/person/collection/detail/OwnTaskTableContainer.tsx new file mode 100644 index 0000000..6f90a07 --- /dev/null +++ b/app/management/person/collection/detail/OwnTaskTableContainer.tsx @@ -0,0 +1,348 @@ +"use client" + +import { ProjectDetail } from "@/components/label/api/project/typing" +import { getTaskList, taskAcquire } from "@/components/label/api/task" +import { Task } from "@/components/label/api/task/typing" +import { + useBackUrlStore, + useOwnTaskStore, + usePermissionStore, +} from "@/components/label/store/auth" +import { OwnTaskStatusOpts } from "@/components/label/utils/constants" +import { Button, Group, Paper, Select, Stack, TextInput } from "@mantine/core" +import { notifications } from "@mantine/notifications" +import { IconRefresh } from "@tabler/icons-react" +import { DataTable, DataTableColumn } from "mantine-datatable" +import { useRouter, useSearchParams } from "next/navigation" +import { useCallback, useEffect, useMemo, useState } from "react" +import TaskStatusTag from "./components/TaskStatusTag" +import { runCommand } from "./util" + +export default function OwnTaskTableContainer(props: { + info: ProjectDetail.DataProps | undefined + userEnums: { + allEnum: Map + labelEnum: Map + review1Enum: Map + review2Enum: Map + } +}) { + const { info, userEnums } = props + const urlParams = useSearchParams() + const id = urlParams.get("id") + const type = urlParams.get("type") + const projectId = + typeof id === "string" && !isNaN(Number(id)) ? Number(id) : -1 + + const router = useRouter() + const { setBackProps } = useBackUrlStore() + const user_id = usePermissionStore((s) => s.user_id) + const user_name = usePermissionStore((s) => s.user_name) + const user_password = usePermissionStore((s) => s.user_password) + + const { params, total, setParams, resetParams, setPagination, setTotal } = + useOwnTaskStore() + + const [form, setForm] = useState<{ + search_id: string + search_status: string + }>({ + search_id: "", + search_status: "0", + }) + + const [records, setRecords] = useState([]) + const [loading, setLoading] = useState(false) + const [queryTrigger, setQueryTrigger] = useState(0) + + const [isLabel, isReview1, isReview2] = useMemo(() => { + const uid = typeof user_id === "number" ? user_id : Number(user_id ?? 0) + return [ + userEnums.labelEnum?.has(uid), + userEnums.review1Enum?.has(uid), + userEnums.review2Enum?.has(uid), + ] + }, [userEnums, user_id]) + + const appliedParams = useMemo(() => { + const uid = typeof user_id === "number" ? user_id : Number(user_id ?? 0) + const next: Task.ListRequest = { + ...params, + page_number: params.page_number ?? 1, + page_size: params.page_size ?? 20, + current_uid: uid ? [uid] : undefined, + project_id: projectId !== -1 ? [projectId] : undefined, + get_data: false, + } + + if (form.search_id) { + const idValue = Number(form.search_id) + if (Number.isFinite(idValue)) next.id = [idValue] + } + if (form.search_status !== "0") { + const statusValue = Number(form.search_status) + if (Number.isFinite(statusValue)) next.label_status = [statusValue] + } else { + delete next.label_status + } + return next + }, [form.search_id, form.search_status, params, projectId, user_id]) + + const load = useCallback(async () => { + if (projectId === -1) return + setLoading(true) + try { + const res = await getTaskList(appliedParams) + setRecords(res?.task_list ?? []) + setTotal(res?.total_items ?? 0) + } catch (e) { + setRecords([]) + setTotal(0) + notifications.show({ + color: "red", + title: "加载失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } finally { + setLoading(false) + } + }, [appliedParams, projectId, queryTrigger, setTotal]) + + useEffect(() => { + load() + }, [load]) + + const handleAcquireTask = async (label_action: 1 | 2 | 3) => { + const uid = typeof user_id === "number" ? user_id : Number(user_id ?? 0) + if (!uid || projectId === -1) return + try { + await taskAcquire({ + uid, + project_id: projectId, + label_action, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: "领取成功", + }) + setQueryTrigger((v) => v + 1) + } catch (e) { + notifications.show({ + color: "red", + title: "领取失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + const columns = useMemo(() => { + const cols: DataTableColumn[] = [ + { + accessor: "id", + title: "任务ID", + width: 100, + render: (record) => ( + + ), + }, + { + accessor: "label_status", + title: "状态", + width: 120, + render: (record) => ( + + ), + }, + { + accessor: "label_object_size.object_size", + title: "对象总数", + width: 110, + render: (record) => record.label_object_size?.object_size ?? "-", + }, + { + accessor: "label_object_size.valid_size", + title: "有效数", + width: 100, + render: (record) => record.label_object_size?.valid_size ?? "-", + }, + { + accessor: "label_object_size.new_size", + title: "新增数", + width: 100, + render: (record) => record.label_object_size?.new_size ?? "-", + }, + { + accessor: "label_object_size.prelabel_size", + title: "预标注总数", + width: 120, + render: (record) => record.label_object_size?.prelabel_size ?? "-", + }, + { + accessor: "label_object_size.prelabel_modify_size", + title: "预标注修改数", + width: 130, + render: (record) => + record.label_object_size?.prelabel_modify_size ?? "-", + }, + { + accessor: "label_object_size.first_comment_size", + title: "批注一轮", + width: 110, + render: (record) => record.label_object_size?.first_comment_size ?? "-", + }, + { + accessor: "label_object_size.second_comment_size", + title: "批注二轮", + width: 110, + render: (record) => + record.label_object_size?.second_comment_size ?? "-", + }, + ] + return cols + }, [info, projectId, router, setBackProps, type, user_name, user_password]) + + return ( + + + + + + setForm((s) => ({ ...s, search_id: e.currentTarget.value })) + } + style={{ width: 160 }} + /> + ({ + value, + label, + }))} + value={form.search_status} + onChange={(v) => + setForm((s) => ({ ...s, search_status: v ?? "0" })) + } + allowDeselect={false} + style={{ width: 160 }} + /> + + + + + + + + + + + + + width="100%" + style={{ width: "100%" }} + withTableBorder + withRowBorders + pinFirstColumn + pinLastColumn + scrollAreaProps={{ type: "auto" }} + fetching={loading} + records={records} + columns={columns} + totalRecords={total} + recordsPerPage={params.page_size ?? 20} + page={params.page_number ?? 1} + onPageChange={(p) => { + setPagination(p, params.page_size ?? 20) + setQueryTrigger((v) => v + 1) + }} + onRecordsPerPageChange={(s) => { + setPagination(1, s) + setQueryTrigger((v) => v + 1) + }} + recordsPerPageOptions={[10, 20, 50, 100]} + noRecordsText="暂无数据" + /> + + + + setWorkflowTaskId(null)} + /> + { + setDispatchRecord(null) + if (refresh) setQueryTrigger((v) => v + 1) + }} + /> + { + setReleaseRecord(null) + if (refresh) setQueryTrigger((v) => v + 1) + }} + /> + + ) +} diff --git a/app/management/person/collection/detail/components/AdminUserModal.tsx b/app/management/person/collection/detail/components/AdminUserModal.tsx new file mode 100644 index 0000000..5b55997 --- /dev/null +++ b/app/management/person/collection/detail/components/AdminUserModal.tsx @@ -0,0 +1,54 @@ +"use client" + +import { Button, Group, Modal, MultiSelect, Stack } from "@mantine/core" +import { useEffect } from "react" +import { useForm } from "@mantine/form" + +export default function AdminUserModal(props: { + opened: boolean + value: number[] + options: Array<{ label: string; value: string }> + onClose: (next?: number[]) => void +}) { + const { opened, value, options, onClose } = props + + const form = useForm({ + initialValues: { users: value.map(String) }, + }) + + useEffect(() => { + if (!opened) return + form.setValues({ users: value.map(String) }) + form.resetDirty() + }, [opened, value, form]) + + return ( + onClose()} title="编辑项目管理员" centered size={560}> +
{ + const next = values.users + .map((v) => Number(v)) + .filter((v) => Number.isFinite(v)) + onClose(next) + })}> + + form.setFieldValue("users", v)} + /> + + + + + +
+
+ ) +} + diff --git a/app/management/person/collection/detail/components/DispatchModal.tsx b/app/management/person/collection/detail/components/DispatchModal.tsx new file mode 100644 index 0000000..2774cf8 --- /dev/null +++ b/app/management/person/collection/detail/components/DispatchModal.tsx @@ -0,0 +1,137 @@ +"use client" + +import { taskDispatch } from "@/components/label/api/task" +import { Task } from "@/components/label/api/task/typing" +import { usePermissionStore } from "@/components/label/store/auth" +import { Button, Group, Modal, Select, Stack, Switch, Text } from "@mantine/core" +import { useForm } from "@mantine/form" +import { notifications } from "@mantine/notifications" +import { dispatchOpts } from "../../config" + +export default function DispatchModal(props: { + opened: boolean + record: Task.DataProps | null + userOptions: Array<{ label: string; value: string }> + onClose: (refresh?: boolean) => void +}) { + const { opened, record, userOptions, onClose } = props + const operation_uid = usePermissionStore((s) => s.user_id) + + const form = useForm({ + initialValues: { + reject: false, + task_status_dst: "", + uid_dst: "", + }, + }) + + const taskStatusSrc = record?.label_status ?? 0 + const dstOptions = + dispatchOpts[taskStatusSrc]?.opts?.map((o) => ({ + label: o.label, + value: String(o.value), + })) ?? [] + + return ( + onClose()} + title="调度任务" + centered + size={520}> +
{ + const opUid = typeof operation_uid === "number" ? operation_uid : Number(operation_uid ?? 0) + if (!record?.id || !opUid) { + notifications.show({ + color: "red", + title: "操作失败", + message: "缺少必要参数", + }) + return + } + const taskStatusDst = Number(values.task_status_dst) + if (!Number.isFinite(taskStatusDst)) { + notifications.show({ + color: "red", + title: "校验失败", + message: "请选择目标状态", + }) + return + } + + const uidDst = values.uid_dst ? Number(values.uid_dst) : null + if (values.uid_dst && !Number.isFinite(uidDst)) { + notifications.show({ + color: "red", + title: "校验失败", + message: "请选择目标人员", + }) + return + } + + try { + await taskDispatch({ + operation_uid: opUid, + reject: values.reject, + task_ids: [record.id], + task_status_src: taskStatusSrc, + task_status_dst: taskStatusDst, + uid_src: record.current_uid ?? null, + uid_dst: uidDst, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已调度任务", + }) + onClose(true) + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + })}> + + + 当前任务:{record?.id ?? "-"}(源状态:{taskStatusSrc}) + + + form.setFieldValue("uid_dst", v ?? "")} + /> + + form.setFieldValue("reject", e.currentTarget.checked)} + /> + + + + + + +
+
+ ) +} + diff --git a/app/management/person/collection/detail/components/LabelSchemeContainer.tsx b/app/management/person/collection/detail/components/LabelSchemeContainer.tsx new file mode 100644 index 0000000..185bcf2 --- /dev/null +++ b/app/management/person/collection/detail/components/LabelSchemeContainer.tsx @@ -0,0 +1,88 @@ +"use client" + +import { ProjectDetail } from "@/components/label/api/project/typing" +import { Badge, Group, Paper, Stack, Text } from "@mantine/core" +import { DataTable, DataTableColumn } from "mantine-datatable" +import { useMemo } from "react" + +export default function LabelSchemeContainer(props: { + info: ProjectDetail.DataProps | undefined +}) { + const schemas = props.info?.label_schema_list ?? [] + + const columns = useMemo(() => { + const cols: DataTableColumn[] = [ + { accessor: "category_id", title: "ID", width: 70 }, + { accessor: "label_class", title: "类别", width: 140 }, + { + accessor: "color", + title: "颜色", + width: 120, + render: (record) => { + const color = Array.isArray(record.color) ? record.color : [] + const bg = + color.length >= 3 + ? `rgb(${color[0]}, ${color[1]}, ${color[2]})` + : "rgba(0,0,0,0.15)" + return ( + +
+ + {color.length >= 3 ? `${color[0]},${color[1]},${color[2]}` : "-"} + + + ) + }, + }, + { + accessor: "label_type", + title: "类型", + width: 90, + render: (record) => {record.label_type}, + }, + { + accessor: "sub_attributes", + title: "子属性", + width: 240, + render: (record) => ( + + {(record.sub_attributes ?? []).join("、") || "-"} + + ), + }, + ] + return cols + }, []) + + return ( + + + 标注方案 + + withTableBorder + withRowBorders + records={schemas} + columns={columns} + minHeight={160} + noRecordsText="暂无数据" + /> + + + ) +} + diff --git a/app/management/person/collection/detail/components/NoteRulesTable.tsx b/app/management/person/collection/detail/components/NoteRulesTable.tsx new file mode 100644 index 0000000..4457b4a --- /dev/null +++ b/app/management/person/collection/detail/components/NoteRulesTable.tsx @@ -0,0 +1,55 @@ +"use client" + +import { ProjectDetail } from "@/components/label/api/project/typing" +import { List, Paper, Stack, Text } from "@mantine/core" + +export default function NoteRulesTable(props: { + info: ProjectDetail.DataProps | undefined +}) { + const rules = props.info?.comment_rule + const textRules = rules?.text_rules ?? [] + const checkBoxRules = rules?.check_box_rules ?? [] + + return ( + + + 批注规则 + + + + 文本规则 + + {textRules.length ? ( + + {textRules.map((r, idx) => ( + {r} + ))} + + ) : ( + + - + + )} + + + + + 勾选规则 + + {checkBoxRules.length ? ( + + {checkBoxRules.map((r, idx) => ( + {String(r)} + ))} + + ) : ( + + - + + )} + + + + ) +} + diff --git a/app/management/person/collection/detail/components/ReleaseModal.tsx b/app/management/person/collection/detail/components/ReleaseModal.tsx new file mode 100644 index 0000000..35d8b13 --- /dev/null +++ b/app/management/person/collection/detail/components/ReleaseModal.tsx @@ -0,0 +1,86 @@ +"use client" + +import { taskRelease } from "@/components/label/api/task" +import { Task } from "@/components/label/api/task/typing" +import { usePermissionStore } from "@/components/label/store/auth" +import { Button, Group, Modal, Stack, Switch, Text } from "@mantine/core" +import { useForm } from "@mantine/form" +import { notifications } from "@mantine/notifications" + +export default function ReleaseModal(props: { + opened: boolean + record: Task.DataProps | null + onClose: (refresh?: boolean) => void +}) { + const { opened, record, onClose } = props + const operation_uid = usePermissionStore((s) => s.user_id) + + const form = useForm({ + initialValues: { + reject: false, + }, + }) + + return ( + onClose()} + title="释放任务" + centered + size={520}> +
{ + const opUid = typeof operation_uid === "number" ? operation_uid : Number(operation_uid ?? 0) + if (!record?.id || !opUid || !record.current_uid) { + notifications.show({ + color: "red", + title: "操作失败", + message: "缺少必要参数", + }) + return + } + try { + await taskRelease({ + operation_uid: opUid, + reject: values.reject, + task_ids: [record.id], + task_status: record.label_status, + uid: record.current_uid, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已释放任务", + }) + onClose(true) + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + })}> + + + 当前任务:{record?.id ?? "-"}(状态:{record?.label_status ?? "-"}) + + form.setFieldValue("reject", e.currentTarget.checked)} + /> + + + + + +
+
+ ) +} + diff --git a/app/management/person/collection/detail/components/SamplingPlanContainer.tsx b/app/management/person/collection/detail/components/SamplingPlanContainer.tsx new file mode 100644 index 0000000..34c266d --- /dev/null +++ b/app/management/person/collection/detail/components/SamplingPlanContainer.tsx @@ -0,0 +1,136 @@ +"use client" + +import { projectConfig } from "@/components/label/api/project" +import { ProjectDetail } from "@/components/label/api/project/typing" +import { Button, Checkbox, Group, NumberInput, Paper, SimpleGrid, Stack, Text } from "@mantine/core" +import { useForm } from "@mantine/form" +import { notifications } from "@mantine/notifications" +import { useEffect } from "react" + +type GradeKey = "A" | "B" | "C" + +export default function SamplingPlanContainer(props: { + projectId: number + info: ProjectDetail.DataProps | undefined + onUpdated: () => void +}) { + const { projectId, info, onUpdated } = props + + const form = useForm({ + initialValues: { + check_task: false, + check_frame: false, + A_task: 0, + A_frame: 0, + B_task: 0, + B_frame: 0, + C_task: 0, + C_frame: 0, + }, + }) + + useEffect(() => { + if (!info) return + const grade = info.user_grade ?? ({} as any) + const toPercent = (v?: number) => Number(((v ?? 0) * 100).toFixed(2)) + form.setValues({ + check_task: !!info.check_task, + check_frame: !!info.check_frame, + A_task: toPercent(grade.A?.task), + A_frame: toPercent(grade.A?.frame), + B_task: toPercent(grade.B?.task), + B_frame: toPercent(grade.B?.frame), + C_task: toPercent(grade.C?.task), + C_frame: toPercent(grade.C?.frame), + }) + form.resetDirty() + }, [info, form]) + + const save = async () => { + try { + const toRatio = (v: number) => Number((v / 100).toFixed(4)) + await projectConfig({ + project_id: projectId, + check_task: form.values.check_task, + check_frame: form.values.check_frame, + user_grade: { + A: { task: toRatio(form.values.A_task), frame: toRatio(form.values.A_frame) }, + B: { task: toRatio(form.values.B_task), frame: toRatio(form.values.B_frame) }, + C: { task: toRatio(form.values.C_task), frame: toRatio(form.values.C_frame) }, + }, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存抽检配置", + }) + onUpdated() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + return ( + + + + 抽检方案 + + + + + form.setFieldValue("check_task", e.currentTarget.checked)} + /> + form.setFieldValue("check_frame", e.currentTarget.checked)} + /> + + + + {(["A", "B", "C"] as GradeKey[]).map((g) => ( + + + 等级 {g} + form.setFieldValue(`${g}_task` as any, Number(v ?? 0))} + hideControls + /> + form.setFieldValue(`${g}_frame` as any, Number(v ?? 0))} + hideControls + /> + + + ))} + + + + ) +} + diff --git a/app/management/person/collection/detail/components/StageUserModal.tsx b/app/management/person/collection/detail/components/StageUserModal.tsx new file mode 100644 index 0000000..0ace126 --- /dev/null +++ b/app/management/person/collection/detail/components/StageUserModal.tsx @@ -0,0 +1,55 @@ +"use client" + +import { Button, Group, Modal, MultiSelect, Stack } from "@mantine/core" +import { useForm } from "@mantine/form" +import { useEffect } from "react" + +export default function StageUserModal(props: { + opened: boolean + title: string + value: number[] + options: Array<{ label: string; value: string }> + onClose: (next?: number[]) => void +}) { + const { opened, title, value, options, onClose } = props + + const form = useForm({ + initialValues: { users: value.map(String) }, + }) + + useEffect(() => { + if (!opened) return + form.setValues({ users: value.map(String) }) + form.resetDirty() + }, [opened, value, form]) + + return ( + onClose()} title={title} centered size={560}> +
{ + const next = values.users + .map((v) => Number(v)) + .filter((v) => Number.isFinite(v)) + onClose(next) + })}> + + form.setFieldValue("users", v)} + /> + + + + + +
+
+ ) +} + diff --git a/app/management/person/collection/detail/components/TaskStageTabsContainer.tsx b/app/management/person/collection/detail/components/TaskStageTabsContainer.tsx new file mode 100644 index 0000000..c1bf20c --- /dev/null +++ b/app/management/person/collection/detail/components/TaskStageTabsContainer.tsx @@ -0,0 +1,139 @@ +"use client" + +import { projectConfig } from "@/components/label/api/project" +import { ProjectDetail } from "@/components/label/api/project/typing" +import { Button, Group, Paper, Stack, Text } from "@mantine/core" +import { notifications } from "@mantine/notifications" +import { useMemo, useState } from "react" +import StageUserModal from "./StageUserModal" + +type StageKey = "label" | "review1" | "review2" + +function stageTitle(stage: StageKey) { + if (stage === "label") return "标注阶段" + if (stage === "review1") return "审核阶段" + return "复审阶段" +} + +function stageAction(stage: StageKey) { + if (stage === "label") return 1 + if (stage === "review1") return 2 + return 3 +} + +export default function TaskStageTabsContainer(props: { + projectId: number + info: ProjectDetail.DataProps | undefined + userOptions: Array<{ label: string; value: string }> + userEnums: { + allEnum: Map + labelEnum: Map + review1Enum: Map + review2Enum: Map + } + onUpdated: () => void +}) { + const { projectId, info, userOptions, userEnums, onUpdated } = props + + const stageUsers = useMemo(() => { + const detail = info?.label_process?.step_detail ?? [] + const findUsers = (action: number) => { + const item = detail.find((d) => d.action === action) + return (item?.user_list ?? []) as number[] + } + return { + label: findUsers(1), + review1: findUsers(2), + review2: findUsers(3), + } + }, [info]) + + const [editingStage, setEditingStage] = useState(null) + + const currentStageValue = useMemo(() => { + if (!editingStage) return [] + return stageUsers[editingStage] ?? [] + }, [editingStage, stageUsers]) + + const saveStageUsers = async (stage: StageKey, users: number[]) => { + try { + if (stage === "label") { + await projectConfig({ project_id: projectId, label_users: users }) + } else if (stage === "review1") { + await projectConfig({ project_id: projectId, review1_users: users }) + } else { + await projectConfig({ project_id: projectId, review2_users: users }) + } + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存阶段人员", + }) + onUpdated() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + const renderUsersText = (uids: number[]) => { + const names = uids.map((uid) => { + const name = userEnums.allEnum.get(uid) + return name ? String(name) : String(uid) + }) + return names.length ? names.join("、") : "-" + } + + return ( + <> + + + 任务工序配置 + + {(["label", "review1", "review2"] as StageKey[]).map((stage) => ( + + + + {stageTitle(stage)} + + + + {renderUsersText(stageUsers[stage] ?? [])} + + + + ))} + + + + + { + const stage = editingStage + setEditingStage(null) + if (!stage || !next) return + await saveStageUsers(stage, next) + }} + /> + + ) +} + diff --git a/app/management/person/collection/detail/components/TaskStatusTag.tsx b/app/management/person/collection/detail/components/TaskStatusTag.tsx new file mode 100644 index 0000000..92b90b6 --- /dev/null +++ b/app/management/person/collection/detail/components/TaskStatusTag.tsx @@ -0,0 +1,45 @@ +"use client" + +import { TaskStatusEnum } from "@/components/label/utils/constants" +import { Badge, Group } from "@mantine/core" +import { useMemo } from "react" + +function statusColor(status?: number | null) { + switch (status) { + case 1: + return "yellow" + case 2: + case 4: + case 6: + return "blue" + case 3: + case 5: + case 7: + return "green" + case 8: + return "red" + default: + return "gray" + } +} + +export default function TaskStatusTag(props: { + status: number + rejected?: boolean + center?: boolean +}) { + const text = useMemo(() => TaskStatusEnum.get(props.status) || "默认", [props.status]) + return ( + + + {text} + + {props.rejected ? ( + + 返工 + + ) : null} + + ) +} + diff --git a/app/management/person/collection/detail/components/WorkflowModal.tsx b/app/management/person/collection/detail/components/WorkflowModal.tsx new file mode 100644 index 0000000..5f1b8ff --- /dev/null +++ b/app/management/person/collection/detail/components/WorkflowModal.tsx @@ -0,0 +1,81 @@ +"use client" + +import { getTaskWorkFlow } from "@/components/label/api/task" +import { Button, Group, Modal, ScrollArea, Stack, Text } from "@mantine/core" +import { notifications } from "@mantine/notifications" +import { useEffect, useState } from "react" + +export default function WorkflowModal(props: { + opened: boolean + taskId: number | null + onClose: () => void +}) { + const { opened, taskId, onClose } = props + const [loading, setLoading] = useState(false) + const [records, setRecords] = useState([]) + + useEffect(() => { + if (!opened || !taskId) return + const run = async () => { + setLoading(true) + try { + const res = await getTaskWorkFlow(taskId) + setRecords(res ?? []) + } catch (e) { + setRecords([]) + notifications.show({ + color: "red", + title: "加载失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } finally { + setLoading(false) + } + } + run() + }, [opened, taskId]) + + return ( + + + + + {records.length ? ( + records.map((r, idx) => ( + + {JSON.stringify(r, null, 2)} + + )) + ) : ( + + {loading ? "加载中..." : "暂无数据"} + + )} + + + + + + + + ) +} + diff --git a/app/management/person/collection/detail/components/config.ts b/app/management/person/collection/detail/components/config.ts new file mode 100644 index 0000000..4677bd3 --- /dev/null +++ b/app/management/person/collection/detail/components/config.ts @@ -0,0 +1,72 @@ +type FormItemProps = { + label: string + name: string + type: "input" | "select" | "radio" + options?: Array<{ label: any; value: any }> +} + +const monthOpt = Array.from({ length: 12 }).map((_, idx) => { + const n = idx + 1 + return { label: n, value: n } +}) + +export const confirmOpt = [ + { label: "否", value: false }, + { label: "是", value: true }, +] + +export const labelStageConfig: FormItemProps[] = [ + { label: "可领取最多任务条数", name: "max_size", type: "input" }, + { label: "单次可领取任务条数", name: "acquire_size", type: "input" }, + { label: "标注配置更新月份", name: "label_update_month", type: "select", options: monthOpt }, + { label: "动态标注倍速", name: "dynamic_label_speed", type: "input" }, + { label: "静态标注倍速", name: "static_label_speed", type: "input" }, + { label: "标注准确率", name: "label_accuracy_rate", type: "input" }, + { label: "标注结果数量倍数", name: "avg_remind_threshold", type: "input" }, + { label: "标注结果数量告警阈值", name: "fix_remind_threshold", type: "input" }, + { + label: "领取顺序", + name: "acquire_mode", + type: "radio", + options: [ + { label: "顺序领取", value: 0 }, + { label: "乱序领取", value: 1 }, + ], + }, +] + +export const reviewStageConfig: FormItemProps[] = [ + { label: "可领取最多任务条数", name: "max_size", type: "input" }, + { label: "单次可领取任务条数", name: "acquire_size", type: "input" }, + { label: "审核配置更新月份", name: "review1_update_month", type: "select", options: monthOpt }, + { label: "动态审核倍速", name: "dynamic_review1_speed", type: "input" }, + { label: "静态审核倍速", name: "static_review1_speed", type: "input" }, + { label: "审核准确率", name: "review1_accuracy_rate", type: "input" }, + { + label: "领取顺序", + name: "acquire_mode", + type: "radio", + options: [ + { label: "顺序领取", value: 0 }, + { label: "乱序领取", value: 1 }, + ], + }, +] + +export const recheckStageConfig: FormItemProps[] = [ + { label: "可领取最多任务条数", name: "max_size", type: "input" }, + { label: "单次可领取任务条数", name: "acquire_size", type: "input" }, + { label: "复审配置更新月份", name: "review2_update_month", type: "select", options: monthOpt }, + { label: "动态复审倍速", name: "dynamic_review2_speed", type: "input" }, + { label: "静态复审倍速", name: "static_review2_speed", type: "input" }, + { + label: "领取顺序", + name: "acquire_mode", + type: "radio", + options: [ + { label: "顺序领取", value: 0 }, + { label: "乱序领取", value: 1 }, + ], + }, +] + diff --git a/app/management/person/collection/detail/page.tsx b/app/management/person/collection/detail/page.tsx new file mode 100644 index 0000000..8af8824 --- /dev/null +++ b/app/management/person/collection/detail/page.tsx @@ -0,0 +1,206 @@ +"use client" + +import { getProjectDetailById } from "@/components/label/api/project" +import { ProjectDetail } from "@/components/label/api/project/typing" +import { useAllUserStore, useBackUrlStore } from "@/components/label/store/auth" +import { + Breadcrumbs, + Button, + Group, + Paper, + SegmentedControl, + Stack, + Text, + UnstyledButton, +} from "@mantine/core" +import { IconChevronLeft } from "@tabler/icons-react" +import { useRouter, useSearchParams } from "next/navigation" +import { useEffect, useMemo, useState } from "react" +import InfoSettingContainer from "./InfoSettingContainer" +import OwnTaskTableContainer from "./OwnTaskTableContainer" +import TaskBoardContainer from "./TaskBoardContainer" +import TaskTableContainer from "./TaskTableContainer" + +type TabKey = "own" | "all" | "setting" | "board" + +export default function CollectionDetailPage() { + const router = useRouter() + const urlParams = useSearchParams() + const type = urlParams.get("type") + const id = urlParams.get("id") + const projectId = + typeof id === "string" && !isNaN(Number(id)) ? Number(id) : -1 + + const { backTitle, setBackProps } = useBackUrlStore() + const { userOpts } = useAllUserStore() + + const [selectKey, setSelectedKey] = useState( + (backTitle as TabKey) || "own" + ) + const [info, setInfo] = useState() + const [userEnums, setUserEnums] = useState<{ + allEnum: Map + labelEnum: Map + review1Enum: Map + review2Enum: Map + }>({ + allEnum: new Map(), + labelEnum: new Map(), + review1Enum: new Map(), + review2Enum: new Map(), + }) + + useEffect(() => { + if (projectId === -1) return + const getProjectInfo = async () => { + const res = await getProjectDetailById(projectId) + setInfo(res) + } + getProjectInfo() + }, [projectId]) + + const updateProjectInfo = async () => { + if (projectId === -1) return + const res = await getProjectDetailById(projectId) + setInfo(res) + } + + useEffect(() => { + if (!info?.label_process) return + const { step_detail } = info.label_process + let label: number[] = [] + let review1: number[] = [] + let review2: number[] = [] + step_detail.forEach(({ action, user_list }: any) => { + if (action === 1) label = user_list + else if (action === 2) review1 = user_list + else if (action === 3) review2 = user_list + }) + const all = Array.from(new Set([...label, ...review1, ...review2])) + + const toLabelMap = (uids: number[]) => + new Map( + uids.map((uid) => { + const opt = userOpts.find((o) => o.value === uid) + return [uid, opt?.label ?? String(uid)] + }) + ) + + queueMicrotask(() => { + setUserEnums({ + allEnum: toLabelMap(all), + labelEnum: toLabelMap(label), + review1Enum: toLabelMap(review1), + review2Enum: toLabelMap(review2), + }) + }) + }, [info, userOpts]) + + const breadcrumbsItems = useMemo(() => { + return [ + { + title: type + ? type === "collect" + ? "我的收藏" + : type === "dynamic" + ? "动态项目" + : "全部项目" + : "我的收藏", + href: type + ? `/management/person/collection?type=${type}` + : "/management/person/collection?type=collect", + }, + { + title: info?.name || "项目名称", + href: "", + }, + ].map((item) => ( + { + if (!item.href) return + router.push(item.href) + }}> + + {item.title} + + + )) + }, [info?.name, router, type]) + + return ( + + + + + + {breadcrumbsItems} + + { + const next = v as TabKey + setSelectedKey(next) + const backUrl = type + ? `/management/person/collection/detail?id=${projectId}&type=${type}` + : `/management/person/collection/detail?id=${projectId}` + setBackProps(backUrl, next) + }} + /> + + + + + + {selectKey === "own" ? ( + + ) : null} + {selectKey === "all" ? ( + + ) : null} + {selectKey === "board" ? : null} + {selectKey === "setting" ? ( + + ) : null} + + + + ) +} diff --git a/app/management/person/collection/detail/util.ts b/app/management/person/collection/detail/util.ts new file mode 100644 index 0000000..3dd4414 --- /dev/null +++ b/app/management/person/collection/detail/util.ts @@ -0,0 +1 @@ +export { runCommand } from "@/app/management/person/dashboard/components/util" diff --git a/app/management/person/collection/page.tsx b/app/management/person/collection/page.tsx new file mode 100644 index 0000000..dbbe593 --- /dev/null +++ b/app/management/person/collection/page.tsx @@ -0,0 +1,536 @@ +"use client" + +import { + getProjectList, + getProjectTypeList, + projectCollect, +} from "@/components/label/api/project" +import { Project } from "@/components/label/api/project/typing" +import { + initAllParams, + initCollectParams, + initDynamicParams, + useAllUserStore, + useProjectStore, +} from "@/components/label/store/auth" +import { + projectLabelTypeOpts, + projectStatusOpts, + STATUS_CODE, +} from "@/components/label/utils/constants" +import { + ActionIcon, + Badge, + Button, + Collapse, + Group, + Paper, + Select, + SimpleGrid, + Stack, + Text, + TextInput, +} from "@mantine/core" +import { notifications } from "@mantine/notifications" +import { + IconChevronDown, + IconChevronUp, + IconRefresh, + IconSearch, + IconStar, + IconStarFilled, +} from "@tabler/icons-react" +import dayjs from "dayjs" +import { DataTable, DataTableColumn } from "mantine-datatable" +import { useRouter, useSearchParams } from "next/navigation" +import { useCallback, useEffect, useMemo, useState } from "react" + +type PageType = "all" | "collect" | "dynamic" + +function statusBadgeColor(status?: number | null) { + if (!status) return "gray" + if ([191, 199, 700].includes(status)) return "red" + if ([580].includes(status)) return "yellow" + if ([600].includes(status)) return "green" + if ([201, 202, 203].includes(status)) return "cyan" + return "blue" +} + +function parseStatusValue(value?: string) { + if (!value) return undefined + if (value.includes(",")) { + return value + .split(",") + .map((v) => Number(v)) + .filter((n) => Number.isFinite(n)) + } + const n = Number(value) + return Number.isFinite(n) ? [n] : undefined +} + +export default function CollectionPage() { + const router = useRouter() + const urlParams = useSearchParams() + const type = (urlParams.get("type") as PageType | null) ?? "collect" + + const { userOpts } = useAllUserStore() + const { + allParams, + allTotal, + collectParams, + collectTotal, + dynamicParams, + dynamicTotal, + resetParams, + setSearchParams, + setTotal, + }: any = useProjectStore() + + const storeParams = useMemo(() => { + return type === "collect" + ? collectParams + : type === "dynamic" + ? dynamicParams + : allParams + }, [allParams, collectParams, dynamicParams, type]) + + const total = useMemo(() => { + return type === "collect" + ? collectTotal + : type === "dynamic" + ? dynamicTotal + : allTotal + }, [allTotal, collectTotal, dynamicTotal, type]) + + const [typeOpts, setTypeOpts] = useState< + Array<{ label: string; value: string }> + >([]) + const [loading, setLoading] = useState(false) + const [records, setRecords] = useState([]) + + const [searchExpanded, setSearchExpanded] = useState(false) + const [filters, setFilters] = useState(() => { + return { + project_name: "", + project_type: "", + status: "", + owner: "", + label_type: "", + admin_user: "", + start_date: dayjs().subtract(6, "day").format("YYYY-MM-DD"), + end_date: dayjs().format("YYYY-MM-DD"), + } + }) + + const [appliedFilters, setAppliedFilters] = useState(filters) + const [queryTrigger, setQueryTrigger] = useState(0) + + useEffect(() => { + const initFormOpts = async () => { + const res = await getProjectTypeList() + setTypeOpts((res ?? []).map((name) => ({ label: name, value: name }))) + } + initFormOpts() + }, []) + + const queryParams = useMemo(() => { + const base = + type === "collect" + ? { ...initCollectParams } + : type === "dynamic" + ? { ...initDynamicParams } + : { ...initAllParams } + + const params: Project.ListRequest & any = { + ...base, + ...storeParams, + page_number: storeParams.page_number ?? 1, + page_size: storeParams.page_size ?? 15, + project_name: appliedFilters.project_name || undefined, + project_type: appliedFilters.project_type || undefined, + owner: appliedFilters.owner || undefined, + admin_user: appliedFilters.admin_user || undefined, + start_date: appliedFilters.start_date || undefined, + end_date: appliedFilters.end_date || undefined, + label_type: appliedFilters.label_type + ? Number(appliedFilters.label_type) + : undefined, + status: parseStatusValue(appliedFilters.status), + } + + return params + }, [appliedFilters, storeParams, type]) + + const load = useCallback(async () => { + if (!queryTrigger) return + setLoading(true) + try { + const res = await getProjectList(queryParams) + setRecords(res?.project_list ?? []) + setTotal(res?.total_items ?? 0, type) + } catch (e) { + setRecords([]) + setTotal(0, type) + notifications.show({ + color: "red", + title: "加载失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } finally { + setLoading(false) + } + }, [queryParams, queryTrigger, setTotal, type]) + + useEffect(() => { + load() + }, [load]) + + useEffect(() => { + queueMicrotask(() => { + setQueryTrigger((v) => v + 1) + }) + }, []) + + const columns = useMemo(() => { + const cols: DataTableColumn[] = [ + { + accessor: "id", + title: "项目ID", + width: 90, + }, + { + accessor: "name", + title: "项目名称", + width: 220, + render: (record) => ( + + ), + }, + { + accessor: "project_type", + title: "所属业务", + width: 140, + render: (record) => record.project_type ?? "-", + }, + { + accessor: "status", + title: "状态", + width: 110, + render: (record) => ( + + {STATUS_CODE.get(record.status) ?? record.status} + + ), + }, + { + accessor: "owner", + title: "负责人", + width: 120, + render: (record) => record.owner ?? "-", + }, + { + accessor: "label_schema_name", + title: "标注方案", + width: 160, + render: (record) => record.label_schema_name ?? "-", + }, + { + accessor: "update_at", + title: "更新时间", + width: 160, + render: (record) => record.update_at ?? record.create_at ?? "-", + }, + { + accessor: "operation", + title: "操作", + width: 80, + textAlign: "center", + render: (record) => ( + { + try { + await projectCollect({ + project_id: record.id, + is_collect: !record.is_collect, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: record.is_collect ? "已取消收藏" : "已收藏", + }) + load() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + }}> + {record.is_collect ? ( + + ) : ( + + )} + + ), + }, + ] + return cols + }, [load, router, type]) + + return ( + + + + {/* { + const next = v as PageType + router.replace(`/management/person/collection?type=${next}`) + }} + /> */} + + 我的收藏 + + + + + + + + + + + + + setFilters((s) => ({ + ...s, + project_name: e.currentTarget.value, + })) + } + /> + ({ + label: o.label, + value: o.label, + }))} + value={filters.status} + onChange={(v) => setFilters((s) => ({ ...s, status: v ?? "" }))} + /> + + setFilters((s) => ({ ...s, owner: e.currentTarget.value })) + } + /> + ({ + label: o.label, + value: String(o.value), + }))} + value={filters.admin_user || null} + onChange={(v) => + setFilters((s) => ({ ...s, admin_user: v ?? "" })) + } + /> + + setFilters((s) => ({ + ...s, + start_date: e.currentTarget.value, + })) + } + /> + + setFilters((s) => ({ ...s, end_date: e.currentTarget.value })) + } + /> + + + + + + + + + width="100%" + style={{ width: "100%" }} + withTableBorder + withRowBorders + pinFirstColumn + pinLastColumn + scrollAreaProps={{ type: "auto" }} + fetching={loading} + records={records} + columns={columns} + totalRecords={total} + recordsPerPage={storeParams.page_size ?? 15} + page={storeParams.page_number ?? 1} + onPageChange={(p) => { + setSearchParams({ ...storeParams, page_number: p }, type) + setQueryTrigger((v) => v + 1) + }} + onRecordsPerPageChange={(s) => { + setSearchParams( + { ...storeParams, page_number: 1, page_size: s }, + type + ) + setQueryTrigger((v) => v + 1) + }} + recordsPerPageOptions={[10, 15, 20, 50]} + noRecordsText="暂无数据" + /> + + + + ) +}