diff --git a/app/management/project/detail/InfoSettingContainer.tsx b/app/management/project/detail/InfoSettingContainer.tsx index 96cb17c..85f6798 100644 --- a/app/management/project/detail/InfoSettingContainer.tsx +++ b/app/management/project/detail/InfoSettingContainer.tsx @@ -1,11 +1,31 @@ "use client" -import { projectConfig } from "@/components/label/api/project" +import { + getProjectAdminList, + 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 { + ActionIcon, + Collapse, + Group, + Paper, + ScrollArea, + SimpleGrid, + Stack, + Switch, + Text, +} from "@mantine/core" import { notifications } from "@mantine/notifications" -import { useMemo, useState } from "react" +import { + IconCheck, + IconChevronDown, + IconChevronRight, + IconEdit, + IconX, +} from "@tabler/icons-react" +import { useCallback, useEffect, useMemo, useState } from "react" import AdminUserModal from "./components/AdminUserModal" import LabelSchemeContainer from "./components/LabelSchemeContainer" import NoteRulesTable from "./components/NoteRulesTable" @@ -26,16 +46,55 @@ export default function InfoSettingContainer(props: { const projectId = info?.id ?? -1 const { userOpts } = useAllUserStore() - const userOptions = useMemo(() => { + const fallbackUserOptions = useMemo(() => { return userOpts.map((o) => ({ label: o.label, value: String(o.value) })) }, [userOpts]) + const [adminOptions, setAdminOptions] = useState< + Array<{ label: string; value: string }> + >([]) + const [adminModalOpen, setAdminModalOpen] = useState(false) const [lockingValue, setLockingValue] = useState( !!info?.is_lock_object ) + const [lockingEditValue, setLockingEditValue] = useState( + !!info?.is_lock_object + ) + const [isLockingEdit, setIsLockingEdit] = useState(false) const [savingLock, setSavingLock] = useState(false) + const [show, setShow] = useState({ + process: true, + scheme: true, + sampling: true, + note: true, + }) + + useEffect(() => { + const v = !!info?.is_lock_object + setLockingValue(v) + setLockingEditValue(v) + setIsLockingEdit(false) + }, [info?.is_lock_object]) + + const loadAdminOptions = useCallback(async () => { + try { + const res = await getProjectAdminList() + const opts = (res ?? []).map((item) => ({ + label: item.label, + value: String(item.value), + })) + setAdminOptions(opts) + } catch { + setAdminOptions([]) + } + }, []) + + useEffect(() => { + queueMicrotask(loadAdminOptions) + }, [loadAdminOptions]) + const adminText = useMemo(() => { const adminUids = info?.admin_user ?? [] const names = adminUids.map((uid) => { @@ -51,13 +110,15 @@ export default function InfoSettingContainer(props: { try { await projectConfig({ project_id: projectId, - is_lock_object: lockingValue, + is_lock_object: lockingEditValue, }) notifications.show({ color: "green", title: "操作成功", message: "已保存配置", }) + setLockingValue(lockingEditValue) + setIsLockingEdit(false) updateAction() } catch (e) { notifications.show({ @@ -70,9 +131,45 @@ export default function InfoSettingContainer(props: { } } + const sectionTitle = ( + key: keyof typeof show, + title: string, + color: "blue" | "orange" | "teal" | "grape" + ) => { + const opened = show[key] + return ( + + +
+ {title} + + setShow((s) => ({ ...s, [key]: !opened }))}> + {opened ? ( + + ) : ( + + )} + + + ) + } + + const finalAdminOptions = + adminOptions.length > 0 ? adminOptions : fallbackUserOptions + return ( <> - + - 基础配置 - - - - - - - 项目管理员 - - - {adminText} - - - - - - - - - 图片对象锁定 - - - 开启后对象可被锁定,避免多人同时标注冲突 - - - - setLockingValue(e.currentTarget.checked)} - /> - - + - + + + + {sectionTitle("process", "任务流程", "blue")} + + + - + {sectionTitle("scheme", "标注方案", "orange")} + + + - + {sectionTitle("sampling", "抽检方案", "teal")} + + + - + {sectionTitle("note", "批注规则", "grape")} + + + + + + { setAdminModalOpen(false) if (!next || projectId === -1) return diff --git a/app/management/project/detail/components/LabelSchemeContainer.tsx b/app/management/project/detail/components/LabelSchemeContainer.tsx index c4dfbae..bec4415 100644 --- a/app/management/project/detail/components/LabelSchemeContainer.tsx +++ b/app/management/project/detail/components/LabelSchemeContainer.tsx @@ -1,97 +1,224 @@ "use client" import { ProjectDetail } from "@/components/label/api/project/typing" -import { Badge, Group, Paper, Stack, Text } from "@mantine/core" +import { + labelTypeMap, + selectModeMap, + staticsColor, +} from "@/components/label/utils/constants" +import { Badge, Button, Group, Modal, Paper, Stack, Text } from "@mantine/core" import { DataTable, DataTableColumn } from "mantine-datatable" -import { useMemo } from "react" +import { useState } from "react" + +type DetailRow = ProjectDetail.SubAttributesDescribe + +function getColorIndex(color: number[] | undefined) { + if (!Array.isArray(color) || color.length < 3) return undefined + const key = color.slice(0, 3).join("_") + for (const item of staticsColor) { + const [colorKey, colorIndex] = Object.entries(item)[0] ?? [] + if (colorKey === key && typeof colorIndex === "number") { + return colorIndex + } + } + return undefined +} export default function LabelSchemeContainer(props: { info: ProjectDetail.DataProps | undefined }) { const schemas = props.info?.label_schema_list ?? [] + const [detailOpen, setDetailOpen] = useState(false) + const [detailRows, setDetailRows] = useState([]) - 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)" + const columns: DataTableColumn[] = [ + { + accessor: "label_class", + title: "类别", + width: 120, + }, + { + accessor: "super_category", + title: "属性", + width: 120, + render: (record) => record.super_category || "-", + }, + { + accessor: "category_id", + title: "ID", + width: 90, + }, + { + accessor: "label_type", + title: "标注方式", + width: 140, + render: (record) => ( + + {labelTypeMap.get(record.label_type) ?? + String(record.label_type ?? "-")} + + ), + }, + { + accessor: "color", + title: "标签颜色", + width: 130, + render: (record) => { + const color = Array.isArray(record.color) ? record.color : [] + const idx = getColorIndex(color) + if (color.length < 3) { return ( - -
- - {color.length >= 3 - ? `${color[0]},${color[1]},${color[2]}` - : "-"} - - + + - + ) - }, + } + return ( + +
+ {idx ?? "-"} + + ) }, - { - accessor: "label_type", - title: "类型", - width: 90, - render: (record) => {record.label_type}, - }, - { - accessor: "sub_attributes", - title: "子属性", - width: 240, - render: (record) => ( + }, + { + accessor: "sub_attributes", + title: "子属性", + width: 180, + render: (record) => { + const attrs = Array.isArray(record.sub_attributes) + ? record.sub_attributes + : [] + return ( - {(record.sub_attributes ?? []).join("、") || "-"} + {attrs.length ? attrs.join("、") : "-"} - ), + ) }, - ] - return cols - }, []) + }, + { + accessor: "sub_attributes_describe", + title: "子属性描述", + width: 120, + textAlign: "center", + render: (record) => { + const details = Array.isArray(record.sub_attributes_describe) + ? record.sub_attributes_describe + : [] + if (!details.length) { + return ( + + - + + ) + } + return ( + + ) + }, + }, + ] + + const detailColumns: DataTableColumn[] = [ + { + accessor: "chinese_name", + title: "子属性", + width: 140, + render: (record) => record.chinese_name || "-", + }, + { + accessor: "select_mode", + title: "输入方式", + width: 100, + render: (record) => + selectModeMap.get(record.select_mode) ?? + String(record.select_mode ?? "-"), + }, + { + accessor: "isrequired", + title: "是否必填", + width: 90, + render: (record) => (record.isrequired === 1 ? "是" : "否"), + }, + { + accessor: "optional_item", + title: "可选属性值", + render: (record) => { + const opts = Array.isArray(record.optional_item) + ? record.optional_item + : [] + return opts.length ? opts.join(";") : "-" + }, + }, + ] return ( - - - 标注方案 - + <> + + + 标注方案 + + withTableBorder + withRowBorders + idAccessor="category_id" + records={schemas} + columns={columns} + minHeight={160} + noRecordsText="暂无数据" + /> + + + + { + setDetailOpen(false) + setDetailRows([]) + }} + title="子属性描述" + centered + size="70%"> + withTableBorder withRowBorders - idAccessor="category_id" - records={schemas} - columns={columns} - minHeight={160} + records={detailRows} + columns={detailColumns} noRecordsText="暂无数据" + minHeight={220} + idAccessor="chinese_name" /> - - + + ) } diff --git a/app/management/project/detail/components/NoteRulesTable.tsx b/app/management/project/detail/components/NoteRulesTable.tsx index 3c52d1c..4901b14 100644 --- a/app/management/project/detail/components/NoteRulesTable.tsx +++ b/app/management/project/detail/components/NoteRulesTable.tsx @@ -1,61 +1,278 @@ "use client" +import { projectConfig } from "@/components/label/api/project" import { ProjectDetail } from "@/components/label/api/project/typing" -import { List, Paper, Stack, Text } from "@mantine/core" +import { usePermissionStore } from "@/components/label/store/auth" +import { + ActionIcon, + Badge, + Button, + Group, + Modal, + Paper, + Stack, + Text, + TextInput, +} from "@mantine/core" +import { modals } from "@mantine/modals" +import { notifications } from "@mantine/notifications" +import { IconPlus, IconTrash } from "@tabler/icons-react" +import dayjs from "dayjs" +import { DataTable, DataTableColumn } from "mantine-datatable" +import { useMemo, useState } from "react" + +type RulePayload = { + text: string + create_user: string + create_date: string +} + +type RuleRow = RulePayload & { + index: number +} + +function normalizeRule(item: unknown): RulePayload { + if (typeof item === "string") { + return { text: item, create_user: "-", create_date: "-" } + } + if (item && typeof item === "object") { + const rule = item as { + text?: unknown + create_user?: unknown + create_date?: unknown + } + return { + text: String(rule.text ?? ""), + create_user: String(rule.create_user ?? "-"), + create_date: String(rule.create_date ?? "-"), + } + } + return { text: "", create_user: "-", create_date: "-" } +} export default function NoteRulesTable(props: { info: ProjectDetail.DataProps | undefined + onUpdated: () => void }) { - const rules = props.info?.comment_rule - const textRules = rules?.text_rules ?? [] - const checkBoxRules = rules?.check_box_rules ?? [] + const { info, onUpdated } = props + const projectId = info?.id ?? -1 + const userName = usePermissionStore((s) => s.user_name) + + const [open, setOpen] = useState(false) + const [inputText, setInputText] = useState("") + const [saving, setSaving] = useState(false) + + const textRules = useMemo(() => { + const rules = (info?.comment_rule as any)?.text_rules + return Array.isArray(rules) ? rules.map((r) => String(r ?? "")) : [] + }, [info?.comment_rule]) + + const checkBoxRules = useMemo(() => { + const raw = (info?.comment_rule as any)?.check_box_rules + const list = Array.isArray(raw) ? raw : [] + return list.map((item) => normalizeRule(item)) + }, [info?.comment_rule]) + + const rows = useMemo(() => { + return checkBoxRules.map((rule, index) => ({ ...rule, index })) + }, [checkBoxRules]) + + const saveRules = async (nextRules: RulePayload[], successText: string) => { + if (projectId === -1) return + setSaving(true) + try { + await projectConfig({ + project_id: projectId, + comment_rule: { + check_box_rules: nextRules, + text_rules: textRules, + }, + }) + notifications.show({ + color: "green", + title: "操作成功", + message: successText, + }) + setOpen(false) + setInputText("") + onUpdated() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } finally { + setSaving(false) + } + } + + const handleAdd = async () => { + const value = inputText.trim() + if (!value) { + notifications.show({ + color: "red", + title: "新增失败", + message: "批注内容不能为空", + }) + return + } + const exists = rows.some((row) => row.text.trim() === value) + if (exists) { + notifications.show({ + color: "red", + title: "新增失败", + message: "批注规则重复,请重新输入", + }) + return + } + const next: RulePayload[] = [ + ...checkBoxRules, + { + text: value, + create_user: userName ? String(userName) : "-", + create_date: dayjs().format("YYYY-MM-DD"), + }, + ] + await saveRules(next, "已添加规则") + } + + const handleDelete = async (row: RuleRow) => { + const next = checkBoxRules.filter((_, index) => index !== row.index) + await saveRules(next, "已删除规则") + } + + const columns: DataTableColumn[] = [ + { + accessor: "method", + title: "批注方式", + width: 90, + render: () => 勾选, + }, + { + accessor: "text", + title: "内容", + }, + { + accessor: "create_user", + title: "添加者", + width: 120, + }, + { + accessor: "create_date", + title: "添加日期", + width: 120, + }, + { + accessor: "operation", + title: "操作", + width: 90, + textAlign: "center", + render: (record) => ( + { + modals.openConfirmModal({ + title: "删除规则", + centered: true, + children: 确定删除该批注规则吗?, + labels: { confirm: "确定", cancel: "取消" }, + confirmProps: { color: "red" }, + onConfirm: () => handleDelete(record), + }) + }}> + + + ), + }, + ] return ( - - - 批注规则 + <> + + + + 批注规则 + + - - - 文本规则 - - {textRules.length ? ( - - {textRules.map((r, idx) => ( - {r} - ))} - - ) : ( - - - + + + 文本规则 - )} - + + {textRules.length ? textRules.join(";") : "-"} + + - - - 勾选规则 - - {checkBoxRules.length ? ( - - {checkBoxRules.map((r, idx) => ( - {String(r)} - ))} - - ) : ( - - - - - )} + + withTableBorder + withRowBorders + records={rows} + columns={columns} + noRecordsText="暂无规则" + minHeight={200} + idAccessor={(record) => + `${record.index}_${record.text}_${record.create_user}_${record.create_date}` + } + /> - - + + + { + if (saving) return + setOpen(false) + setInputText("") + }} + title="添加批注规则" + centered> + + + + 批注方式 + + 勾选 + + setInputText(e.currentTarget.value)} + /> + + + + + + + ) } diff --git a/app/management/project/detail/components/TaskStageTabsContainer.tsx b/app/management/project/detail/components/TaskStageTabsContainer.tsx index 0972504..c061436 100644 --- a/app/management/project/detail/components/TaskStageTabsContainer.tsx +++ b/app/management/project/detail/components/TaskStageTabsContainer.tsx @@ -1,25 +1,153 @@ "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 { Project, ProjectDetail } from "@/components/label/api/project/typing" +import { + ActionIcon, + Badge, + Button, + Group, + MultiSelect, + NumberInput, + Paper, + Select, + SegmentedControl, + SimpleGrid, + Stack, + Switch, + Tabs, + Text, + TextInput, +} from "@mantine/core" +import { modals } from "@mantine/modals" import { notifications } from "@mantine/notifications" -import { useMemo, useState } from "react" +import { IconEdit, IconPlus, IconX } from "@tabler/icons-react" +import { DataTable, DataTableColumn } from "mantine-datatable" +import { ReactNode, useEffect, useMemo, useState } from "react" import StageUserModal from "./StageUserModal" +import { + confirmOpt, + labelStageConfig, + recheckStageConfig, + reviewStageConfig, +} from "./config" type StageKey = "label" | "review1" | "review2" -function stageTitle(stage: StageKey) { - if (stage === "label") return "标注阶段" - if (stage === "review1") return "审核阶段" - return "复审阶段" +type StageUserRow = { + uid: number + name: string } -// function stageAction(stage: StageKey) { -// if (stage === "label") return 1 -// if (stage === "review1") return 2 -// return 3 -// } +type LabelForm = { + max_size?: number + acquire_size?: number + label_update_month?: number + dynamic_label_speed?: number + static_label_speed?: number + label_accuracy_rate?: number + avg_remind_threshold?: number + fix_remind_threshold?: number + acquire_mode?: number + filter_pre_label: boolean + pre_label_confirm: boolean +} + +type Review1Form = { + max_size?: number + acquire_size?: number + review1_update_month?: number + dynamic_review1_speed?: number + static_review1_speed?: number + review1_accuracy_rate?: number + acquire_mode?: number + filter_task_batch: boolean + start_date: string + end_date: string + filter_label_first_sub: boolean + label_first_sub_start_date: string + label_first_sub_end_date: string + filter_track_user: boolean + track_user_list: string[] + filter_objs: boolean + min_objs?: number + max_objs?: number + filter_rework: boolean + is_rework: boolean +} + +type Review2Form = { + max_size?: number + acquire_size?: number + review2_update_month?: number + dynamic_review2_speed?: number + static_review2_speed?: number + acquire_mode?: number + filter_task_batch: boolean + start_date: string + end_date: string + filter_review1_first_sub: boolean + review1_first_sub_start_date: string + review1_first_sub_end_date: string + filter_track_user: boolean + track_user_list: string[] + filter_review1_user: boolean + review1_user_list: string[] + filter_objs: boolean + min_objs?: number + max_objs?: number + filter_rework: boolean + is_rework: boolean +} + +function stageLabel(stage: StageKey) { + if (stage === "label") return "标注" + if (stage === "review1") return "审核" + return "复审" +} + +function normalizeNumber(v: unknown) { + const n = Number(v) + return Number.isFinite(n) ? n : undefined +} + +function uidText( + uid: number, + userEnums: { + allEnum: Map + labelEnum: Map + review1Enum: Map + review2Enum: Map + }, + userOptions: Array<{ label: string; value: string }> +) { + const enumName = userEnums.allEnum.get(uid) + if (enumName) return String(enumName) + const opt = userOptions.find((o) => Number(o.value) === uid) + if (opt) return opt.label + return String(uid) +} + +function toNumberArray(values: string[]) { + return values.map((v) => Number(v)).filter((v) => Number.isFinite(v)) +} + +function mapOptionLabel( + options: Array<{ label: any; value: any }> | undefined, + value: unknown +) { + if (!options) return String(value ?? "-") + const opt = options.find((o) => o.value === value) + return opt ? String(opt.label) : String(value ?? "-") +} + +function boolToSelectValue(v: boolean | undefined) { + return v ? "true" : "false" +} + +function selectValueToBool(v: string | null) { + return v === "true" +} export default function TaskStageTabsContainer(props: { projectId: number @@ -35,27 +163,219 @@ export default function TaskStageTabsContainer(props: { }) { const { projectId, info, userOptions, userEnums, onUpdatedAction } = props + const [activeKey, setActiveKey] = useState("label") + const [batchEditStage, setBatchEditStage] = useState(null) + + const [labelEditing, setLabelEditing] = useState(false) + const [review1Editing, setReview1Editing] = useState(false) + const [review2Editing, setReview2Editing] = useState(false) + + const [labelForm, setLabelForm] = useState({ + filter_pre_label: false, + pre_label_confirm: false, + }) + const [labelInit, setLabelInit] = useState({ + filter_pre_label: false, + pre_label_confirm: false, + }) + + const [review1Form, setReview1Form] = useState({ + filter_task_batch: false, + start_date: "", + end_date: "", + filter_label_first_sub: false, + label_first_sub_start_date: "", + label_first_sub_end_date: "", + filter_track_user: false, + track_user_list: [], + filter_objs: false, + filter_rework: false, + is_rework: false, + }) + const [review1Init, setReview1Init] = useState({ + filter_task_batch: false, + start_date: "", + end_date: "", + filter_label_first_sub: false, + label_first_sub_start_date: "", + label_first_sub_end_date: "", + filter_track_user: false, + track_user_list: [], + filter_objs: false, + filter_rework: false, + is_rework: false, + }) + + const [review2Form, setReview2Form] = useState({ + filter_task_batch: false, + start_date: "", + end_date: "", + filter_review1_first_sub: false, + review1_first_sub_start_date: "", + review1_first_sub_end_date: "", + filter_track_user: false, + track_user_list: [], + filter_review1_user: false, + review1_user_list: [], + filter_objs: false, + filter_rework: false, + is_rework: false, + }) + const [review2Init, setReview2Init] = useState({ + filter_task_batch: false, + start_date: "", + end_date: "", + filter_review1_first_sub: false, + review1_first_sub_start_date: "", + review1_first_sub_end_date: "", + filter_track_user: false, + track_user_list: [], + filter_review1_user: false, + review1_user_list: [], + filter_objs: false, + filter_rework: false, + is_rework: false, + }) + 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[] + const details = info?.label_process?.step_detail ?? [] + const getByAction = (action: number) => { + const row = details.find((d) => d.action === action) + return (row?.user_list ?? []) as number[] } return { - label: findUsers(1), - review1: findUsers(2), - review2: findUsers(3), + label: getByAction(1), + review1: getByAction(2), + review2: getByAction(3), } }, [info]) - const [editingStage, setEditingStage] = useState(null) + const labelUserRows = useMemo(() => { + return stageUsers.label.map((uid) => ({ + uid, + name: uidText(uid, userEnums, userOptions), + })) + }, [stageUsers.label, userEnums, userOptions]) - const currentStageValue = useMemo(() => { - if (!editingStage) return [] - return stageUsers[editingStage] ?? [] - }, [editingStage, stageUsers]) + const review1UserRows = useMemo(() => { + return stageUsers.review1.map((uid) => ({ + uid, + name: uidText(uid, userEnums, userOptions), + })) + }, [stageUsers.review1, userEnums, userOptions]) - const saveStageUsers = async (stage: StageKey, users: number[]) => { + const review2UserRows = useMemo(() => { + return stageUsers.review2.map((uid) => ({ + uid, + name: uidText(uid, userEnums, userOptions), + })) + }, [stageUsers.review2, userEnums, userOptions]) + + const labelUserOpts = useMemo(() => { + return labelUserRows.map((u) => ({ label: u.name, value: String(u.uid) })) + }, [labelUserRows]) + + const review1UserOpts = useMemo(() => { + return review1UserRows.map((u) => ({ label: u.name, value: String(u.uid) })) + }, [review1UserRows]) + + useEffect(() => { + if (!info) return + + const alarm = info.alarm_threshold ?? ({} as any) + const multiplier = info.labeled_multiplier ?? ({} as any) + const process = info.label_process ?? ({} as any) + const details = process.step_detail ?? [] + const filters = process.filter_task_info ?? [] + + const labelDetail = details.find((d: any) => d.action === 1) + const review1Detail = details.find((d: any) => d.action === 2) + const review2Detail = details.find((d: any) => d.action === 3) + + const review1Filter = (filters.find((f: any) => f.action === 2) ?? + {}) as Partial + const review2Filter = (filters.find((f: any) => f.action === 3) ?? + {}) as Partial + + const nextLabel: LabelForm = { + max_size: normalizeNumber(labelDetail?.max_size), + acquire_size: normalizeNumber(labelDetail?.acquire_size), + label_update_month: normalizeNumber(multiplier.label_update_month), + dynamic_label_speed: normalizeNumber(multiplier.dynamic_label_speed), + static_label_speed: normalizeNumber(multiplier.static_label_speed), + label_accuracy_rate: normalizeNumber(multiplier.label_accuracy_rate), + avg_remind_threshold: normalizeNumber(alarm.avg_remind_threshold), + fix_remind_threshold: normalizeNumber(alarm.fix_remind_threshold), + acquire_mode: normalizeNumber(labelDetail?.acquire_mode), + filter_pre_label: !!process.filter_pre_label, + pre_label_confirm: !!process.pre_label_confirm, + } + + const nextReview1: Review1Form = { + max_size: normalizeNumber(review1Detail?.max_size), + acquire_size: normalizeNumber(review1Detail?.acquire_size), + review1_update_month: normalizeNumber(multiplier.review1_update_month), + dynamic_review1_speed: normalizeNumber(multiplier.dynamic_review1_speed), + static_review1_speed: normalizeNumber(multiplier.static_review1_speed), + review1_accuracy_rate: normalizeNumber(multiplier.review1_accuracy_rate), + acquire_mode: normalizeNumber(review1Detail?.acquire_mode), + filter_task_batch: !!review1Filter.filter_task_batch, + start_date: review1Filter.start_date ?? "", + end_date: review1Filter.end_date ?? "", + filter_label_first_sub: !!review1Filter.filter_label_first_sub, + label_first_sub_start_date: + review1Filter.label_first_sub_start_date ?? "", + label_first_sub_end_date: review1Filter.label_first_sub_end_date ?? "", + filter_track_user: !!review1Filter.filter_track_user, + track_user_list: (review1Filter.track_user_list ?? []).map(String), + filter_objs: !!review1Filter.filter_objs, + min_objs: normalizeNumber(review1Filter.min_objs), + max_objs: normalizeNumber(review1Filter.max_objs), + filter_rework: !!review1Filter.filter_rework, + is_rework: !!review1Filter.is_rework, + } + + const nextReview2: Review2Form = { + max_size: normalizeNumber(review2Detail?.max_size), + acquire_size: normalizeNumber(review2Detail?.acquire_size), + review2_update_month: normalizeNumber(multiplier.review2_update_month), + dynamic_review2_speed: normalizeNumber(multiplier.dynamic_review2_speed), + static_review2_speed: normalizeNumber(multiplier.static_review2_speed), + acquire_mode: normalizeNumber(review2Detail?.acquire_mode), + filter_task_batch: !!review2Filter.filter_task_batch, + start_date: review2Filter.start_date ?? "", + end_date: review2Filter.end_date ?? "", + filter_review1_first_sub: !!review2Filter.filter_review1_first_sub, + review1_first_sub_start_date: + review2Filter.review1_first_sub_start_date ?? "", + review1_first_sub_end_date: + review2Filter.review1_first_sub_end_date ?? "", + filter_track_user: !!review2Filter.filter_track_user, + track_user_list: (review2Filter.track_user_list ?? []).map(String), + filter_review1_user: !!review2Filter.filter_review1_user, + review1_user_list: (review2Filter.review1_user_list ?? []).map(String), + filter_objs: !!review2Filter.filter_objs, + min_objs: normalizeNumber(review2Filter.min_objs), + max_objs: normalizeNumber(review2Filter.max_objs), + filter_rework: !!review2Filter.filter_rework, + is_rework: !!review2Filter.is_rework, + } + + // Sync editable forms when upstream project info changes. + // eslint-disable-next-line react-hooks/set-state-in-effect + setLabelForm(nextLabel) + setLabelInit(nextLabel) + setReview1Form(nextReview1) + setReview1Init(nextReview1) + setReview2Form(nextReview2) + setReview2Init(nextReview2) + + setLabelEditing(false) + setReview1Editing(false) + setReview2Editing(false) + }, [info]) + + const saveUsers = async (stage: StageKey, users: number[]) => { try { if (stage === "label") { await projectConfig({ project_id: projectId, label_users: users }) @@ -67,7 +387,7 @@ export default function TaskStageTabsContainer(props: { notifications.show({ color: "green", title: "操作成功", - message: "已保存阶段人员", + message: `已更新${stageLabel(stage)}员`, }) onUpdatedAction() } catch (e) { @@ -79,12 +399,1049 @@ export default function TaskStageTabsContainer(props: { } } - 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("、") : "-" + const removeSingleUser = async (stage: StageKey, uid: number) => { + const users = stageUsers[stage].filter((u) => u !== uid) + await saveUsers(stage, users) + } + + const saveLabelConfig = async () => { + try { + const payload: Project.LabelConfig = { + max_size: normalizeNumber(labelForm.max_size) ?? 0, + acquire_size: normalizeNumber(labelForm.acquire_size) ?? 0, + label_update_month: normalizeNumber(labelForm.label_update_month) ?? 0, + dynamic_label_speed: + normalizeNumber(labelForm.dynamic_label_speed) ?? 0, + static_label_speed: normalizeNumber(labelForm.static_label_speed) ?? 0, + label_accuracy_rate: + normalizeNumber(labelForm.label_accuracy_rate) ?? 0, + avg_remind_threshold: + normalizeNumber(labelForm.avg_remind_threshold) ?? 0, + fix_remind_threshold: + normalizeNumber(labelForm.fix_remind_threshold) ?? 0, + acquire_mode: normalizeNumber(labelForm.acquire_mode) ?? 0, + filter_pre_label: !!labelForm.filter_pre_label, + pre_label_confirm: !!labelForm.pre_label_confirm, + } + await projectConfig({ project_id: projectId, label_config: payload }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存标注流程配置", + }) + setLabelEditing(false) + onUpdatedAction() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + const saveReview1Config = async () => { + try { + const payload: Project.Review1Config = { + max_size: normalizeNumber(review1Form.max_size) ?? 0, + acquire_size: normalizeNumber(review1Form.acquire_size) ?? 0, + review1_update_month: + normalizeNumber(review1Form.review1_update_month) ?? 0, + dynamic_review1_speed: + normalizeNumber(review1Form.dynamic_review1_speed) ?? 0, + static_review1_speed: + normalizeNumber(review1Form.static_review1_speed) ?? 0, + review1_accuracy_rate: + normalizeNumber(review1Form.review1_accuracy_rate) ?? 0, + acquire_mode: normalizeNumber(review1Form.acquire_mode) ?? 0, + filter_task_batch: !!review1Form.filter_task_batch, + start_date: review1Form.start_date || undefined, + end_date: review1Form.end_date || undefined, + filter_label_first_sub: !!review1Form.filter_label_first_sub, + label_first_sub_start_date: + review1Form.label_first_sub_start_date || undefined, + label_first_sub_end_date: + review1Form.label_first_sub_end_date || undefined, + filter_track_user: !!review1Form.filter_track_user, + track_user_list: toNumberArray(review1Form.track_user_list), + filter_objs: !!review1Form.filter_objs, + min_objs: normalizeNumber(review1Form.min_objs), + max_objs: normalizeNumber(review1Form.max_objs), + filter_rework: !!review1Form.filter_rework, + is_rework: !!review1Form.is_rework, + } + await projectConfig({ project_id: projectId, review1_config: payload }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存审核流程配置", + }) + setReview1Editing(false) + onUpdatedAction() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + const saveReview2Config = async () => { + try { + const payload: Project.Review2Config = { + max_size: normalizeNumber(review2Form.max_size) ?? 0, + acquire_size: normalizeNumber(review2Form.acquire_size) ?? 0, + review2_update_month: + normalizeNumber(review2Form.review2_update_month) ?? 0, + dynamic_review2_speed: + normalizeNumber(review2Form.dynamic_review2_speed) ?? 0, + static_review2_speed: + normalizeNumber(review2Form.static_review2_speed) ?? 0, + acquire_mode: normalizeNumber(review2Form.acquire_mode) ?? 0, + filter_task_batch: !!review2Form.filter_task_batch, + start_date: review2Form.start_date || undefined, + end_date: review2Form.end_date || undefined, + filter_review1_first_sub: !!review2Form.filter_review1_first_sub, + review1_first_sub_start_date: + review2Form.review1_first_sub_start_date || undefined, + review1_first_sub_end_date: + review2Form.review1_first_sub_end_date || undefined, + filter_track_user: !!review2Form.filter_track_user, + track_user_list: toNumberArray(review2Form.track_user_list), + filter_review1_user: !!review2Form.filter_review1_user, + review1_user_list: toNumberArray(review2Form.review1_user_list), + filter_objs: !!review2Form.filter_objs, + min_objs: normalizeNumber(review2Form.min_objs), + max_objs: normalizeNumber(review2Form.max_objs), + filter_rework: !!review2Form.filter_rework, + is_rework: !!review2Form.is_rework, + } + await projectConfig({ project_id: projectId, review2_config: payload }) + notifications.show({ + color: "green", + title: "操作成功", + message: "已保存复审流程配置", + }) + setReview2Editing(false) + onUpdatedAction() + } catch (e) { + notifications.show({ + color: "red", + title: "操作失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + } + + const userColumns = (stage: StageKey): DataTableColumn[] => { + return [ + { accessor: "name", title: "姓名" }, + { + accessor: "operation", + title: "操作", + width: 80, + textAlign: "center", + render: (record) => ( + { + modals.openConfirmModal({ + title: "移除人员", + centered: true, + children: ( + + 确定从{stageLabel(stage)}阶段移除该人员吗? + + ), + labels: { confirm: "确定", cancel: "取消" }, + confirmProps: { color: "red" }, + onConfirm: () => removeSingleUser(stage, record.uid), + }) + }}> + + + ), + }, + ] + } + + const labelRows = labelUserRows + const review1Rows = review1UserRows + const review2Rows = review2UserRows + + const optionsToData = (options?: Array<{ label: any; value: any }>) => + (options ?? []).map((o) => ({ + label: String(o.label), + value: String(o.value), + })) + + const renderLabelConfig = () => { + return ( + + {labelStageConfig.map((item) => { + const options = item.options ?? [] + const v = (labelForm as any)[item.name] + return ( + + + {item.label} + + {labelEditing ? ( + item.type === "input" ? ( + + setLabelForm((s) => ({ + ...s, + [item.name]: Number(value ?? 0), + })) + } + min={0} + max={item.name.includes("accuracy_rate") ? 100 : undefined} + suffix={ + item.name.includes("accuracy_rate") ? "%" : undefined + } + size="xs" + radius="xs" + hideControls + style={{ width: 180 }} + /> + ) : ( + + setReview1Form((s) => ({ + ...s, + [item.name]: value === null ? undefined : Number(value), + })) + } + size="xs" + radius="xs" + allowDeselect={false} + style={{ width: 180 }} + /> + ) + ) : ( + {mapOptionLabel(options, v)} + )} + + ) + })} + + + + + 任务批次过滤 + {review1Editing ? ( + + setReview1Form((s) => ({ + ...s, + filter_task_batch: e.currentTarget.checked, + })) + } + /> + ) : ( + + {review1Form.filter_task_batch ? "是" : "否"} + + )} + + + + setReview1Form((s) => ({ + ...s, + start_date: e.currentTarget.value, + })) + } + /> + + setReview1Form((s) => ({ + ...s, + end_date: e.currentTarget.value, + })) + } + /> + + + + + + + + 标注首次提交过滤 + {review1Editing ? ( + + setReview1Form((s) => ({ + ...s, + filter_label_first_sub: e.currentTarget.checked, + })) + } + /> + ) : ( + + {review1Form.filter_label_first_sub ? "是" : "否"} + + )} + + + + setReview1Form((s) => ({ + ...s, + label_first_sub_start_date: e.currentTarget.value, + })) + } + /> + + setReview1Form((s) => ({ + ...s, + label_first_sub_end_date: e.currentTarget.value, + })) + } + /> + + + + + + + + 标注员过滤 + {review1Editing ? ( + + setReview1Form((s) => ({ + ...s, + filter_track_user: e.currentTarget.checked, + })) + } + /> + ) : ( + + {review1Form.filter_track_user ? "是" : "否"} + + )} + + + + setReview1Form((s) => ({ ...s, track_user_list: v })) + } + style={{ flex: 1 }} + /> + {review1Editing ? ( + + ) : null} + + + + + + + + 对象数过滤 + {review1Editing ? ( + + setReview1Form((s) => ({ + ...s, + filter_objs: e.currentTarget.checked, + })) + } + /> + ) : ( + + {review1Form.filter_objs ? "是" : "否"} + + )} + + + + setReview1Form((s) => ({ ...s, min_objs: Number(v ?? 0) })) + } + /> + + setReview1Form((s) => ({ ...s, max_objs: Number(v ?? 0) })) + } + /> + + + + + + + 是否返工 + + {review1Editing ? ( + + + setReview1Form((s) => ({ + ...s, + filter_rework: e.currentTarget.checked, + })) + } + /> + ({ + label: o.label, + value: String(o.value), + }))} + value={boolToSelectValue(review1Form.is_rework)} + onChange={(value) => + setReview1Form((s) => ({ + ...s, + is_rework: selectValueToBool(value), + })) + } + size="xs" + disabled={!review1Form.filter_rework} + /> + + ) : ( + {review1Form.is_rework ? "是" : "否"} + )} + + + ) + } + + const renderReview2Config = () => { + return ( + + {recheckStageConfig.map((item) => { + const options = item.options ?? [] + const v = (review2Form as any)[item.name] + return ( + + + {item.label} + + {review2Editing ? ( + item.type === "input" ? ( + + setReview2Form((s) => ({ + ...s, + [item.name]: Number(value ?? 0), + })) + } + min={0} + size="xs" + radius="xs" + hideControls + style={{ width: 180 }} + /> + ) : ( +