feat(annotation): 添加标注编辑器侧边栏折叠功能

- 引入 MenuFoldOutlined 和 MenuUnfoldOutlined 图标用于侧边栏控制
- 添加 sidebarCollapsed 状态管理侧边栏展开/收起状态
- 扩展 Label Studio 界面配置,启用完整的标注界面组件
- 实现可折叠的左侧文件列表,支持展开/收起操作
- 优化顶部工具栏布局,调整标题层级和按钮标签
- 改进文件列表样式,添加悬停效果和更清晰的状态标识
- 调整整体布局结构,提升编辑器区域的空间利用率
This commit is contained in:
2026-01-19 16:10:26 +08:00
parent a778ac23b5
commit ed7a5c6873

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { App, Button, Card, List, Spin, Typography } from "antd"; import { App, Button, Card, List, Spin, Typography } from "antd";
import { LeftOutlined, ReloadOutlined, SaveOutlined } from "@ant-design/icons"; import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { import {
@@ -54,6 +54,7 @@ export default function LabelStudioTextEditor() {
const [project, setProject] = useState<EditorProjectInfo | null>(null); const [project, setProject] = useState<EditorProjectInfo | null>(null);
const [tasks, setTasks] = useState<EditorTaskListItem[]>([]); const [tasks, setTasks] = useState<EditorTaskListItem[]>([]);
const [selectedFileId, setSelectedFileId] = useState<string>(""); const [selectedFileId, setSelectedFileId] = useState<string>("");
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const postToIframe = (type: string, payload?: any) => { const postToIframe = (type: string, payload?: any) => {
const win = iframeRef.current?.contentWindow; const win = iframeRef.current?.contentWindow;
@@ -128,15 +129,29 @@ export default function LabelStudioTextEditor() {
labelConfig: project.labelConfig, labelConfig: project.labelConfig,
task, task,
user: { id: "datamate" }, user: { id: "datamate" },
// 完整的 Label Studio 原生界面配置
interfaces: [ interfaces: [
"panel", // 核心面板
"update", "panel", // 导航面板(undo/redo/reset)
"controls", "update", // 更新按钮
"submit", // 提交按钮
"controls", // 控制面板
// 侧边栏(包含 Outliner 和 Details)
"side-column", "side-column",
// 标注管理
"annotations:tabs", "annotations:tabs",
"annotations:menu", "annotations:menu",
"annotations:current", "annotations:current",
"annotations:add-new",
"annotations:delete",
"annotations:view-all",
"annotations:history", "annotations:history",
// 预测
"predictions:tabs",
"predictions:menu",
// 其他
"auto-annotation",
"edit-history",
], ],
selectedAnnotationIndex: 0, selectedAnnotationIndex: 0,
allowCreateEmptyAnnotation: true, allowCreateEmptyAnnotation: true,
@@ -282,19 +297,25 @@ export default function LabelStudioTextEditor() {
} }
return ( return (
<div className="h-full flex flex-col gap-3"> <div className="h-full flex flex-col">
<div className="flex items-center justify-between"> {/* 顶部工具栏 */}
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-white">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button icon={<LeftOutlined />} onClick={() => navigate("/data/annotation")}> <Button icon={<LeftOutlined />} onClick={() => navigate("/data/annotation")}>
</Button> </Button>
<Typography.Title level={4} style={{ margin: 0 }}> <Button
icon={sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
title={sidebarCollapsed ? "展开文件列表" : "收起文件列表"}
/>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title> </Typography.Title>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button icon={<ReloadOutlined />} loading={loadingTasks} onClick={() => loadTasks()}> <Button icon={<ReloadOutlined />} loading={loadingTasks} onClick={() => loadTasks()}>
</Button> </Button>
<Button <Button
type="primary" type="primary"
@@ -308,49 +329,60 @@ export default function LabelStudioTextEditor() {
</div> </div>
</div> </div>
<div className="flex items-stretch gap-3 flex-1 min-h-0"> {/* 主体区域 */}
<Card <div className="flex flex-1 min-h-0">
title="文件" {/* 左侧文件列表 - 可折叠 */}
className="h-full flex flex-col" <div
style={{ width: 320 }} className="border-r border-gray-200 bg-gray-50 flex flex-col transition-all duration-200"
bodyStyle={{ padding: 0, flex: 1, overflow: "auto" }} style={{ width: sidebarCollapsed ? 0 : 240, overflow: "hidden" }}
> >
<div className="px-3 py-2 border-b border-gray-200 bg-white font-medium text-sm">
</div>
<div className="flex-1 overflow-auto">
<List <List
loading={loadingTasks} loading={loadingTasks}
size="small"
dataSource={tasks} dataSource={tasks}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
key={item.fileId} key={item.fileId}
className="cursor-pointer hover:bg-blue-50"
style={{ style={{
cursor: "pointer", background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
background: item.fileId === selectedFileId ? "#f0f5ff" : undefined, padding: "8px 12px",
paddingLeft: 12, borderBottom: "1px solid #f0f0f0",
paddingRight: 12,
}} }}
onClick={() => setSelectedFileId(item.fileId)} onClick={() => setSelectedFileId(item.fileId)}
> >
<div className="flex flex-col w-full"> <div className="flex flex-col w-full gap-1">
<div className="flex items-center justify-between gap-2"> <Typography.Text ellipsis style={{ fontSize: 13 }}>
<Typography.Text ellipsis>{item.fileName}</Typography.Text> {item.fileName}
<Typography.Text type="secondary" style={{ whiteSpace: "nowrap" }}> </Typography.Text>
<div className="flex items-center justify-between">
<Typography.Text
type={item.hasAnnotation ? "success" : "secondary"}
style={{ fontSize: 11 }}
>
{item.hasAnnotation ? "已标注" : "未标注"} {item.hasAnnotation ? "已标注" : "未标注"}
</Typography.Text> </Typography.Text>
</div>
{item.annotationUpdatedAt && ( {item.annotationUpdatedAt && (
<Typography.Text type="secondary" style={{ fontSize: 12 }}> <Typography.Text type="secondary" style={{ fontSize: 10 }}>
: {item.annotationUpdatedAt} {item.annotationUpdatedAt}
</Typography.Text> </Typography.Text>
)} )}
</div> </div>
</div>
</List.Item> </List.Item>
)} )}
/> />
</Card> </div>
</div>
<Card title="编辑器" className="flex-1 h-full flex flex-col" bodyStyle={{ padding: 0, flex: 1, overflow: "hidden" }}> {/* 右侧编辑器 - Label Studio iframe */}
<div className="relative h-full"> <div className="flex-1 relative">
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && ( {(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70"> <div className="absolute inset-0 z-10 flex items-center justify-center bg-white/80">
<Spin <Spin
tip={ tip={
!iframeReady !iframeReady
@@ -369,7 +401,6 @@ export default function LabelStudioTextEditor() {
className="w-full h-full border-0" className="w-full h-full border-0"
/> />
</div> </div>
</Card>
</div> </div>
</div> </div>
); );