feat(annotation): 添加模板示例数据配置功能

- 在模板配置表单中新增示例数据输入区域
- 实现不同数据类型的示例输入框(文本、图片、音频、视频等)
- 添加图片类型示例的实时预览功能
- 在模板详情页增加示例数据预览卡片
- 支持多种媒体类型的示例展示(图片、音频、视频、文本)
- 更新前后端数据模型以支持exampleData字段
- 添加示例数据的placeholder提示文案
This commit is contained in:
2026-01-18 21:59:41 +08:00
parent 5057457329
commit a2b0fc3674
4 changed files with 177 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography, Image, Empty } from "antd";
import type { AnnotationTemplate } from "../annotation.model";
const { Text, Paragraph } = Typography;
@@ -81,6 +81,69 @@ const TemplateDetail: React.FC<TemplateDetailProps> = ({
</Space>
</Card>
{/* 示例数据预览 */}
<Card title="示例数据预览" size="small" style={{ marginBottom: 16 }}>
{template.configuration.exampleData &&
Object.keys(template.configuration.exampleData).length > 0 ? (
<Space direction="vertical" style={{ width: "100%" }} size="middle">
{template.configuration.objects.map((obj, index) => {
const varName = obj.value?.replace(/^\$/, "") || obj.name;
const exampleValue = template.configuration.exampleData?.[varName];
if (!exampleValue) return null;
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 }}>
({obj.type})
</Text>
</div>
{/* 根据类型渲染不同的预览 */}
{obj.type === "Image" ? (
<Image
src={exampleValue}
alt={`示例: ${obj.name}`}
style={{ maxHeight: 200, maxWidth: "100%" }}
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgesAAN4TeleOJA8AAAA="
/>
) : obj.type === "Audio" ? (
<audio
src={exampleValue}
controls
style={{ width: "100%" }}
/>
) : obj.type === "Video" ? (
<video
src={exampleValue}
controls
style={{ maxHeight: 200, maxWidth: "100%" }}
/>
) : (
<Paragraph
style={{
background: "#f5f5f5",
padding: 12,
borderRadius: 4,
margin: 0,
whiteSpace: "pre-wrap",
}}
>
{exampleValue}
</Paragraph>
)}
</Space>
</Card>
);
})}
</Space>
) : (
<Empty description="暂无示例数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Card>
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: "100%" }} size="middle">
{template.configuration.labels.map((label, index) => (

View File

@@ -53,6 +53,12 @@ export interface TemplateConfiguration {
labels: LabelDefinition[];
objects: ObjectDefinition[];
metadata?: Record<string, any>;
/**
* 示例数据,用于模板预览
* key: 变量名(不带$前缀,如 "image"、"text")
* value: 示例内容(URL 或文本)
*/
exampleData?: Record<string, string>;
}
export interface AnnotationTemplate {

View File

@@ -8,11 +8,15 @@ import {
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;
@@ -27,6 +31,31 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
);
};
/** 判断对象类型是否为文本类 */
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>
@@ -287,6 +316,78 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
</>
)}
</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>
</>
);
};