You've already forked DataMate
feat(annotation): 添加模板示例数据配置功能
- 在模板配置表单中新增示例数据输入区域 - 实现不同数据类型的示例输入框(文本、图片、音频、视频等) - 添加图片类型示例的实时预览功能 - 在模板详情页增加示例数据预览卡片 - 支持多种媒体类型的示例展示(图片、音频、视频、文本) - 更新前后端数据模型以支持exampleData字段 - 添加示例数据的placeholder提示文案
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
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";
|
import type { AnnotationTemplate } from "../annotation.model";
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
@@ -81,6 +81,69 @@ const TemplateDetail: React.FC<TemplateDetailProps> = ({
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</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 }}>
|
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
|
||||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||||
{template.configuration.labels.map((label, index) => (
|
{template.configuration.labels.map((label, index) => (
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ export interface TemplateConfiguration {
|
|||||||
labels: LabelDefinition[];
|
labels: LabelDefinition[];
|
||||||
objects: ObjectDefinition[];
|
objects: ObjectDefinition[];
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* 示例数据,用于模板预览
|
||||||
|
* key: 变量名(不带$前缀,如 "image"、"text")
|
||||||
|
* value: 示例内容(URL 或文本)
|
||||||
|
*/
|
||||||
|
exampleData?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationTemplate {
|
export interface AnnotationTemplate {
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
Image,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||||
import TagSelector from "../Template/components/TagSelector";
|
import TagSelector from "../Template/components/TagSelector";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface TemplateConfigurationFormProps {
|
interface TemplateConfigurationFormProps {
|
||||||
form: any;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider orientation="left">数据对象</Divider>
|
<Divider orientation="left">数据对象</Divider>
|
||||||
@@ -287,6 +316,78 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ class TemplateConfiguration(BaseModel):
|
|||||||
labels: List[LabelDefinition] = Field(description="标签定义列表")
|
labels: List[LabelDefinition] = Field(description="标签定义列表")
|
||||||
objects: List[ObjectDefinition] = Field(description="对象定义列表")
|
objects: List[ObjectDefinition] = Field(description="对象定义列表")
|
||||||
metadata: Optional[Dict[str, Any]] = Field(None, description="额外元数据")
|
metadata: Optional[Dict[str, Any]] = Field(None, description="额外元数据")
|
||||||
|
example_data: Optional[Dict[str, str]] = Field(
|
||||||
|
None,
|
||||||
|
alias="exampleData",
|
||||||
|
description="示例数据,key 为变量名(不带$),value 为示例内容"
|
||||||
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user