370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
getProjectAdminList,
|
||
projectConfig,
|
||
} from "@/components/label/api/project"
|
||
import { ProjectDetail } from "@/components/label/api/project/typing"
|
||
import { useAllUserStore } from "@/components/label/store/auth"
|
||
import {
|
||
ActionIcon,
|
||
Collapse,
|
||
Group,
|
||
Paper,
|
||
ScrollArea,
|
||
SimpleGrid,
|
||
Stack,
|
||
Switch,
|
||
Text,
|
||
} from "@mantine/core"
|
||
import { notifications } from "@mantine/notifications"
|
||
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"
|
||
import SamplingPlanContainer from "./components/SamplingPlanContainer"
|
||
import TaskStageTabsContainer from "./components/TaskStageTabsContainer"
|
||
|
||
export default function InfoSettingContainer(props: {
|
||
info: ProjectDetail.DataProps | undefined
|
||
userEnums: {
|
||
allEnum: Map<any, any>
|
||
labelEnum: Map<any, any>
|
||
review1Enum: Map<any, any>
|
||
review2Enum: Map<any, any>
|
||
}
|
||
updateAction: () => void
|
||
}) {
|
||
const { info, userEnums, updateAction } = props
|
||
const projectId = info?.id ?? -1
|
||
|
||
const { userOpts } = useAllUserStore()
|
||
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<boolean>(
|
||
!!info?.is_lock_object
|
||
)
|
||
const [lockingEditValue, setLockingEditValue] = useState<boolean>(
|
||
!!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) => {
|
||
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: lockingEditValue,
|
||
})
|
||
notifications.show({
|
||
color: "green",
|
||
title: "操作成功",
|
||
message: "已保存配置",
|
||
})
|
||
setLockingValue(lockingEditValue)
|
||
setIsLockingEdit(false)
|
||
updateAction()
|
||
} catch (e) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "操作失败",
|
||
message: e instanceof Error ? e.message : "请求失败",
|
||
})
|
||
} finally {
|
||
setSavingLock(false)
|
||
}
|
||
}
|
||
|
||
const sectionTitle = (
|
||
key: keyof typeof show,
|
||
title: string,
|
||
color: "blue" | "orange" | "teal" | "grape"
|
||
) => {
|
||
const opened = show[key]
|
||
return (
|
||
<Group justify="space-between" wrap="nowrap">
|
||
<Group gap={8}>
|
||
<div
|
||
style={{
|
||
width: 3,
|
||
height: 14,
|
||
borderRadius: 2,
|
||
backgroundColor: `var(--mantine-color-${color}-6)`,
|
||
}}
|
||
/>
|
||
<Text fw={700}>{title}</Text>
|
||
</Group>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
onClick={() => setShow((s) => ({ ...s, [key]: !opened }))}>
|
||
{opened ? (
|
||
<IconChevronDown size={16} />
|
||
) : (
|
||
<IconChevronRight size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
</Group>
|
||
)
|
||
}
|
||
|
||
const finalAdminOptions =
|
||
adminOptions.length > 0 ? adminOptions : fallbackUserOptions
|
||
|
||
return (
|
||
<>
|
||
<Stack gap="md" style={{ flex: 1, minHeight: 0 }} w="100%">
|
||
<Paper
|
||
withBorder
|
||
p="sm"
|
||
radius="md"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Stack gap="sm">
|
||
<Group justify="space-between" wrap="wrap">
|
||
<Text fw={700}>项目配置</Text>
|
||
<ActionIcon variant="subtle" onClick={updateAction}>
|
||
<IconEdit size={16} />
|
||
</ActionIcon>
|
||
</Group>
|
||
|
||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="sm">
|
||
<Paper
|
||
withBorder
|
||
p="xs"
|
||
radius="sm"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Group justify="space-between" wrap="nowrap" align="center">
|
||
<Stack gap={2} style={{ minWidth: 0 }}>
|
||
<Text size="sm" fw={600}>
|
||
项目管理员
|
||
</Text>
|
||
<Text
|
||
size="sm"
|
||
c="dimmed"
|
||
style={{
|
||
maxWidth: 420,
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
whiteSpace: "nowrap",
|
||
}}>
|
||
{adminText}
|
||
</Text>
|
||
</Stack>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="blue"
|
||
onClick={() => {
|
||
setAdminModalOpen(true)
|
||
if (adminOptions.length === 0) {
|
||
queueMicrotask(loadAdminOptions)
|
||
}
|
||
}}>
|
||
<IconEdit size={16} />
|
||
</ActionIcon>
|
||
</Group>
|
||
</Paper>
|
||
|
||
<Paper
|
||
withBorder
|
||
p="xs"
|
||
radius="sm"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Group justify="space-between" wrap="nowrap" align="center">
|
||
<Stack gap={2}>
|
||
<Text size="sm" fw={600}>
|
||
图片对象锁定
|
||
</Text>
|
||
<Text size="sm" c="dimmed">
|
||
开启后对象可被锁定,避免多人同时标注冲突
|
||
</Text>
|
||
</Stack>
|
||
{isLockingEdit ? (
|
||
<Group gap={4} wrap="nowrap">
|
||
<Switch
|
||
checked={lockingEditValue}
|
||
onChange={(e) =>
|
||
setLockingEditValue(e.currentTarget.checked)
|
||
}
|
||
/>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
onClick={() => {
|
||
setLockingEditValue(lockingValue)
|
||
setIsLockingEdit(false)
|
||
}}>
|
||
<IconX size={16} />
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="blue"
|
||
loading={savingLock}
|
||
onClick={saveLock}>
|
||
<IconCheck size={16} />
|
||
</ActionIcon>
|
||
</Group>
|
||
) : (
|
||
<Group gap={8} wrap="nowrap">
|
||
<Switch checked={lockingValue} readOnly />
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="blue"
|
||
onClick={() => setIsLockingEdit(true)}>
|
||
<IconEdit size={16} />
|
||
</ActionIcon>
|
||
</Group>
|
||
)}
|
||
</Group>
|
||
</Paper>
|
||
</SimpleGrid>
|
||
|
||
<Group justify="flex-end">
|
||
<ActionIcon variant="subtle" onClick={updateAction}>
|
||
<IconCheck size={16} />
|
||
</ActionIcon>
|
||
<Text size="xs" c="dimmed">
|
||
刷新项目详情
|
||
</Text>
|
||
</Group>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
<Paper
|
||
withBorder
|
||
p="sm"
|
||
radius="md"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
flex: 1,
|
||
minHeight: 0,
|
||
}}>
|
||
<ScrollArea h="100%" type="auto">
|
||
<Stack gap="sm">
|
||
{sectionTitle("process", "任务流程", "blue")}
|
||
<Collapse in={show.process}>
|
||
<TaskStageTabsContainer
|
||
projectId={projectId}
|
||
info={info}
|
||
userOptions={fallbackUserOptions}
|
||
userEnums={userEnums}
|
||
onUpdatedAction={updateAction}
|
||
/>
|
||
</Collapse>
|
||
|
||
{sectionTitle("scheme", "标注方案", "orange")}
|
||
<Collapse in={show.scheme}>
|
||
<LabelSchemeContainer info={info} />
|
||
</Collapse>
|
||
|
||
{sectionTitle("sampling", "抽检方案", "teal")}
|
||
<Collapse in={show.sampling}>
|
||
<SamplingPlanContainer
|
||
projectId={projectId}
|
||
info={info}
|
||
onUpdated={updateAction}
|
||
/>
|
||
</Collapse>
|
||
|
||
{sectionTitle("note", "批注规则", "grape")}
|
||
<Collapse in={show.note}>
|
||
<NoteRulesTable info={info} onUpdated={updateAction} />
|
||
</Collapse>
|
||
</Stack>
|
||
</ScrollArea>
|
||
</Paper>
|
||
</Stack>
|
||
|
||
<AdminUserModal
|
||
opened={adminModalOpen}
|
||
value={info?.admin_user ?? []}
|
||
options={finalAdminOptions}
|
||
onCloseAction={async (next) => {
|
||
setAdminModalOpen(false)
|
||
if (!next || projectId === -1) return
|
||
try {
|
||
await projectConfig({ project_id: projectId, admin_user: next })
|
||
notifications.show({
|
||
color: "green",
|
||
title: "操作成功",
|
||
message: "已保存项目管理员",
|
||
})
|
||
updateAction()
|
||
} catch (e) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "操作失败",
|
||
message: e instanceof Error ? e.message : "请求失败",
|
||
})
|
||
}
|
||
}}
|
||
/>
|
||
</>
|
||
)
|
||
}
|