feat(annotation): 添加模板受限编辑模式

- 引入 restrictedMode 属性控制表单编辑权限
- 在数据对象区域显示锁定状态提示
- 禁用受限制字段的输入功能
- 隐藏受限制时的删除和添加按钮
- 在标签控件区域显示可编辑状态提示
- 更新XML编辑器为只读模式并显示相应提示
- 添加模板选择状态跟踪功能
This commit is contained in:
2026-01-19 15:55:23 +08:00
parent e192c826eb
commit a778ac23b5
2 changed files with 106 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { mapDataset } from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select, message, Tabs, Radio, Typography } from "antd";
import { Button, Form, Input, Modal, Select, message, Tabs, Radio } from "antd";
import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react";
import {
@@ -33,6 +33,8 @@ export default function CreateAnnotationTask({
const [previewTaskData, setPreviewTaskData] = useState<Record<string, any>>({});
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
// 是否已选择模板(用于启用受限编辑模式)
const [hasSelectedTemplate, setHasSelectedTemplate] = useState(false);
useEffect(() => {
if (!open) return;
@@ -76,6 +78,7 @@ export default function CreateAnnotationTask({
setPreviewTaskData({});
setConfigMode("template");
setTemplateEditTab("visual");
setHasSelectedTemplate(false);
}
}, [open, manualForm]);
@@ -179,6 +182,15 @@ export default function CreateAnnotationTask({
// 当选择模板时,加载模板配置到表单
const handleTemplateSelect = (value: string, option: any) => {
// 处理清除选择的情况
if (!value) {
setHasSelectedTemplate(false);
setCustomXml("");
return;
}
setHasSelectedTemplate(true);
if (option && option.config) {
setCustomXml(option.config);
}
@@ -297,7 +309,7 @@ export default function CreateAnnotationTask({
<Form.Item
label="数据集"
name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]}
rules={[{ required: true, message: "请选择数据集" }]}
>
<Select
placeholder="请选择数据集"
@@ -440,23 +452,29 @@ export default function CreateAnnotationTask({
setTemplateEditTab(key as "visual" | "xml");
}}
size="small"
items={[
items={[
{
key: "visual",
label: "可视化配置",
children: (
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
<TemplateConfigurationForm form={manualForm} />
<TemplateConfigurationForm
form={manualForm}
restrictedMode={hasSelectedTemplate}
/>
</div>
),
},
{
key: "xml",
label: "XML编辑器(高级)",
label: hasSelectedTemplate ? "XML配置(只读)" : "XML编辑器(高级)",
children: (
<div>
<div className="mb-2 text-xs text-gray-500">
Label Studio XML
{hasSelectedTemplate
? "基于模板创建时,XML 配置为只读。如需完全自定义,请切换'自定义配置'模式。"
: "直接编辑 Label Studio XML 配置。注意:在此修改后切换回可视化配置可能会丢失部分高级设置。"
}
</div>
<TextArea
rows={10}
@@ -464,6 +482,7 @@ export default function CreateAnnotationTask({
onChange={(e) => setCustomXml(e.target.value)}
placeholder="<View>...</View>"
style={{ fontFamily: 'monospace', fontSize: 12 }}
disabled={hasSelectedTemplate}
/>
</div>
),
@@ -486,7 +505,7 @@ export default function CreateAnnotationTask({
onCancel={() => setShowPreview(false)}
title="标注界面预览"
width={1000}
footer={[
footer={[
<Button key="close" onClick={() => setShowPreview(false)}>
</Button>
@@ -506,4 +525,4 @@ export default function CreateAnnotationTask({
</Modal>
</>
);
}
}

View File

@@ -8,18 +8,28 @@ import {
Divider,
Card,
Checkbox,
Typography,
} from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import { PlusOutlined, MinusCircleOutlined, LockOutlined } from "@ant-design/icons";
import TagSelector from "../Template/components/TagSelector";
const { Option } = Select;
const { Text } = Typography;
interface TemplateConfigurationFormProps {
form: any;
/**
* 受限编辑模式:选择现有模板时启用
* - 数据对象部分完全只读
* - 标签控件的结构(类型、来源名称、目标对象等)只读
* - 只允许编辑标签/选项的值
*/
restrictedMode?: boolean;
}
const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
form,
restrictedMode = false,
}) => {
const needsOptions = (type: string) => {
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(
@@ -29,7 +39,14 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
return (
<>
<Divider orientation="left"></Divider>
<Divider orientation="left">
{restrictedMode && (
<Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
<LockOutlined />
</Text>
)}
</Divider>
<Form.List name="objects">
{(fields, { add, remove }) => (
<>
@@ -43,7 +60,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0, width: 150 }}
>
<Input placeholder="例如:image" />
<Input placeholder="例如:image" disabled={restrictedMode} />
</Form.Item>
<Form.Item
@@ -53,7 +70,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0, width: 150 }}
>
<TagSelector type="object" />
<TagSelector type="object" disabled={restrictedMode} />
</Form.Item>
<Form.Item
@@ -66,10 +83,10 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
]}
style={{ marginBottom: 0, width: 150 }}
>
<Input placeholder="$image" />
<Input placeholder="$image" disabled={restrictedMode} />
</Form.Item>
{fields.length > 1 && (
{!restrictedMode && fields.length > 1 && (
<MinusCircleOutlined
style={{ marginTop: 30, color: "red" }}
onClick={() => remove(field.name)}
@@ -78,19 +95,28 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
</Space>
</Card>
))}
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
</Button>
{!restrictedMode && (
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
</Button>
)}
</>
)}
</Form.List>
<Divider orientation="left"></Divider>
<Divider orientation="left">
{restrictedMode && (
<Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
/
</Text>
)}
</Divider>
<Form.List name="labels">
{(fields, { add, remove }) => (
<>
@@ -134,10 +160,12 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
</Space>
}
extra={
<MinusCircleOutlined
style={{ color: "red" }}
onClick={() => remove(field.name)}
/>
!restrictedMode && (
<MinusCircleOutlined
style={{ color: "red" }}
onClick={() => remove(field.name)}
/>
)
}
>
<Space
@@ -162,7 +190,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
style={{ marginBottom: 0 }}
tooltip="此控件的唯一标识符"
>
<Input placeholder="例如:choice" />
<Input placeholder="例如:choice" disabled={restrictedMode} />
</Form.Item>
<Form.Item
@@ -174,7 +202,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
tooltip="选择此控件将标注哪个数据对象"
dependencies={["objects"]}
>
<Select placeholder="选择数据对象">
<Select placeholder="选择数据对象" disabled={restrictedMode}>
{(form.getFieldValue("objects") || []).map(
(obj: any, idx: number) => (
<Option key={idx} value={obj?.name || ""}>
@@ -193,7 +221,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }}
>
<TagSelector type="control" />
<TagSelector type="control" disabled={restrictedMode} />
</Form.Item>
<Form.Item
@@ -203,7 +231,7 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
valuePropName="checked"
style={{ marginBottom: 0 }}
>
<Checkbox></Checkbox>
<Checkbox disabled={restrictedMode}></Checkbox>
</Form.Item>
</div>
@@ -229,7 +257,16 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
return (
<Form.Item
{...field}
label={controlType === "Choices" ? "选项" : "标签"}
label={
<>
{controlType === "Choices" ? "选项" : "标签"}
{restrictedMode && (
<Text type="success" style={{ fontSize: 11, marginLeft: 6 }}>
</Text>
)}
</>
}
name={[field.name, fieldName]}
rules={[
{ required: true, message: "至少需要一个选项" },
@@ -264,26 +301,29 @@ const TemplateConfigurationForm: React.FC<TemplateConfigurationFormProps> = ({
<Input
placeholder="为标注人员提供此控件的使用说明"
maxLength={200}
disabled={restrictedMode}
/>
</Form.Item>
</Space>
</Card>
))}
<Button
type="dashed"
onClick={() =>
add({
fromName: "",
toName: "",
type: "Choices",
required: false,
})
}
block
icon={<PlusOutlined />}
>
</Button>
{!restrictedMode && (
<Button
type="dashed"
onClick={() =>
add({
fromName: "",
toName: "",
type: "Choices",
required: false,
})
}
block
icon={<PlusOutlined />}
>
</Button>
)}
</>
)}
</Form.List>