You've already forked DataMate
Revert "feat: fix the problem in the Operator Market frontend pages"
This commit is contained in:
@@ -1,155 +1,155 @@
|
||||
import React from "react";
|
||||
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
interface TemplateDetailProps {
|
||||
visible: boolean;
|
||||
template?: AnnotationTemplate;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TemplateDetail: React.FC<TemplateDetailProps> = ({
|
||||
visible,
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="模板详情"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="名称" span={2}>
|
||||
{template.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>
|
||||
{template.description || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数据类型">
|
||||
<Tag color="cyan">{template.dataType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标注类型">
|
||||
<Tag color="geekblue">{template.labelingType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
<Tag color="blue">{template.category}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="样式">
|
||||
{template.style}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
<Tag color={template.builtIn ? "gold" : "default"}>
|
||||
{template.builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">
|
||||
{template.version}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间" span={2}>
|
||||
{new Date(template.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
{template.updatedAt && (
|
||||
<Descriptions.Item label="更新时间" span={2}>
|
||||
{new Date(template.updatedAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Divider>配置详情</Divider>
|
||||
|
||||
<Card title="数据对象" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{template.configuration.objects.map((obj, index) => (
|
||||
<Card key={index} size="small" type="inner">
|
||||
<Space>
|
||||
<Text strong>名称:</Text>
|
||||
<Tag>{obj.name}</Tag>
|
||||
<Text strong>类型:</Text>
|
||||
<Tag color="blue">{obj.type}</Tag>
|
||||
<Text strong>值:</Text>
|
||||
<Tag color="green">{obj.value}</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{template.configuration.labels.map((label, index) => (
|
||||
<Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Text strong>来源名称:</Text>
|
||||
<Tag>{label.fromName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>目标名称:</Text>
|
||||
<Tag>{label.toName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>类型:</Text>
|
||||
<Tag color="purple">{label.type}</Tag>
|
||||
|
||||
{label.required && <Tag color="red">必填</Tag>}
|
||||
</div>
|
||||
|
||||
{label.description && (
|
||||
<div>
|
||||
<Text strong>描述:</Text>
|
||||
<Text type="secondary">{label.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.options && label.options.length > 0 && (
|
||||
<div>
|
||||
<Text strong>选项:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.options.map((opt, i) => (
|
||||
<Tag key={i} color="cyan">{opt}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.labels && label.labels.length > 0 && (
|
||||
<div>
|
||||
<Text strong>标签:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.labels.map((lbl, i) => (
|
||||
<Tag key={i} color="geekblue">{lbl}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{template.labelConfig && (
|
||||
<Card title="Label Studio XML 配置" size="small">
|
||||
<Paragraph>
|
||||
<pre style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: "auto",
|
||||
maxHeight: 300
|
||||
}}>
|
||||
{template.labelConfig}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDetail;
|
||||
import React from "react";
|
||||
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
interface TemplateDetailProps {
|
||||
visible: boolean;
|
||||
template?: AnnotationTemplate;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TemplateDetail: React.FC<TemplateDetailProps> = ({
|
||||
visible,
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="模板详情"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="名称" span={2}>
|
||||
{template.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>
|
||||
{template.description || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数据类型">
|
||||
<Tag color="cyan">{template.dataType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标注类型">
|
||||
<Tag color="geekblue">{template.labelingType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
<Tag color="blue">{template.category}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="样式">
|
||||
{template.style}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
<Tag color={template.builtIn ? "gold" : "default"}>
|
||||
{template.builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">
|
||||
{template.version}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间" span={2}>
|
||||
{new Date(template.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
{template.updatedAt && (
|
||||
<Descriptions.Item label="更新时间" span={2}>
|
||||
{new Date(template.updatedAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Divider>配置详情</Divider>
|
||||
|
||||
<Card title="数据对象" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{template.configuration.objects.map((obj, index) => (
|
||||
<Card key={index} size="small" type="inner">
|
||||
<Space>
|
||||
<Text strong>名称:</Text>
|
||||
<Tag>{obj.name}</Tag>
|
||||
<Text strong>类型:</Text>
|
||||
<Tag color="blue">{obj.type}</Tag>
|
||||
<Text strong>值:</Text>
|
||||
<Tag color="green">{obj.value}</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{template.configuration.labels.map((label, index) => (
|
||||
<Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Text strong>来源名称:</Text>
|
||||
<Tag>{label.fromName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>目标名称:</Text>
|
||||
<Tag>{label.toName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>类型:</Text>
|
||||
<Tag color="purple">{label.type}</Tag>
|
||||
|
||||
{label.required && <Tag color="red">必填</Tag>}
|
||||
</div>
|
||||
|
||||
{label.description && (
|
||||
<div>
|
||||
<Text strong>描述:</Text>
|
||||
<Text type="secondary">{label.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.options && label.options.length > 0 && (
|
||||
<div>
|
||||
<Text strong>选项:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.options.map((opt, i) => (
|
||||
<Tag key={i} color="cyan">{opt}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.labels && label.labels.length > 0 && (
|
||||
<div>
|
||||
<Text strong>标签:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.labels.map((lbl, i) => (
|
||||
<Tag key={i} color="geekblue">{lbl}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{template.labelConfig && (
|
||||
<Card title="Label Studio XML 配置" size="small">
|
||||
<Paragraph>
|
||||
<pre style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: "auto",
|
||||
maxHeight: 300
|
||||
}}>
|
||||
{template.labelConfig}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDetail;
|
||||
|
||||
@@ -1,427 +1,427 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Divider,
|
||||
Card,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
createAnnotationTemplateUsingPost,
|
||||
updateAnnotationTemplateByIdUsingPut,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TagSelector from "./components/TagSelector";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface TemplateFormProps {
|
||||
visible: boolean;
|
||||
mode: "create" | "edit";
|
||||
template?: AnnotationTemplate;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
template,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && template && mode === "edit") {
|
||||
form.setFieldsValue({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
dataType: template.dataType,
|
||||
labelingType: template.labelingType,
|
||||
style: template.style,
|
||||
category: template.category,
|
||||
labels: template.configuration.labels,
|
||||
objects: template.configuration.objects,
|
||||
});
|
||||
} else if (visible && mode === "create") {
|
||||
form.resetFields();
|
||||
// Set default values
|
||||
form.setFieldsValue({
|
||||
style: "horizontal",
|
||||
category: "custom",
|
||||
labels: [],
|
||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||
});
|
||||
}
|
||||
}, [visible, template, mode, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
console.log("Form values:", values);
|
||||
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataType: values.dataType,
|
||||
labelingType: values.labelingType,
|
||||
style: values.style,
|
||||
category: values.category,
|
||||
configuration: {
|
||||
labels: values.labels,
|
||||
objects: values.objects,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Request data:", requestData);
|
||||
|
||||
let response;
|
||||
if (mode === "create") {
|
||||
response = await createAnnotationTemplateUsingPost(requestData);
|
||||
} else {
|
||||
response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success(`模板${mode === "create" ? "创建" : "更新"}成功`);
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
message.error("请填写所有必填字段");
|
||||
} else {
|
||||
message.error(`模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
console.error(error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsOptions = (type: string) => {
|
||||
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={mode === "create" ? "创建模板" : "编辑模板"}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:产品质量分类" maxLength={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea
|
||||
placeholder="描述此模板的用途"
|
||||
rows={2}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: "100%" }} size="large">
|
||||
<Form.Item
|
||||
label="数据类型"
|
||||
name="dataType"
|
||||
rules={[{ required: true, message: "请选择数据类型" }]}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<Select placeholder="选择数据类型">
|
||||
<Option value="image">图像</Option>
|
||||
<Option value="text">文本</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="video">视频</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注类型"
|
||||
name="labelingType"
|
||||
rules={[{ required: true, message: "请选择标注类型" }]}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
<Select placeholder="选择标注类型">
|
||||
<Option value="classification">分类</Option>
|
||||
<Option value="object-detection">目标检测</Option>
|
||||
<Option value="segmentation">分割</Option>
|
||||
<Option value="ner">命名实体识别</Option>
|
||||
<Option value="multi-stage">多阶段</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="样式"
|
||||
name="style"
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="horizontal">水平</Option>
|
||||
<Option value="vertical">垂直</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="分类"
|
||||
name="category"
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="computer-vision">计算机视觉</Option>
|
||||
<Option value="nlp">自然语言处理</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="quality-control">质量控制</Option>
|
||||
<Option value="custom">自定义</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider>数据对象</Divider>
|
||||
|
||||
<Form.List name="objects">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card key={field.key} size="small" style={{ marginBottom: 8 }}>
|
||||
<Space align="start" style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="名称"
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="例如:image" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<TagSelector type="object" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="值"
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{ required: true, message: "必填" },
|
||||
{ pattern: /^\$/, message: "必须以 $ 开头" },
|
||||
]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="$image" />
|
||||
</Form.Item>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<MinusCircleOutlined
|
||||
style={{ marginTop: 30, color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加对象
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider>标签控件</Divider>
|
||||
|
||||
<Form.List name="labels">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={
|
||||
<Space>
|
||||
<span>控件 {fields.indexOf(field) + 1}</span>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const controlType = form.getFieldValue(["labels", field.name, "type"]);
|
||||
const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
|
||||
if (controlType || fromName) {
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
|
||||
({fromName || '未命名'} - {controlType || '未设置类型'})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<MinusCircleOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="来源名称"
|
||||
name={[field.name, "fromName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="此控件的唯一标识符"
|
||||
>
|
||||
<Input placeholder="例如:choice" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="标注目标对象"
|
||||
name={[field.name, "toName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="选择此控件将标注哪个数据对象"
|
||||
dependencies={['objects']}
|
||||
>
|
||||
<Select placeholder="选择数据对象">
|
||||
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
|
||||
<Option key={idx} value={obj?.name || ''}>
|
||||
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="控件类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<TagSelector type="control" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label=" "
|
||||
name={[field.name, "required"]}
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Checkbox>必填</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
const prevType = prevValues.labels?.[field.name]?.type;
|
||||
const currType = currentValues.labels?.[field.name]?.type;
|
||||
return prevType !== currType;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const controlType = getFieldValue(["labels", field.name, "type"]);
|
||||
const fieldName = controlType === "Choices" ? "options" : "labels";
|
||||
|
||||
if (needsOptions(controlType)) {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={controlType === "Choices" ? "选项" : "标签"}
|
||||
name={[field.name, fieldName]}
|
||||
rules={[{ required: true, message: "至少需要一个选项" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
open={false}
|
||||
placeholder={
|
||||
controlType === "Choices"
|
||||
? "输入选项内容,按回车添加。例如:是、否、不确定"
|
||||
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Row 3: 描述 */}
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="描述"
|
||||
name={[field.name, "description"]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="向标注人员显示的帮助信息"
|
||||
>
|
||||
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() =>
|
||||
add({
|
||||
fromName: "",
|
||||
toName: "",
|
||||
type: "Choices",
|
||||
required: false,
|
||||
})
|
||||
}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加标签控件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateForm;
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Divider,
|
||||
Card,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
createAnnotationTemplateUsingPost,
|
||||
updateAnnotationTemplateByIdUsingPut,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TagSelector from "./components/TagSelector";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface TemplateFormProps {
|
||||
visible: boolean;
|
||||
mode: "create" | "edit";
|
||||
template?: AnnotationTemplate;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
template,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && template && mode === "edit") {
|
||||
form.setFieldsValue({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
dataType: template.dataType,
|
||||
labelingType: template.labelingType,
|
||||
style: template.style,
|
||||
category: template.category,
|
||||
labels: template.configuration.labels,
|
||||
objects: template.configuration.objects,
|
||||
});
|
||||
} else if (visible && mode === "create") {
|
||||
form.resetFields();
|
||||
// Set default values
|
||||
form.setFieldsValue({
|
||||
style: "horizontal",
|
||||
category: "custom",
|
||||
labels: [],
|
||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||
});
|
||||
}
|
||||
}, [visible, template, mode, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
console.log("Form values:", values);
|
||||
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataType: values.dataType,
|
||||
labelingType: values.labelingType,
|
||||
style: values.style,
|
||||
category: values.category,
|
||||
configuration: {
|
||||
labels: values.labels,
|
||||
objects: values.objects,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Request data:", requestData);
|
||||
|
||||
let response;
|
||||
if (mode === "create") {
|
||||
response = await createAnnotationTemplateUsingPost(requestData);
|
||||
} else {
|
||||
response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success(`模板${mode === "create" ? "创建" : "更新"}成功`);
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
message.error("请填写所有必填字段");
|
||||
} else {
|
||||
message.error(`模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
console.error(error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsOptions = (type: string) => {
|
||||
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={mode === "create" ? "创建模板" : "编辑模板"}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:产品质量分类" maxLength={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea
|
||||
placeholder="描述此模板的用途"
|
||||
rows={2}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: "100%" }} size="large">
|
||||
<Form.Item
|
||||
label="数据类型"
|
||||
name="dataType"
|
||||
rules={[{ required: true, message: "请选择数据类型" }]}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<Select placeholder="选择数据类型">
|
||||
<Option value="image">图像</Option>
|
||||
<Option value="text">文本</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="video">视频</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注类型"
|
||||
name="labelingType"
|
||||
rules={[{ required: true, message: "请选择标注类型" }]}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
<Select placeholder="选择标注类型">
|
||||
<Option value="classification">分类</Option>
|
||||
<Option value="object-detection">目标检测</Option>
|
||||
<Option value="segmentation">分割</Option>
|
||||
<Option value="ner">命名实体识别</Option>
|
||||
<Option value="multi-stage">多阶段</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="样式"
|
||||
name="style"
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="horizontal">水平</Option>
|
||||
<Option value="vertical">垂直</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="分类"
|
||||
name="category"
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="computer-vision">计算机视觉</Option>
|
||||
<Option value="nlp">自然语言处理</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="quality-control">质量控制</Option>
|
||||
<Option value="custom">自定义</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider>数据对象</Divider>
|
||||
|
||||
<Form.List name="objects">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card key={field.key} size="small" style={{ marginBottom: 8 }}>
|
||||
<Space align="start" style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="名称"
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="例如:image" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<TagSelector type="object" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="值"
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{ required: true, message: "必填" },
|
||||
{ pattern: /^\$/, message: "必须以 $ 开头" },
|
||||
]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="$image" />
|
||||
</Form.Item>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<MinusCircleOutlined
|
||||
style={{ marginTop: 30, color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加对象
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider>标签控件</Divider>
|
||||
|
||||
<Form.List name="labels">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={
|
||||
<Space>
|
||||
<span>控件 {fields.indexOf(field) + 1}</span>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const controlType = form.getFieldValue(["labels", field.name, "type"]);
|
||||
const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
|
||||
if (controlType || fromName) {
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
|
||||
({fromName || '未命名'} - {controlType || '未设置类型'})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<MinusCircleOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="来源名称"
|
||||
name={[field.name, "fromName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="此控件的唯一标识符"
|
||||
>
|
||||
<Input placeholder="例如:choice" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="标注目标对象"
|
||||
name={[field.name, "toName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="选择此控件将标注哪个数据对象"
|
||||
dependencies={['objects']}
|
||||
>
|
||||
<Select placeholder="选择数据对象">
|
||||
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
|
||||
<Option key={idx} value={obj?.name || ''}>
|
||||
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="控件类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<TagSelector type="control" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label=" "
|
||||
name={[field.name, "required"]}
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Checkbox>必填</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
const prevType = prevValues.labels?.[field.name]?.type;
|
||||
const currType = currentValues.labels?.[field.name]?.type;
|
||||
return prevType !== currType;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const controlType = getFieldValue(["labels", field.name, "type"]);
|
||||
const fieldName = controlType === "Choices" ? "options" : "labels";
|
||||
|
||||
if (needsOptions(controlType)) {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={controlType === "Choices" ? "选项" : "标签"}
|
||||
name={[field.name, fieldName]}
|
||||
rules={[{ required: true, message: "至少需要一个选项" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
open={false}
|
||||
placeholder={
|
||||
controlType === "Choices"
|
||||
? "输入选项内容,按回车添加。例如:是、否、不确定"
|
||||
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Row 3: 描述 */}
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="描述"
|
||||
name={[field.name, "description"]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="向标注人员显示的帮助信息"
|
||||
>
|
||||
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() =>
|
||||
add({
|
||||
fromName: "",
|
||||
toName: "",
|
||||
type: "Choices",
|
||||
required: false,
|
||||
})
|
||||
}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加标签控件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateForm;
|
||||
|
||||
@@ -1,311 +1,311 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
Card,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
deleteAnnotationTemplateByIdUsingDelete,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TemplateForm from "./TemplateForm.tsx";
|
||||
import TemplateDetail from "./TemplateDetail.tsx";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import {
|
||||
AnnotationTypeMap,
|
||||
ClassificationMap,
|
||||
DataTypeMap,
|
||||
TemplateTypeMap
|
||||
} from "@/pages/DataAnnotation/annotation.const.tsx";
|
||||
|
||||
const TemplateList: React.FC = () => {
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "category",
|
||||
label: "分类",
|
||||
options: [...Object.values(ClassificationMap)],
|
||||
},
|
||||
{
|
||||
key: "dataType",
|
||||
label: "数据类型",
|
||||
options: [...Object.values(DataTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "labelingType",
|
||||
label: "标注类型",
|
||||
options: [...Object.values(AnnotationTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "builtIn",
|
||||
label: "模板类型",
|
||||
options: [...Object.values(TemplateTypeMap)],
|
||||
},
|
||||
];
|
||||
|
||||
// Modals
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>();
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
setSelectedTemplate(undefined);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (template: AnnotationTemplate) => {
|
||||
setFormMode("edit");
|
||||
setSelectedTemplate(template);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (template: AnnotationTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
try {
|
||||
const response = await deleteAnnotationTemplateByIdUsingDelete(templateId);
|
||||
if (response.code === 200) {
|
||||
message.success("模板删除成功");
|
||||
fetchData();
|
||||
} else {
|
||||
message.error(response.message || "删除模板失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("删除模板失败");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setIsFormVisible(false);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
"computer-vision": "blue",
|
||||
"nlp": "green",
|
||||
"audio": "purple",
|
||||
"quality-control": "orange",
|
||||
"custom": "default",
|
||||
};
|
||||
return colors[category] || "default";
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AnnotationTemplate> = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
onFilter: (value, record) =>
|
||||
record.name.toLowerCase().includes(value.toString().toLowerCase()) ||
|
||||
(record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false),
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
render: (description: string) => (
|
||||
<Tooltip title={description}>
|
||||
<div
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.5em',
|
||||
maxHeight: '3em',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "数据类型",
|
||||
dataIndex: "dataType",
|
||||
key: "dataType",
|
||||
width: 120,
|
||||
render: (dataType: string) => (
|
||||
<Tag color="cyan">{dataType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标注类型",
|
||||
dataIndex: "labelingType",
|
||||
key: "labelingType",
|
||||
width: 150,
|
||||
render: (labelingType: string) => (
|
||||
<Tag color="geekblue">{labelingType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 150,
|
||||
render: (category: string) => (
|
||||
<Tag color={getCategoryColor(category)}>{category}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "builtIn",
|
||||
key: "builtIn",
|
||||
width: 100,
|
||||
render: (builtIn: boolean) => (
|
||||
<Tag color={builtIn ? "gold" : "default"}>
|
||||
{builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
dataIndex: "version",
|
||||
key: "version",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!record.builtIn && (
|
||||
<>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: Create button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
创建模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={{ x: 1400, y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<TemplateForm
|
||||
visible={isFormVisible}
|
||||
mode={formMode}
|
||||
template={selectedTemplate}
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={() => setIsFormVisible(false)}
|
||||
/>
|
||||
|
||||
<TemplateDetail
|
||||
visible={isDetailVisible}
|
||||
template={selectedTemplate}
|
||||
onClose={() => setIsDetailVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateList;
|
||||
export { TemplateList };
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
Card,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
deleteAnnotationTemplateByIdUsingDelete,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TemplateForm from "./TemplateForm.tsx";
|
||||
import TemplateDetail from "./TemplateDetail.tsx";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import {
|
||||
AnnotationTypeMap,
|
||||
ClassificationMap,
|
||||
DataTypeMap,
|
||||
TemplateTypeMap
|
||||
} from "@/pages/DataAnnotation/annotation.const.tsx";
|
||||
|
||||
const TemplateList: React.FC = () => {
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "category",
|
||||
label: "分类",
|
||||
options: [...Object.values(ClassificationMap)],
|
||||
},
|
||||
{
|
||||
key: "dataType",
|
||||
label: "数据类型",
|
||||
options: [...Object.values(DataTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "labelingType",
|
||||
label: "标注类型",
|
||||
options: [...Object.values(AnnotationTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "builtIn",
|
||||
label: "模板类型",
|
||||
options: [...Object.values(TemplateTypeMap)],
|
||||
},
|
||||
];
|
||||
|
||||
// Modals
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>();
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
setSelectedTemplate(undefined);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (template: AnnotationTemplate) => {
|
||||
setFormMode("edit");
|
||||
setSelectedTemplate(template);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (template: AnnotationTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
try {
|
||||
const response = await deleteAnnotationTemplateByIdUsingDelete(templateId);
|
||||
if (response.code === 200) {
|
||||
message.success("模板删除成功");
|
||||
fetchData();
|
||||
} else {
|
||||
message.error(response.message || "删除模板失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("删除模板失败");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setIsFormVisible(false);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
"computer-vision": "blue",
|
||||
"nlp": "green",
|
||||
"audio": "purple",
|
||||
"quality-control": "orange",
|
||||
"custom": "default",
|
||||
};
|
||||
return colors[category] || "default";
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AnnotationTemplate> = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
onFilter: (value, record) =>
|
||||
record.name.toLowerCase().includes(value.toString().toLowerCase()) ||
|
||||
(record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false),
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
render: (description: string) => (
|
||||
<Tooltip title={description}>
|
||||
<div
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.5em',
|
||||
maxHeight: '3em',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "数据类型",
|
||||
dataIndex: "dataType",
|
||||
key: "dataType",
|
||||
width: 120,
|
||||
render: (dataType: string) => (
|
||||
<Tag color="cyan">{dataType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标注类型",
|
||||
dataIndex: "labelingType",
|
||||
key: "labelingType",
|
||||
width: 150,
|
||||
render: (labelingType: string) => (
|
||||
<Tag color="geekblue">{labelingType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 150,
|
||||
render: (category: string) => (
|
||||
<Tag color={getCategoryColor(category)}>{category}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "builtIn",
|
||||
key: "builtIn",
|
||||
width: 100,
|
||||
render: (builtIn: boolean) => (
|
||||
<Tag color={builtIn ? "gold" : "default"}>
|
||||
{builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
dataIndex: "version",
|
||||
key: "version",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!record.builtIn && (
|
||||
<>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: Create button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
创建模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={{ x: 1400, y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<TemplateForm
|
||||
visible={isFormVisible}
|
||||
mode={formMode}
|
||||
template={selectedTemplate}
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={() => setIsFormVisible(false)}
|
||||
/>
|
||||
|
||||
<TemplateDetail
|
||||
visible={isDetailVisible}
|
||||
template={selectedTemplate}
|
||||
onClose={() => setIsDetailVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateList;
|
||||
export { TemplateList };
|
||||
|
||||
@@ -1,161 +1,161 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Row,
|
||||
Col,
|
||||
Drawer,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
CodeOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { TagBrowser } from "./components";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface VisualTemplateBuilderProps {
|
||||
onSave?: (templateCode: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual Template Builder
|
||||
* Provides a drag-and-drop interface for building Label Studio templates
|
||||
*/
|
||||
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<
|
||||
Array<{ name: string; category: "object" | "control" }>
|
||||
>([]);
|
||||
|
||||
const handleTagSelect = (tagName: string, category: "object" | "control") => {
|
||||
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
|
||||
setSelectedTags([...selectedTags, { name: tagName, category }]);
|
||||
setDrawerVisible(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Generate template XML from selectedTags
|
||||
message.success("模板保存成功");
|
||||
onSave?.("<View><!-- Generated template --></View>");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title="可视化模板构建器"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
浏览标签
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => setPreviewVisible(true)}
|
||||
>
|
||||
查看代码
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存模板
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "400px",
|
||||
border: "2px dashed #d9d9d9",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{selectedTags.length === 0 ? (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
点击"浏览标签"开始构建您的标注模板
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
添加标签
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size="large">
|
||||
{selectedTags.map((tag, index) => (
|
||||
<Card key={index} size="small">
|
||||
<div>
|
||||
{tag.category === "object" ? "对象" : "控件"}: {tag.name}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Drawer
|
||||
title="标签浏览器"
|
||||
placement="right"
|
||||
width={800}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
>
|
||||
<TagBrowser onTagSelect={handleTagSelect} />
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="模板代码预览"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={previewVisible}
|
||||
onClose={() => setPreviewVisible(false)}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{`<View>
|
||||
<!-- 根据选择的标签生成的模板代码 -->
|
||||
${selectedTags
|
||||
.map(
|
||||
(tag) =>
|
||||
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
|
||||
)
|
||||
.join("\n ")}
|
||||
</View>`}
|
||||
</code>
|
||||
</pre>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualTemplateBuilder;
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Row,
|
||||
Col,
|
||||
Drawer,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
CodeOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { TagBrowser } from "./components";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface VisualTemplateBuilderProps {
|
||||
onSave?: (templateCode: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual Template Builder
|
||||
* Provides a drag-and-drop interface for building Label Studio templates
|
||||
*/
|
||||
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<
|
||||
Array<{ name: string; category: "object" | "control" }>
|
||||
>([]);
|
||||
|
||||
const handleTagSelect = (tagName: string, category: "object" | "control") => {
|
||||
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
|
||||
setSelectedTags([...selectedTags, { name: tagName, category }]);
|
||||
setDrawerVisible(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Generate template XML from selectedTags
|
||||
message.success("模板保存成功");
|
||||
onSave?.("<View><!-- Generated template --></View>");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title="可视化模板构建器"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
浏览标签
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => setPreviewVisible(true)}
|
||||
>
|
||||
查看代码
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存模板
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "400px",
|
||||
border: "2px dashed #d9d9d9",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{selectedTags.length === 0 ? (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
点击"浏览标签"开始构建您的标注模板
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
添加标签
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size="large">
|
||||
{selectedTags.map((tag, index) => (
|
||||
<Card key={index} size="small">
|
||||
<div>
|
||||
{tag.category === "object" ? "对象" : "控件"}: {tag.name}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Drawer
|
||||
title="标签浏览器"
|
||||
placement="right"
|
||||
width={800}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
>
|
||||
<TagBrowser onTagSelect={handleTagSelect} />
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="模板代码预览"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={previewVisible}
|
||||
onClose={() => setPreviewVisible(false)}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{`<View>
|
||||
<!-- 根据选择的标签生成的模板代码 -->
|
||||
${selectedTags
|
||||
.map(
|
||||
(tag) =>
|
||||
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
|
||||
)
|
||||
.join("\n ")}
|
||||
</View>`}
|
||||
</code>
|
||||
</pre>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualTemplateBuilder;
|
||||
|
||||
@@ -1,260 +1,260 @@
|
||||
import React from "react";
|
||||
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ControlOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTagConfig } from "../../../../hooks/useTagConfig";
|
||||
import {
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
import type { TagOption } from "../../annotation.tagconfig";
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface TagBrowserProps {
|
||||
onTagSelect?: (tagName: string, category: "object" | "control") => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Browser Component
|
||||
* Displays all available Label Studio tags in a browsable interface
|
||||
*/
|
||||
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
|
||||
const { config, objectOptions, controlOptions, loading, error } =
|
||||
useTagConfig();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16 }}>加载标签配置...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<Empty
|
||||
description={
|
||||
<div>
|
||||
<div>{error}</div>
|
||||
<Text type="secondary">无法加载标签配置</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const renderObjectList = () => (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={objectOptions}
|
||||
renderItem={(item: TagOption) => {
|
||||
const objConfig = config?.objects[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "object")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>{getObjectDisplayName(item.value)}</Text>
|
||||
<Tag color="blue"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{objConfig && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: "#8c8c8c" }}>
|
||||
必需属性:{" "}
|
||||
{objConfig.required_attrs.join(", ") || "无"}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderControlsByGroup = () => {
|
||||
const groups = getControlGroups();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="classification"
|
||||
items={Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const groupControls = controlOptions.filter((opt: TagOption) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
|
||||
return {
|
||||
key: groupKey,
|
||||
label: groupConfig.label,
|
||||
children: (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={groupControls}
|
||||
locale={{ emptyText: "此分组暂无控件" }}
|
||||
renderItem={(item: TagOption) => {
|
||||
const ctrlConfig = config?.controls[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "control")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>
|
||||
{getControlDisplayName(item.value)}
|
||||
</Text>
|
||||
<Tag color="green"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{ctrlConfig && (
|
||||
<Space
|
||||
size={4}
|
||||
wrap
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{ctrlConfig.requires_children && (
|
||||
<Tag
|
||||
color="orange"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
需要 <{ctrlConfig.child_tag}>
|
||||
</Tag>
|
||||
)}
|
||||
{ctrlConfig.required_attrs.includes("toName") && (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
绑定对象
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Tabs
|
||||
defaultActiveKey="controls"
|
||||
items={[
|
||||
{
|
||||
key: "controls",
|
||||
label: (
|
||||
<span>
|
||||
<ControlOutlined />
|
||||
控件标签 ({controlOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderControlsByGroup(),
|
||||
},
|
||||
{
|
||||
key: "objects",
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined />
|
||||
数据对象 ({objectOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderObjectList(),
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
label: (
|
||||
<span>
|
||||
<InfoCircleOutlined />
|
||||
使用说明
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Title level={4}>Label Studio 标签配置说明</Title>
|
||||
<Paragraph>
|
||||
标注模板由两类标签组成:
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<Text strong>数据对象标签</Text>:定义要标注的数据类型(如图像、文本、音频等)
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>控件标签</Text>:定义标注工具和交互方式(如矩形框、分类选项、文本输入等)
|
||||
</li>
|
||||
</ul>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
基本结构
|
||||
</Title>
|
||||
<Paragraph>
|
||||
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
|
||||
{`<View>
|
||||
<!-- 数据对象 -->
|
||||
<Image name="image" value="$image" />
|
||||
|
||||
<!-- 控件 -->
|
||||
<RectangleLabels name="label" toName="image">
|
||||
<Label value="人物" />
|
||||
<Label value="车辆" />
|
||||
</RectangleLabels>
|
||||
</View>`}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
属性说明
|
||||
</Title>
|
||||
<ul>
|
||||
<li>
|
||||
<Text code>name</Text>:控件的唯一标识符
|
||||
</li>
|
||||
<li>
|
||||
<Text code>toName</Text>:指向要标注的数据对象的 name
|
||||
</li>
|
||||
<li>
|
||||
<Text code>value</Text>:数据源字段,以 $ 开头(如 $image, $text)
|
||||
</li>
|
||||
<li>
|
||||
<Text code>required</Text>:是否必填(可选)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagBrowser;
|
||||
import React from "react";
|
||||
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ControlOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTagConfig } from "../../../../hooks/useTagConfig";
|
||||
import {
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
import type { TagOption } from "../../annotation.tagconfig";
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface TagBrowserProps {
|
||||
onTagSelect?: (tagName: string, category: "object" | "control") => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Browser Component
|
||||
* Displays all available Label Studio tags in a browsable interface
|
||||
*/
|
||||
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
|
||||
const { config, objectOptions, controlOptions, loading, error } =
|
||||
useTagConfig();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16 }}>加载标签配置...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<Empty
|
||||
description={
|
||||
<div>
|
||||
<div>{error}</div>
|
||||
<Text type="secondary">无法加载标签配置</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const renderObjectList = () => (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={objectOptions}
|
||||
renderItem={(item: TagOption) => {
|
||||
const objConfig = config?.objects[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "object")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>{getObjectDisplayName(item.value)}</Text>
|
||||
<Tag color="blue"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{objConfig && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: "#8c8c8c" }}>
|
||||
必需属性:{" "}
|
||||
{objConfig.required_attrs.join(", ") || "无"}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderControlsByGroup = () => {
|
||||
const groups = getControlGroups();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="classification"
|
||||
items={Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const groupControls = controlOptions.filter((opt: TagOption) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
|
||||
return {
|
||||
key: groupKey,
|
||||
label: groupConfig.label,
|
||||
children: (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={groupControls}
|
||||
locale={{ emptyText: "此分组暂无控件" }}
|
||||
renderItem={(item: TagOption) => {
|
||||
const ctrlConfig = config?.controls[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "control")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>
|
||||
{getControlDisplayName(item.value)}
|
||||
</Text>
|
||||
<Tag color="green"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{ctrlConfig && (
|
||||
<Space
|
||||
size={4}
|
||||
wrap
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{ctrlConfig.requires_children && (
|
||||
<Tag
|
||||
color="orange"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
需要 <{ctrlConfig.child_tag}>
|
||||
</Tag>
|
||||
)}
|
||||
{ctrlConfig.required_attrs.includes("toName") && (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
绑定对象
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Tabs
|
||||
defaultActiveKey="controls"
|
||||
items={[
|
||||
{
|
||||
key: "controls",
|
||||
label: (
|
||||
<span>
|
||||
<ControlOutlined />
|
||||
控件标签 ({controlOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderControlsByGroup(),
|
||||
},
|
||||
{
|
||||
key: "objects",
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined />
|
||||
数据对象 ({objectOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderObjectList(),
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
label: (
|
||||
<span>
|
||||
<InfoCircleOutlined />
|
||||
使用说明
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Title level={4}>Label Studio 标签配置说明</Title>
|
||||
<Paragraph>
|
||||
标注模板由两类标签组成:
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<Text strong>数据对象标签</Text>:定义要标注的数据类型(如图像、文本、音频等)
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>控件标签</Text>:定义标注工具和交互方式(如矩形框、分类选项、文本输入等)
|
||||
</li>
|
||||
</ul>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
基本结构
|
||||
</Title>
|
||||
<Paragraph>
|
||||
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
|
||||
{`<View>
|
||||
<!-- 数据对象 -->
|
||||
<Image name="image" value="$image" />
|
||||
|
||||
<!-- 控件 -->
|
||||
<RectangleLabels name="label" toName="image">
|
||||
<Label value="人物" />
|
||||
<Label value="车辆" />
|
||||
</RectangleLabels>
|
||||
</View>`}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
属性说明
|
||||
</Title>
|
||||
<ul>
|
||||
<li>
|
||||
<Text code>name</Text>:控件的唯一标识符
|
||||
</li>
|
||||
<li>
|
||||
<Text code>toName</Text>:指向要标注的数据对象的 name
|
||||
</li>
|
||||
<li>
|
||||
<Text code>value</Text>:数据源字段,以 $ 开头(如 $image, $text)
|
||||
</li>
|
||||
<li>
|
||||
<Text code>required</Text>:是否必填(可选)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagBrowser;
|
||||
|
||||
@@ -1,301 +1,301 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getTagConfigUsingGet } from "../../annotation.api";
|
||||
import type {
|
||||
LabelStudioTagConfig,
|
||||
TagOption,
|
||||
} from "../../annotation.tagconfig";
|
||||
import {
|
||||
parseTagConfig,
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
|
||||
const { Option, OptGroup } = Select;
|
||||
|
||||
interface TagSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
type: "object" | "control";
|
||||
placeholder?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Selector Component
|
||||
* Dynamically fetches and displays available Label Studio tags from backend config
|
||||
*/
|
||||
const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
placeholder,
|
||||
style,
|
||||
disabled,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagConfig();
|
||||
}, []);
|
||||
|
||||
const fetchTagConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const config: LabelStudioTagConfig = response.data;
|
||||
const { objectOptions, controlOptions } = parseTagConfig(config);
|
||||
|
||||
if (type === "object") {
|
||||
setTagOptions(objectOptions);
|
||||
} else {
|
||||
setTagOptions(controlOptions);
|
||||
}
|
||||
} else {
|
||||
setError(response.message || "获取标签配置失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
setError("加载标签配置时出错");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Select
|
||||
placeholder="加载中..."
|
||||
style={style}
|
||||
disabled
|
||||
suffixIcon={<Spin size="small" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Tooltip title={error}>
|
||||
<Select
|
||||
placeholder="加载失败,点击重试"
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
status="error"
|
||||
onClick={() => fetchTagConfig()}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Group controls by usage pattern
|
||||
if (type === "control") {
|
||||
const groups = getControlGroups();
|
||||
const groupedOptions: Record<string, TagOption[]> = {};
|
||||
const ungroupedOptions: TagOption[] = [];
|
||||
|
||||
// Group the controls
|
||||
Object.entries(groups).forEach(([groupKey, groupConfig]) => {
|
||||
groupedOptions[groupKey] = tagOptions.filter((opt) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
});
|
||||
|
||||
// Find ungrouped controls
|
||||
const allGroupedControls = new Set(
|
||||
Object.values(groups).flatMap((g) => g.controls)
|
||||
);
|
||||
tagOptions.forEach((opt) => {
|
||||
if (!allGroupedControls.has(opt.value)) {
|
||||
ungroupedOptions.push(opt);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择控件类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const options = groupedOptions[groupKey];
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<OptGroup key={groupKey} label={groupConfig.label}>
|
||||
{options.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
);
|
||||
})}
|
||||
{ungroupedOptions.length > 0 && (
|
||||
<OptGroup label="其他">
|
||||
{ungroupedOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// Objects selector (no grouping)
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择数据对象类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{tagOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getObjectDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelector;
|
||||
|
||||
/**
|
||||
* Tag Info Panel Component
|
||||
* Displays detailed information about a selected tag
|
||||
*/
|
||||
interface TagInfoPanelProps {
|
||||
tagConfig: LabelStudioTagConfig | null;
|
||||
tagType: string;
|
||||
category: "object" | "control";
|
||||
}
|
||||
|
||||
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
|
||||
tagConfig,
|
||||
tagType,
|
||||
category,
|
||||
}) => {
|
||||
if (!tagConfig || !tagType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config =
|
||||
category === "object"
|
||||
? tagConfig.objects[tagType]
|
||||
: tagConfig.controls[tagType];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "标签配置详情",
|
||||
children: (
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<strong>描述:</strong>
|
||||
{config.description}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>必需属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{config.required_attrs.map((attr: string) => (
|
||||
<Tag key={attr} color="red">
|
||||
{attr}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.optional_attrs &&
|
||||
Object.keys(config.optional_attrs).length > 0 && (
|
||||
<div>
|
||||
<strong>可选属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{Object.entries(config.optional_attrs).map(
|
||||
([attrName, attrConfig]: [string, any]) => (
|
||||
<Tooltip
|
||||
key={attrName}
|
||||
title={
|
||||
<div>
|
||||
{attrConfig.description && (
|
||||
<div>{attrConfig.description}</div>
|
||||
)}
|
||||
{attrConfig.type && (
|
||||
<div>类型: {attrConfig.type}</div>
|
||||
)}
|
||||
{attrConfig.default !== undefined && (
|
||||
<div>默认值: {String(attrConfig.default)}</div>
|
||||
)}
|
||||
{attrConfig.values && (
|
||||
<div>
|
||||
可选值: {attrConfig.values.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag color="blue" style={{ cursor: "help" }}>
|
||||
{attrName}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.requires_children && (
|
||||
<div>
|
||||
<strong>子元素:</strong>
|
||||
<Tag color="green">需要 <{config.child_tag}></Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getTagConfigUsingGet } from "../../annotation.api";
|
||||
import type {
|
||||
LabelStudioTagConfig,
|
||||
TagOption,
|
||||
} from "../../annotation.tagconfig";
|
||||
import {
|
||||
parseTagConfig,
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
|
||||
const { Option, OptGroup } = Select;
|
||||
|
||||
interface TagSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
type: "object" | "control";
|
||||
placeholder?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Selector Component
|
||||
* Dynamically fetches and displays available Label Studio tags from backend config
|
||||
*/
|
||||
const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
placeholder,
|
||||
style,
|
||||
disabled,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagConfig();
|
||||
}, []);
|
||||
|
||||
const fetchTagConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const config: LabelStudioTagConfig = response.data;
|
||||
const { objectOptions, controlOptions } = parseTagConfig(config);
|
||||
|
||||
if (type === "object") {
|
||||
setTagOptions(objectOptions);
|
||||
} else {
|
||||
setTagOptions(controlOptions);
|
||||
}
|
||||
} else {
|
||||
setError(response.message || "获取标签配置失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
setError("加载标签配置时出错");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Select
|
||||
placeholder="加载中..."
|
||||
style={style}
|
||||
disabled
|
||||
suffixIcon={<Spin size="small" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Tooltip title={error}>
|
||||
<Select
|
||||
placeholder="加载失败,点击重试"
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
status="error"
|
||||
onClick={() => fetchTagConfig()}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Group controls by usage pattern
|
||||
if (type === "control") {
|
||||
const groups = getControlGroups();
|
||||
const groupedOptions: Record<string, TagOption[]> = {};
|
||||
const ungroupedOptions: TagOption[] = [];
|
||||
|
||||
// Group the controls
|
||||
Object.entries(groups).forEach(([groupKey, groupConfig]) => {
|
||||
groupedOptions[groupKey] = tagOptions.filter((opt) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
});
|
||||
|
||||
// Find ungrouped controls
|
||||
const allGroupedControls = new Set(
|
||||
Object.values(groups).flatMap((g) => g.controls)
|
||||
);
|
||||
tagOptions.forEach((opt) => {
|
||||
if (!allGroupedControls.has(opt.value)) {
|
||||
ungroupedOptions.push(opt);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择控件类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const options = groupedOptions[groupKey];
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<OptGroup key={groupKey} label={groupConfig.label}>
|
||||
{options.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
);
|
||||
})}
|
||||
{ungroupedOptions.length > 0 && (
|
||||
<OptGroup label="其他">
|
||||
{ungroupedOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// Objects selector (no grouping)
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择数据对象类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{tagOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getObjectDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelector;
|
||||
|
||||
/**
|
||||
* Tag Info Panel Component
|
||||
* Displays detailed information about a selected tag
|
||||
*/
|
||||
interface TagInfoPanelProps {
|
||||
tagConfig: LabelStudioTagConfig | null;
|
||||
tagType: string;
|
||||
category: "object" | "control";
|
||||
}
|
||||
|
||||
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
|
||||
tagConfig,
|
||||
tagType,
|
||||
category,
|
||||
}) => {
|
||||
if (!tagConfig || !tagType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config =
|
||||
category === "object"
|
||||
? tagConfig.objects[tagType]
|
||||
: tagConfig.controls[tagType];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "标签配置详情",
|
||||
children: (
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<strong>描述:</strong>
|
||||
{config.description}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>必需属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{config.required_attrs.map((attr: string) => (
|
||||
<Tag key={attr} color="red">
|
||||
{attr}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.optional_attrs &&
|
||||
Object.keys(config.optional_attrs).length > 0 && (
|
||||
<div>
|
||||
<strong>可选属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{Object.entries(config.optional_attrs).map(
|
||||
([attrName, attrConfig]: [string, any]) => (
|
||||
<Tooltip
|
||||
key={attrName}
|
||||
title={
|
||||
<div>
|
||||
{attrConfig.description && (
|
||||
<div>{attrConfig.description}</div>
|
||||
)}
|
||||
{attrConfig.type && (
|
||||
<div>类型: {attrConfig.type}</div>
|
||||
)}
|
||||
{attrConfig.default !== undefined && (
|
||||
<div>默认值: {String(attrConfig.default)}</div>
|
||||
)}
|
||||
{attrConfig.values && (
|
||||
<div>
|
||||
可选值: {attrConfig.values.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag color="blue" style={{ cursor: "help" }}>
|
||||
{attrName}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.requires_children && (
|
||||
<div>
|
||||
<strong>子元素:</strong>
|
||||
<Tag color="green">需要 <{config.child_tag}></Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as TagSelector } from "./TagSelector";
|
||||
export { default as TagBrowser } from "./TagBrowser";
|
||||
export { TagInfoPanel } from "./TagSelector";
|
||||
export { default as TagSelector } from "./TagSelector";
|
||||
export { default as TagBrowser } from "./TagBrowser";
|
||||
export { TagInfoPanel } from "./TagSelector";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as TemplateList } from "./TemplateList";
|
||||
export { default as TemplateForm } from "./TemplateForm";
|
||||
export { default as TemplateDetail } from "./TemplateDetail";
|
||||
export { TagBrowser, TagSelector, TagInfoPanel } from "./components";
|
||||
export { default as TemplateList } from "./TemplateList";
|
||||
export { default as TemplateForm } from "./TemplateForm";
|
||||
export { default as TemplateDetail } from "./TemplateDetail";
|
||||
export { TagBrowser, TagSelector, TagInfoPanel } from "./components";
|
||||
|
||||
Reference in New Issue
Block a user