Files
DataMate/frontend/src/pages/DataAnnotation/components/TemplateConfigurationForm.tsx
Jerry Yan a2b0fc3674 feat(annotation): 添加模板示例数据配置功能
- 在模板配置表单中新增示例数据输入区域
- 实现不同数据类型的示例输入框(文本、图片、音频、视频等)
- 添加图片类型示例的实时预览功能
- 在模板详情页增加示例数据预览卡片
- 支持多种媒体类型的示例展示(图片、音频、视频、文本)
- 更新前后端数据模型以支持exampleData字段
- 添加示例数据的placeholder提示文案
2026-01-18 21:59:41 +08:00

396 lines
15 KiB
TypeScript

import React from "react";
import {
Form,
Input,
Select,
Button,
Space,
Divider,
Card,
Checkbox,
Typography,
Image,
} from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import TagSelector from "../Template/components/TagSelector";
const { Option } = Select;
const { TextArea } = Input;
const { Text } = Typography;
interface TemplateConfigurationFormProps {
form: any;
}
const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
form,
}) => {
const needsOptions = (type: string) => {
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(
type
);
};
/** 判断对象类型是否为文本类 */
const isTextType = (type: string) => {
return ["Text", "HyperText", "Table", "List"].includes(type);
};
/** 判断对象类型是否为图片类 */
const isImageType = (type: string) => {
return ["Image"].includes(type);
};
/** 获取示例数据输入提示 */
const getExamplePlaceholder = (type: string) => {
const map: Record<string, string> = {
Text: "输入示例文本内容...",
HyperText: "输入示例 HTML 内容...",
Image: "输入图片 URL,如 https://example.com/image.jpg",
Audio: "输入音频 URL,如 https://example.com/audio.mp3",
Video: "输入视频 URL,如 https://example.com/video.mp4",
Table: "输入示例表格数据(JSON 格式)...",
List: "输入示例列表数据(JSON 格式)...",
TimeSeries: "输入时间序列数据 URL...",
};
return map[type] || "输入示例数据...";
};
return (
<>
<Divider orientation="left"></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 orientation="left"></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>
<Divider orientation="left"></Divider>
<Card size="small" style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ display: "block", marginBottom: 16 }}>
</Text>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.objects !== curr.objects}>
{({ getFieldValue }) => {
const objects = getFieldValue("objects") || [];
if (objects.length === 0) {
return (
<Text type="secondary"></Text>
);
}
return (
<Space direction="vertical" style={{ width: "100%" }} size="middle">
{objects.map((obj: any, index: number) => {
if (!obj?.name) return null;
const varName = obj.value?.replace(/^\$/, "") || obj.name;
const objType = obj.type || "Text";
return (
<Card key={index} size="small" type="inner">
<Space direction="vertical" style={{ width: "100%" }}>
<div>
<Text strong>{obj.name}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
({objType})
</Text>
</div>
<Form.Item
name={["exampleData", varName]}
style={{ marginBottom: 0 }}
>
{isTextType(objType) ? (
<TextArea
rows={3}
placeholder={getExamplePlaceholder(objType)}
/>
) : (
<Input placeholder={getExamplePlaceholder(objType)} />
)}
</Form.Item>
{/* 图片类型显示预览 */}
<Form.Item noStyle shouldUpdate>
{({ getFieldValue: getVal }) => {
const url = getVal(["exampleData", varName]);
if (isImageType(objType) && url) {
return (
<div style={{ marginTop: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<Image
src={url}
alt="示例图片预览"
style={{ maxHeight: 150, maxWidth: "100%", marginTop: 4 }}
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgesAAN4TeleOJA8AAAA="
/>
</div>
);
}
return null;
}}
</Form.Item>
</Space>
</Card>
);
})}
</Space>
);
}}
</Form.Item>
</Card>
</>
);
};
export default TemplateConfigurationForm;