4、UI插件基础:React组件开发、CVAT UI组件库使用、样式隔离、事件通信
好,咱们进入UI插件开发的正题了。
说实话,CVAT的插件体系里,UI部分是最直观、也最容易让开发者有成就感的。你写一个组件,挂上去,立刻就能在界面上看到效果。但这里头也有不少坑,我踩过,今天帮你一一避开。
4.1 React组件开发——从零搭一个插件界面
CVAT的UI层基于React,这你应该知道。但我要强调一点:插件组件不是独立运行的,它寄生在CVAT的主渲染树里。
什么意思呢?
你写的组件,会被CVAT的插件系统动态挂载到某个DOM节点上。这个节点可能是侧边栏、工具栏、或者标注画布上的某个浮层。所以你的组件必须遵循React的标准生命周期,同时还要考虑它所在的宿主环境。
我个人习惯,写插件组件时先定义一个最小的骨架:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
const MyPluginPanel = () => {
const jobs = useSelector(state => state.jobs.current);
const dispatch = useDispatch();
const handleAction = () => {
dispatch({ type: 'MY_PLUGIN_ACTION', payload: { jobId: jobs.id } });
};
return (
<div className="my-plugin-panel">
<h3>我的插件</h3>
<button onClick={handleAction}>执行操作</button>
</div>
);
};
export default MyPluginPanel;
你看,这里直接用了Redux的useSelector和useDispatch。为什么?因为CVAT的全局状态管理就是Redux,你的插件天然可以访问到所有标注数据、任务信息、用户状态。我在项目中遇到过有人自己又搞了一套状态管理,结果和CVAT主应用的数据不同步,调试了两天才发现是状态源没统一。
4.2 CVAT UI组件库使用——别重复造轮子
CVAT内部有一套成熟的UI组件库,叫CVAT UI Kit。它基于Ant Design做了二次封装,但加了很多标注场景专用的组件。
比如:
LabelSelector:标签选择器,直接绑定当前任务的标签列表AttributeEditor:属性编辑器,自动渲染不同类型的属性输入框JobStatusBadge:任务状态徽章,显示当前标注进度CanvasOverlay:画布浮层容器,用于在标注区域上方显示自定义UI
我建议你开发前先翻一遍CVAT的源码,看看cvat-ui/src/components目录下有哪些现成组件。我曾经花了两天写了一个标签管理面板,结果发现CVAT UI Kit里已经有LabelManager组件了,直接引入改改配置就行。嗯,那感觉挺尴尬的。
引入方式很简单:
import { LabelSelector, AttributeEditor } from 'cvat-ui/dist/components/annotation';
但要注意版本匹配。CVAT的UI组件库没有单独发布到npm,你需要从CVAT项目的node_modules里引用,或者配置webpack的alias指向本地构建产物。
| 组件名 | 用途 | 常用Props |
|---|---|---|
| LabelSelector | 选择当前任务的标签 | taskId, value, onChange |
| AttributeEditor | 编辑标签的属性值 | attribute, value, onChange |
| JobStatusBadge | 显示任务状态 | status, size |
| CanvasOverlay | 在画布上叠加UI | position, children |
4.3 样式隔离——别让你的CSS污染了CVAT
这是新手最容易翻车的地方。
CVAT的样式用了CSS Modules和BEM命名规范。你的插件如果直接写全局CSS,很可能覆盖掉CVAT本身的样式,或者被CVAT的样式覆盖。
我推荐三种方案:
- CSS Modules:和CVAT保持一致。你的样式文件命名为
MyPlugin.module.css,然后通过import styles from './MyPlugin.module.css'引入。类名会被自动哈希,不会冲突。 - Styled Components:用CSS-in-JS方案。我个人比较喜欢这个,因为样式和组件逻辑在一起,维护起来方便。而且它天然隔离,不会泄漏到全局。
- Shadow DOM:如果你的插件需要完全独立于CVAT的样式体系,可以用Shadow DOM包裹。但要注意,Shadow DOM里的事件冒泡和表单提交行为有些差异,需要额外处理。
举个例子,用CSS Modules写一个面板:
/* MyPlugin.module.css */
.panel {
background: #f0f5ff;
border: 1px solid #d6e4ff;
border-radius: 8px;
padding: 16px;
margin: 8px 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: #1d1d1d;
margin-bottom: 12px;
}
// MyPlugin.jsx
import styles from './MyPlugin.module.css';
const MyPlugin = () => (
<div className={styles.panel}>
<h3 className={styles.title}>我的插件面板</h3>
</div>
);
!important来覆盖CVAT样式。我曾经见过一个插件因为用了!important,导致CVAT的标注工具按钮全部变形,用户反馈说「标注按钮点不动了」。排查了半天,发现是插件的一个!important把按钮的z-index覆盖了。
4.4 事件通信——让插件和CVAT「对话」
插件不是孤岛。它需要知道用户当前选中了哪个标注框、标注状态有没有变化、任务有没有切换。这些信息怎么获取?
CVAT提供了三种事件通信机制:
- Redux Store:全局状态共享。你的插件通过
useSelector订阅需要的状态片段,通过dispatch触发action来修改状态。这是最推荐的方式。 - CVAT Event Bus:一个基于EventEmitter的全局事件总线。CVAT会在特定时机触发事件,比如
annotation:created、annotation:updated、job:changed。你的插件可以监听这些事件。 - Plugin API:CVAT暴露了一些插件专用的API方法,比如
getCurrentJob()、getSelectedShapes()。这些方法封装了复杂的Redux选择器逻辑,用起来更简单。
我一般这样用:
import { useDispatch, useSelector } from 'react-redux';
import { getSelectedShapes } from 'cvat-core/src/plugins/api';
const MyPlugin = () => {
const selectedShapes = useSelector(state => {
const job = state.annotation.job;
if (!job) return [];
return getSelectedShapes(job);
});
// 或者用事件总线监听
useEffect(() => {
const handler = (event) => {
console.log('标注创建了:', event.detail);
};
window.cvatEvents.addEventListener('annotation:created', handler);
return () => window.cvatEvents.removeEventListener('annotation:created', handler);
}, []);
return (
<div>
<p>当前选中 {selectedShapes.length} 个标注</p>
</div>
);
};
这里有个细节:事件总线的addEventListener和removeEventListener一定要成对出现,否则会造成内存泄漏。我在项目中就踩过这个坑——插件卸载了,但事件监听还挂在全局,导致每次切换任务都重复执行回调,页面越来越卡。
4.5 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
说白了,UI插件开发就是围绕这四个点展开的。组件是骨架,UI库是现成的积木,样式隔离是防护墙,事件通信是神经线。把这四个点吃透了,你就能写出既稳定又漂亮的插件。
嗯,下一节我们聊聊更进阶的话题——插件如何与CVAT的标注引擎深度交互。不过那是后话了,先把今天的内容消化掉。
公众号:蓝海资料掘金营,微信deep3321