You've already forked DataMate
- 在模板配置表单中新增示例数据输入区域 - 实现不同数据类型的示例输入框(文本、图片、音频、视频等) - 添加图片类型示例的实时预览功能 - 在模板详情页增加示例数据预览卡片 - 支持多种媒体类型的示例展示(图片、音频、视频、文本) - 更新前后端数据模型以支持exampleData字段 - 添加示例数据的placeholder提示文案
396 lines
15 KiB
TypeScript
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;
|