feat(project/audit): add audit page

This commit is contained in:
2026-02-06 16:10:07 +08:00
parent e159270fdd
commit f0298b91e3
4 changed files with 1285 additions and 0 deletions

View File

@@ -0,0 +1,459 @@
"use client"
import {
checkLabelDataSource,
getProjectAdminList,
getProjectTypeList,
projectAdd,
} from "@/components/label/api/project"
import {
getAllVersionByName,
getLabelSchemeList,
} from "@/components/label/api/scheme"
import { usePermissionStore } from "@/components/label/store/auth"
import {
Alert,
Button,
Group,
Modal,
MultiSelect,
Radio,
Select,
Stack,
Switch,
Text,
TextInput,
Textarea,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { notifications } from "@mantine/notifications"
import { IconAlertTriangle } from "@tabler/icons-react"
import { useEffect, useMemo, useRef, useState } from "react"
export default function NewProjectModal(props: {
opened: boolean
onCloseAction: (refresh?: boolean) => void
}) {
const { opened, onCloseAction } = props
const userName = usePermissionStore((s) => s.user_name)
const [typeOpts, setTypeOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [labelSchemeOpts, setLabelSchemeOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [versionOpts, setVersionOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [adminUserOpts, setAdminUserOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [dataSize, setDataSize] = useState<number>(0)
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const form = useForm({
initialValues: {
project_name: "",
project_type: "",
is_dynamic: false,
data_source_radio: "" as "" | "0" | "1",
data_source: "",
label_schema_name: "",
label_schema_version: "",
is_embedding: false,
admin_user: [] as string[],
remark: "",
},
validate: {
project_name: (v) => {
if (!v) return "必填"
if (/^\s*$/.test(v)) return "项目名称不能全为空格"
return null
},
project_type: (v) => (!v ? "必填" : null),
label_schema_name: (v) => (!v ? "必填" : null),
label_schema_version: (v) => (!v ? "必填" : null),
admin_user: (v) => (v && v.length ? null : "必填"),
},
})
const isDynamic = form.values.is_dynamic
const projectType = form.values.project_type
const labelSchema = form.values.label_schema_name
useEffect(() => {
if (!opened) return
const initFormOpts = async () => {
try {
const [schemeRes, adminRes, typeRes] = await Promise.all([
getLabelSchemeList({ page_number: 1, page_size: 5000 } as any),
getProjectAdminList(),
getProjectTypeList(),
])
const schemes = (schemeRes as any)?.label_schema_list ?? []
setLabelSchemeOpts(
schemes.map((item: any) => ({ label: item.name, value: item.name }))
)
setAdminUserOpts(
(adminRes ?? []).map((item: any) => ({
label: item.label,
value: String(item.value),
}))
)
setTypeOpts(
(typeRes ?? []).map((name) => ({ label: name, value: name }))
)
} catch {
setLabelSchemeOpts([])
setAdminUserOpts([])
setTypeOpts([])
}
}
initFormOpts()
}, [opened])
useEffect(() => {
if (!opened) return
const run = async () => {
form.setFieldValue("label_schema_version", "")
setVersionOpts([])
if (!labelSchema) return
try {
const res = await getAllVersionByName(labelSchema)
const list = (res as any)?.label_schema_version_list ?? []
setVersionOpts(
list.map((item: any) => ({
label: item.version,
value: item.version,
}))
)
} catch {
setVersionOpts([])
}
}
run()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [labelSchema, opened])
useEffect(() => {
if (!opened) return
if (isDynamic) {
form.setFieldValue("data_source_radio", "")
form.setFieldValue("data_source", "")
queueMicrotask(() => setDataSize(0))
form.clearFieldError("data_source")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDynamic, opened])
const triggerCheckDataSource = (value: string) => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
if (!value) {
setDataSize(0)
form.clearFieldError("data_source")
return
}
const radio = form.values.data_source_radio
if (!projectType) {
setDataSize(0)
form.setFieldError("data_source", "请先选择所属业务")
return
}
if (radio !== "0" && radio !== "1") {
setDataSize(0)
form.setFieldError("data_source", "请先选择标注数据源对应来源类型")
return
}
debounceTimerRef.current = setTimeout(async () => {
try {
const res = await checkLabelDataSource({
project_type: projectType,
nfs_path: value,
flag: Number(radio),
})
const errorCode = (res as any)?.error_code
const size = (res as any)?.data_size ?? 0
const errorMap = new Map<number, string>([
[1, "nfs路径不存在"],
[2, "nfs路径下不存在data目录"],
[3, "nfs路径下不存在tag目录"],
[4, "tag文件数量小于data文件数量"],
[5, "pattern.json文件不存在"],
[6, "clip.json文件不合法"],
[7, "tag.json文件不合法"],
[8, "pattern.json文件不合法"],
])
if (errorCode === 0) {
setDataSize(size)
form.clearFieldError("data_source")
} else if (errorMap.has(errorCode)) {
setDataSize(0)
form.setFieldError(
"data_source",
errorMap.get(errorCode) || "标注数据源填写有误"
)
} else {
setDataSize(0)
form.setFieldError("data_source", "标注数据源填写有误")
}
} catch (e) {
setDataSize(0)
form.setFieldError(
"data_source",
e instanceof Error ? e.message : "标注数据源校验失败"
)
}
}, 1000)
}
const canSubmit = useMemo(() => {
if (!opened) return false
if (!form.values.project_name || /^\s*$/.test(form.values.project_name))
return false
if (!form.values.project_type) return false
if (!form.values.label_schema_name || !form.values.label_schema_version)
return false
if (!form.values.admin_user.length) return false
if (!form.values.is_dynamic) {
if (!form.values.data_source_radio) return false
if (!form.values.data_source) return false
if (form.errors.data_source) return false
}
return true
}, [form.errors.data_source, form.values, opened])
return (
<Modal
opened={opened}
onClose={() => onCloseAction()}
title="新增标注任务"
centered
size="50%"
closeOnClickOutside={false}>
<Stack gap="sm">
<Alert
color="yellow"
radius="xs"
p="xs"
icon={<IconAlertTriangle size={16} />}
title="提示">
MDS模块进行数据处理
</Alert>
<form
onSubmit={form.onSubmit(async (values) => {
if (!canSubmit) return
const data: any = { ...values }
data.is_dynamic = values.is_dynamic ? 1 : 0
data.is_embedding = values.is_embedding ? 1 : 0
data.data_source = values.data_source ?? ""
data.create_user = userName ?? ""
data.data_size = dataSize
data.admin_user = values.admin_user
.map((v) => Number(v))
.filter((n) => Number.isFinite(n))
data.remarks = values.remark || undefined
delete data.remark
delete data.data_source_radio
try {
await projectAdd(data)
notifications.show({
color: "green",
title: "项目创建成功",
message: "已创建项目",
})
form.reset()
setDataSize(0)
onCloseAction(true)
} catch (e) {
notifications.show({
color: "red",
title: "创建失败",
message: e instanceof Error ? e.message : "请求失败",
})
}
})}>
<div
style={{ maxHeight: "58vh", overflowY: "auto", paddingRight: 6 }}>
<Stack gap="sm">
<TextInput
label="项目名称"
radius="xs"
size="xs"
placeholder="请输入项目名称"
withAsterisk
{...form.getInputProps("project_name")}
/>
<Select
label="所属业务"
radius="xs"
size="xs"
placeholder="请选择所属业务"
data={typeOpts}
searchable
clearable
withAsterisk
value={form.values.project_type || null}
onChange={(v) => form.setFieldValue("project_type", v ?? "")}
error={form.errors.project_type}
/>
<Switch
label="是否为动态任务"
size="sm"
checked={form.values.is_dynamic}
onChange={(e) =>
form.setFieldValue("is_dynamic", e.currentTarget.checked)
}
/>
{!isDynamic ? (
<>
<Radio.Group
label="标注数据源"
size="xs"
withAsterisk
value={form.values.data_source_radio}
onChange={(v) => {
const next = v as "0" | "1" | ""
form.setFieldValue("data_source_radio", next)
if (form.values.data_source)
triggerCheckDataSource(form.values.data_source)
}}>
<Group gap="md" mt={6}>
<Radio
value="0"
label="新建数据源"
size="xs"
disabled={!projectType}
/>
<Radio
value="1"
label="现存数据源"
size="xs"
disabled={!projectType}
/>
</Group>
</Radio.Group>
<TextInput
label="标注数据源"
size="xs"
radius="xs"
placeholder="请输入标注数据源"
withAsterisk
disabled={!projectType}
value={form.values.data_source}
onChange={(e) => {
const v = e.currentTarget.value
form.setFieldValue("data_source", v)
triggerCheckDataSource(v)
}}
error={form.errors.data_source}
rightSection={
dataSize > 0 && form.values.data_source ? (
<Text size="xs" c="blue" style={{ paddingRight: 6 }}>
{dataSize}
</Text>
) : undefined
}
/>
</>
) : null}
<Select
label="标注方案"
size="xs"
radius="xs"
placeholder="请选择标注方案"
data={labelSchemeOpts}
searchable
clearable
withAsterisk
value={form.values.label_schema_name || null}
onChange={(v) =>
form.setFieldValue("label_schema_name", v ?? "")
}
error={form.errors.label_schema_name}
/>
<Select
label="方案版本"
size="xs"
radius="xs"
placeholder="请选择标注方案版本号"
data={versionOpts}
searchable
clearable
withAsterisk
value={form.values.label_schema_version || null}
onChange={(v) =>
form.setFieldValue("label_schema_version", v ?? "")
}
error={form.errors.label_schema_version}
/>
<Switch
label="是否生成embedding"
size="sm"
checked={form.values.is_embedding}
onChange={(e) =>
form.setFieldValue("is_embedding", e.currentTarget.checked)
}
/>
<MultiSelect
label="标注管理员"
size="xs"
radius="xs"
placeholder="请选择标注管理员"
data={adminUserOpts}
searchable
clearable
withAsterisk
value={form.values.admin_user}
onChange={(v) => form.setFieldValue("admin_user", v)}
error={form.errors.admin_user}
/>
<Textarea
label="备注"
size="xs"
radius="xs"
placeholder="请输入"
maxLength={50}
autosize
minRows={3}
value={form.values.remark}
onChange={(e) =>
form.setFieldValue("remark", e.currentTarget.value)
}
/>
</Stack>
</div>
<Group justify="flex-end" mt="md" gap="sm">
<Button
variant="default"
size="xs"
radius="xs"
onClick={() => onCloseAction()}>
</Button>
<Button type="submit" size="xs" radius="xs" disabled={!canSubmit}>
</Button>
</Group>
</form>
</Stack>
</Modal>
)
}