"use client" import { getProjectList, getProjectTypeList, projectAccept, projectCollect, projectDeliver, projectEditById, projectPreLabelReview, } from "@/components/label/api/project" import { Project } from "@/components/label/api/project/typing" import { initAllParams, initCollectParams, initDynamicParams, useAllUserStore, usePermissionStore, useProjectStore, } from "@/components/label/store/auth" import { ALL_CODE, projectLabelTypeOpts, projectStatusOpts, STATUS_CODE, } from "@/components/label/utils/constants" import { SettingContentPanel, SettingFilterActions, SettingFilterPanel, SettingHeaderActions, SettingListHeader, SettingPage, SettingPanel, } from "@/components/setting/PageSurface" import { ActionIcon, Badge, Button, Collapse, CopyButton, Divider, Flex, Group, Loader, Modal, MultiSelect, Pagination, Select, SimpleGrid, Stack, Text, Textarea, TextInput, Tooltip, UnstyledButton, } from "@mantine/core" import { useForm } from "@mantine/form" import { notifications } from "@mantine/notifications" import { IconCalendar, IconCheck, IconChevronDown, IconChevronUp, IconCopy, IconEdit, IconFileUpload, IconInfoCircle, IconRefresh, IconSearch, IconStar, IconStarFilled, } from "@tabler/icons-react" import { User2 } from "lucide-react" import { useRouter } from "next/navigation" import { useCallback, useEffect, useMemo, useState, type ReactNode, } from "react" import useAuth from "../label/hooks/useAuth" import { CardCoverIcon } from "./components/ProjectIcon" 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 } function formatPercent(value?: number | null) { if (value === 0) return "0.00%" if (!value) return "-" return `${(value * 100).toFixed(2)}%` } function getProcessText(record: any) { const rateSpeed = record?.rate_speed if (!rateSpeed || typeof rateSpeed !== "object") return "-" const map = new Map([ ["标注", "label_speed"], ["审核", "review1_speed"], ["复审", "review2_speed"], ]) const arr = ["标注", "审核", "复审"] const parts = arr.map((process) => { const key = map.get(process) const staticKey = key ? `static_${key}` : "" const dynamicKey = key ? `dynamic_${key}` : "" const staticValue = staticKey && (rateSpeed[staticKey] || rateSpeed[staticKey] === 0) ? rateSpeed[staticKey] : "-" const dynamicValue = dynamicKey && (rateSpeed[dynamicKey] || rateSpeed[dynamicKey] === 0) ? rateSpeed[dynamicKey] : "-" return `${process}(${staticValue}/${dynamicValue})` }) return parts.join(",") } export default function ProjectInfoPage({ type, }: { type: "dynamic" | "collect" | "" }) { const router = useRouter() const storeType = type || "all" const { userOpts } = useAllUserStore() const userName = usePermissionStore((s) => s.user_name) const { allParams, allTotal, collectParams, collectTotal, dynamicParams, dynamicTotal, resetParams, setSearchParams, setTotal, }: any = useProjectStore() const storeParams = useMemo(() => { return storeType === "collect" ? collectParams : storeType === "dynamic" ? dynamicParams : allParams }, [allParams, collectParams, dynamicParams, storeType]) const total = useMemo(() => { return storeType === "collect" ? collectTotal : storeType === "dynamic" ? dynamicTotal : allTotal }, [allTotal, collectTotal, dynamicTotal, storeType]) 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: [] as string[], start_date: "", end_date: "", } }) const [appliedFilters, setAppliedFilters] = useState(filters) const [queryTrigger, setQueryTrigger] = useState(0) useEffect(() => { const initFormOpts = async () => { const res = await getProjectTypeList() setTypeOpts( (Object.keys(res) ?? []).map((name) => ({ label: name, value: name })) ) } initFormOpts() }, []) const queryParams = useMemo(() => { const base = storeType === "collect" ? { ...initCollectParams } : storeType === "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?.length ? appliedFilters.admin_user.map((n) => Number(n)) : undefined, start_date: appliedFilters.start_date || undefined, end_date: appliedFilters.end_date || undefined, label_type: appliedFilters.label_type ? Number(appliedFilters.label_type) : undefined, // 0: 图片标注 1: 点云标注 2: 视频标注 project_data_type: 0, status: parseStatusValue(appliedFilters.status) ?? [...ALL_CODE], } return params }, [appliedFilters, storeParams, storeType]) const load = useCallback(async () => { if (!queryTrigger) return setLoading(true) try { const res = await getProjectList(queryParams) setRecords(res?.project_list ?? []) setTotal(res?.total_items ?? 0, storeType) } catch (e) { setRecords([]) setTotal(0, storeType) notifications.show({ color: "red", title: "加载失败", message: e instanceof Error ? e.message : "请求失败", }) } finally { setLoading(false) } }, [queryParams, queryTrigger, setTotal, storeType]) useEffect(() => { load() }, [load]) useEffect(() => { queueMicrotask(() => { setQueryTrigger((v) => v + 1) }) }, []) const currentPage = storeParams.page_number ?? 1 const pageSize = storeParams.page_size ?? 15 const [infoModalRecord, setInfoModalRecord] = useState(null) const [editModalRecord, setEditModalRecord] = useState(null) const [confirmAction, setConfirmAction] = useState<{ record: Project.DataProps | null action: "试标完成" | "试标通过" | "提交验收" | "验收通过" | null }>({ record: null, action: null }) const editForm = useForm<{ project_name: string // owner: string remarks?: string members?: string[] }>({ initialValues: { project_name: "", // owner: "", remarks: "", members: [], }, }) const confirmActionTitle = useMemo(() => { return confirmAction.action ?? "" }, [confirmAction.action]) const runConfirmAction = useCallback(async () => { const record = confirmAction.record const action = confirmAction.action if (!record || !action) return try { if (action === "试标完成") { await projectPreLabelReview({ id: record.id, review_user: userName ?? "", action: 1, }) } else if (action === "试标通过") { await projectPreLabelReview({ id: record.id, review_user: userName ?? "", action: 2, }) } else if (action === "提交验收") { await projectDeliver({ project_id: record.id }) } else if (action === "验收通过") { await projectAccept({ project_id: record.id, pass: true }) } notifications.show({ color: "green", title: "操作成功", message: `${action} 已提交`, }) setConfirmAction({ record: null, action: null }) setQueryTrigger((v) => v + 1) } catch (e) { notifications.show({ color: "red", title: "操作失败", message: e instanceof Error ? e.message : "请求失败", }) } }, [confirmAction.action, confirmAction.record, userName]) const { isShow } = useAuth() const ProjectCard = (record: Project.DataProps) => { const status = record.status const canEdit = status < 600 const actionKey = status === 201 ? "试标完成" : status === 202 ? "试标通过" : status === 500 ? "提交验收" : status === 580 ? "验收通过" : null const canDoAction = !!actionKey return ( { const url = type ? `/project/detail?id=${record.id}&type=${type}` : `/project/detail?id=${record.id}` router.push(url) }}> {record.name} {STATUS_CODE.get(record.status) ?? record.status} {record.owner ?? "-"} {record.create_at ? record.create_at.split(" ")[0] : "-"} { try { await projectCollect({ project_id: record.id, is_collect: !record.is_collect, }) notifications.show({ color: "green", title: "操作成功", message: record.is_collect ? "已取消收藏" : "已收藏", }) setQueryTrigger((v) => v + 1) } catch (e) { notifications.show({ color: "red", title: "操作失败", message: e instanceof Error ? e.message : "请求失败", }) } }}> {record.is_collect ? ( ) : ( )} 备注:{record.remarks ? record.remarks : "暂无备注"}
{isShow("edit") && ( { if (!canEdit) return editForm.setValues({ project_name: record.name ?? "", // owner: record.owner ?? "", remarks: record.remarks ?? "", members: record.members?.map((m) => String(m)) ?? [], }) editForm.resetDirty() setEditModalRecord(record) }}> )}
{ if (!actionKey) return setConfirmAction({ record, action: actionKey }) }}>
setInfoModalRecord(record)}>
) } const PaginationBar = (props: { currentPage: number pageSize: number total: number onChangePage: (page: number) => void onChangePageSize: (size: number) => void }) => { const pageCount = Math.max( 1, Math.ceil((props.total ?? 0) / props.pageSize) ) return (
setFilters((s) => ({ ...s, project_type: v ?? "" })) } /> ({ label: o.label, value: String(o.value), }))} value={filters.label_type || null} onChange={(v) => setFilters((s) => ({ ...s, label_type: v ?? "" })) } /> ({ label: o.label, value: String(o.value), }))} value={filters.admin_user} onChange={(v) => setFilters((s) => ({ ...s, admin_user: v }))} searchable clearable /> setFilters((s) => ({ ...s, start_date: e.currentTarget.value, })) } /> setFilters((s) => ({ ...s, end_date: e.currentTarget.value })) } /> } />
{loading ? (
) : records.length ? (
{records.map(ProjectCard)}
) : (
暂无数据
)}
{ setSearchParams({ ...storeParams, page_number: p }, storeType) setQueryTrigger((v) => v + 1) }} onChangePageSize={(s) => { setSearchParams( { ...storeParams, page_number: 1, page_size: s }, storeType ) setQueryTrigger((v) => v + 1) }} />
setInfoModalRecord(null)} title="项目信息" centered size={850}> {(() => { const record = infoModalRecord const adminUsers = record?.admin_user && Array.isArray(record.admin_user) ? record.admin_user .map((uid) => userOpts.find((o) => o.value === uid)?.label) .filter(Boolean) : [] const labelSchemaText = record?.label_schema_name ? `${record.label_schema_name}${ record.label_schema_version ? ` - ${record.label_schema_version}` : "" }` : "-" const progressText = `${formatPercent(record?.labeled_scale)} / ${formatPercent( record?.reviewed_scale )}` const processText = record ? getProcessText(record) : "-" const Row = (props: { title: ReactNode; children: ReactNode }) => (
{props.title}
{props.children}
) const EllipsisText = (props: { value?: string | null copyable?: boolean }) => { const value = props.value if (!value) return - const textNode = ( {value} ) return props.copyable ? (
{textNode}
{({ copied, copy }) => ( {copied ? ( ) : ( )} )}
) : (
{textNode}
) } return (
{record?.id ?? "-"} {record?.status ? ( {STATUS_CODE.get(record.status) ?? record.status} ) : ( - )} {progressText} {record?.data_size ?? "-"} {record?.owner ?? "-"} 工序 }> {record?.create_at ?? "-"} {record?.update_at ?? "-"}
) })()}
setEditModalRecord(null)} title="编辑项目" centered size={480}>
{ const record = editModalRecord if (!record) return try { await projectEditById( { project_name: values.project_name, // owner: values.owner || undefined, remarks: values.remarks || undefined, members: values.members?.map((v) => Number(v)) || undefined, }, record.id ) notifications.show({ color: "green", title: "编辑成功", message: "已保存", }) setEditModalRecord(null) setQueryTrigger((v) => v + 1) } catch (e) { notifications.show({ color: "red", title: "编辑失败", message: e instanceof Error ? e.message : "请求失败", }) } })}> {/* */} ({ label: o.label, value: String(o.value), }))} {...editForm.getInputProps("members")} />