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

View File

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