本设计文档描述了标注平台角色权限菜单优化和项目配置流程改进的技术实现方案。主要包括:
graph TB
subgraph "Frontend"
subgraph "Components"
SM[Sidebar Menu]
TC[Task Type Cards]
LE[Label Editor]
XP[XML Preview]
CW[Config Wizard]
end
subgraph "Pages"
PL[Project List Page]
EP[External Projects Page]
PC[Project Config Page]
CP[Create Project Page]
end
subgraph "State"
UA[User Auth Atoms]
PA[Project Atoms]
end
end
subgraph "Services"
XG[XML Generator]
RF[Role Filter]
end
SM --> RF
RF --> UA
TC --> XG
LE --> XG
XG --> XP
CW --> TC
CW --> LE
CW --> XP
PL --> PA
EP --> PA
PC --> PA
/projects # 项目管理(仅显示进行中和已完成)
/projects/:id/config # 项目配置页面
/projects/create # 创建项目向导
/external-projects # 管理外部来源数据(管理员专属)
/my-tasks # 我的任务
/annotations # 我的标注
/tasks # 任务管理(管理员专属)
/users # 用户管理(管理员专属)
// 菜单项配置
const menuItems = [
{ id: 'projects', label: '项目管理', path: '/projects', adminOnly: false },
{ id: 'external-projects', label: '管理外部来源数据', path: '/external-projects', adminOnly: true },
{ id: 'tasks', label: '任务管理', path: '/tasks', adminOnly: true },
{ id: 'my-tasks', label: '我的任务', path: '/my-tasks', adminOnly: false },
{ id: 'annotations', label: '我的标注', path: '/annotations', adminOnly: false },
{ id: 'users', label: '用户管理', path: '/users', adminOnly: true },
];
// 角色过滤逻辑
const visibleMenuItems = menuItems.filter(item => {
if (item.adminOnly && currentUser?.role !== 'admin') {
return false;
}
return true;
});
选卡式标注类型选择组件。
interface TaskTypeOption {
id: TaskType;
name: string;
description: string;
icon: React.ReactNode;
dataFormat: 'text' | 'image';
}
interface TaskTypeSelectorProps {
selectedType: TaskType | null;
onSelect: (type: TaskType) => void;
}
const TASK_TYPE_OPTIONS: TaskTypeOption[] = [
{
id: 'text_classification',
name: '文本分类',
description: '对文本内容进行分类标注',
icon: <FileText />,
dataFormat: 'text',
},
{
id: 'image_classification',
name: '图像分类',
description: '对图像进行分类标注',
icon: <Image />,
dataFormat: 'image',
},
{
id: 'object_detection',
name: '目标检测',
description: '在图像中标注目标边界框',
icon: <Square />,
dataFormat: 'image',
},
{
id: 'ner',
name: '命名实体识别',
description: '标注文本中的实体',
icon: <Tag />,
dataFormat: 'text',
},
];
可视化标签编辑组件。
interface LabelConfig {
id: string;
name: string;
color: string;
hotkey?: string;
}
interface LabelEditorProps {
labels: LabelConfig[];
onChange: (labels: LabelConfig[]) => void;
taskType: TaskType;
}
// 预定义颜色列表
const PRESET_COLORS = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F',
'#BB8FCE', '#85C1E9', '#F8B500', '#00CED1',
];
根据配置生成 Label Studio XML。
interface XMLGeneratorConfig {
taskType: TaskType;
labels: LabelConfig[];
choiceType?: 'single' | 'multiple'; // 用于分类任务
}
class XMLGenerator {
static generate(config: XMLGeneratorConfig): string {
switch (config.taskType) {
case 'text_classification':
return this.generateTextClassification(config);
case 'image_classification':
return this.generateImageClassification(config);
case 'object_detection':
return this.generateObjectDetection(config);
case 'ner':
return this.generateNER(config);
default:
throw new Error(`Unsupported task type: ${config.taskType}`);
}
}
private static generateTextClassification(config: XMLGeneratorConfig): string {
const choiceAttr = config.choiceType === 'multiple' ? 'multiple' : 'single';
const labelsXML = config.labels
.map(l => ` <Choice value="${l.name}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
.join('\n');
return `<View>
<Text name="text" value="$text"/>
<Choices name="label" toName="text" choice="${choiceAttr}">
${labelsXML}
</Choices>
</View>`;
}
private static generateImageClassification(config: XMLGeneratorConfig): string {
const choiceAttr = config.choiceType === 'multiple' ? 'multiple' : 'single';
const labelsXML = config.labels
.map(l => ` <Choice value="${l.name}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
.join('\n');
return `<View>
<Image name="image" value="$image"/>
<Choices name="label" toName="image" choice="${choiceAttr}">
${labelsXML}
</Choices>
</View>`;
}
private static generateObjectDetection(config: XMLGeneratorConfig): string {
const labelsXML = config.labels
.map(l => ` <Label value="${l.name}" background="${l.color}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
.join('\n');
return `<View>
<Image name="image" value="$image"/>
<RectangleLabels name="label" toName="image">
${labelsXML}
</RectangleLabels>
</View>`;
}
private static generateNER(config: XMLGeneratorConfig): string {
const labelsXML = config.labels
.map(l => ` <Label value="${l.name}" background="${l.color}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
.join('\n');
return `<View>
<Text name="text" value="$text"/>
<Labels name="label" toName="text">
${labelsXML}
</Labels>
</View>`;
}
}
项目配置向导组件。
type WizardStep = 'task-type' | 'labels' | 'details' | 'preview';
interface ProjectConfigWizardProps {
initialTaskType?: TaskType;
initialLabels?: LabelConfig[];
onComplete: (config: ProjectConfigResult) => void;
onCancel: () => void;
}
interface ProjectConfigResult {
taskType: TaskType;
labels: LabelConfig[];
xmlConfig: string;
name?: string;
description?: string;
}
外部项目管理页面。
interface ExternalProjectsPageProps {}
// 页面状态
interface ExternalProjectsState {
projects: Project[];
loading: boolean;
error: string | null;
statusFilter: ProjectStatus | 'all';
}
// 过滤逻辑:仅显示外部项目
const loadExternalProjects = async (statusFilter?: ProjectStatus) => {
return await listProjects({
source: 'external',
status: statusFilter,
});
};
优化后的项目管理页面。
// 默认过滤条件:仅显示进行中和已完成的项目
const DEFAULT_STATUS_FILTER = ['in_progress', 'completed'];
// 加载项目时应用过滤
const loadProjects = async (sourceFilter?: ProjectSource) => {
const allProjects = await listProjects({
source: sourceFilter,
});
// 过滤只显示进行中和已完成的项目
return allProjects.filter(p =>
p.status === 'in_progress' || p.status === 'completed'
);
};
interface LabelConfig {
id: string; // 唯一标识符
name: string; // 标签名称
color: string; // 标签颜色(十六进制)
hotkey?: string; // 快捷键(可选)
}
type TaskType =
| 'text_classification'
| 'image_classification'
| 'object_detection'
| 'ner';
interface ProjectConfig {
taskType: TaskType;
labels: LabelConfig[];
choiceType?: 'single' | 'multiple';
xmlConfig: string;
}
A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
For any 用户角色,如果角色为 'admin',则可见菜单项数量应等于所有菜单项数量;如果角色为 'annotator',则可见菜单项数量应等于非管理员专属菜单项数量。
Validates: Requirements 1.1, 1.2
For any 用户对象,如果 role 字段为 'admin' 则 isAdmin 应为 true,如果 role 字段为 'annotator' 则 isAdmin 应为 false。
Validates: Requirements 1.5, 1.6
For any 项目列表,外部项目管理页面应仅包含 source='external' 的项目,项目管理页面应仅包含 status='in_progress' 或 status='completed' 的项目。
Validates: Requirements 2.3, 3.1
For any 项目,如果状态为 'draft' 或 'configuring' 则应显示配置按钮,如果状态为 'ready' 则应显示分发按钮。
Validates: Requirements 2.6, 2.7
For any 用户访问项目管理页面,如果用户角色为 'annotator' 则创建项目按钮应隐藏,如果用户角色为 'admin' 则创建项目按钮应显示。
Validates: Requirements 3.7, 3.8
For any 有效的标注类型和标签配置,生成的 XML 应包含所有配置的标签,且 XML 格式应符合 Label Studio 规范(以 <View> 开头和结尾)。
Validates: Requirements 4.6, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7
For any 标签列表操作(添加、删除、编辑),操作后的标签列表应正确反映变更,且不应存在重复的标签名称。
Validates: Requirements 7.1, 7.2, 7.3, 7.8, 7.9, 7.10, 7.11
// 配置验证错误
interface ConfigValidationError {
type: 'validation';
field: string;
message: string;
}
// 标签验证错误
interface LabelValidationError {
type: 'label';
labelId: string;
message: string;
}
// 错误消息
const ERROR_MESSAGES = {
EMPTY_LABEL_NAME: '标签名称不能为空',
DUPLICATE_LABEL_NAME: '标签名称不能重复',
INVALID_HOTKEY: '快捷键格式无效',
NO_LABELS: '至少需要添加一个标签',
INVALID_XML: 'XML 配置格式无效',
};
function validateLabels(labels: LabelConfig[]): LabelValidationError[] {
const errors: LabelValidationError[] = [];
const names = new Set<string>();
labels.forEach(label => {
// 检查空名称
if (!label.name.trim()) {
errors.push({
type: 'label',
labelId: label.id,
message: ERROR_MESSAGES.EMPTY_LABEL_NAME,
});
}
// 检查重复名称
if (names.has(label.name)) {
errors.push({
type: 'label',
labelId: label.id,
message: ERROR_MESSAGES.DUPLICATE_LABEL_NAME,
});
}
names.add(label.name);
});
return errors;
}
使用 fast-check 库进行属性测试:
// jest.config.ts
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
// fast-check 配置
const fcConfig = {
numRuns: 100,
verbose: true,
};
┌─────────────────────────────────┐
│ [Icon] │
│ │
│ 文本分类 │
│ 对文本内容进行分类标注 │
│ │
│ ○ 选择 │
└─────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 标签配置 [+ 添加] │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 正面评价 快捷键: 1 [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 负面评价 快捷键: 2 [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 中性评价 快捷键: 3 [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
步骤指示器:
[1. 选择类型] ─── [2. 配置标签] ─── [3. 项目信息] ─── [4. 预览确认]
● ○ ○ ○