You've already forked DataMate
feat(annotation): 添加标注编辑器侧边栏折叠功能
- 引入 MenuFoldOutlined 和 MenuUnfoldOutlined 图标用于侧边栏控制 - 添加 sidebarCollapsed 状态管理侧边栏展开/收起状态 - 扩展 Label Studio 界面配置,启用完整的标注界面组件 - 实现可折叠的左侧文件列表,支持展开/收起操作 - 优化顶部工具栏布局,调整标题层级和按钮标签 - 改进文件列表样式,添加悬停效果和更清晰的状态标识 - 调整整体布局结构,提升编辑器区域的空间利用率
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
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 {
|
||||
@@ -54,6 +54,7 @@ export default function LabelStudioTextEditor() {
|
||||
const [project, setProject] = useState<EditorProjectInfo | null>(null);
|
||||
const [tasks, setTasks] = useState<EditorTaskListItem[]>([]);
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>("");
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const postToIframe = (type: string, payload?: any) => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
@@ -128,15 +129,29 @@ export default function LabelStudioTextEditor() {
|
||||
labelConfig: project.labelConfig,
|
||||
task,
|
||||
user: { id: "datamate" },
|
||||
// 完整的 Label Studio 原生界面配置
|
||||
interfaces: [
|
||||
"panel",
|
||||
"update",
|
||||
"controls",
|
||||
// 核心面板
|
||||
"panel", // 导航面板(undo/redo/reset)
|
||||
"update", // 更新按钮
|
||||
"submit", // 提交按钮
|
||||
"controls", // 控制面板
|
||||
// 侧边栏(包含 Outliner 和 Details)
|
||||
"side-column",
|
||||
// 标注管理
|
||||
"annotations:tabs",
|
||||
"annotations:menu",
|
||||
"annotations:current",
|
||||
"annotations:add-new",
|
||||
"annotations:delete",
|
||||
"annotations:view-all",
|
||||
"annotations:history",
|
||||
// 预测
|
||||
"predictions:tabs",
|
||||
"predictions:menu",
|
||||
// 其他
|
||||
"auto-annotation",
|
||||
"edit-history",
|
||||
],
|
||||
selectedAnnotationIndex: 0,
|
||||
allowCreateEmptyAnnotation: true,
|
||||
@@ -282,19 +297,25 @@ export default function LabelStudioTextEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 顶部工具栏 */}
|
||||
<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">
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate("/data/annotation")}>
|
||||
返回
|
||||
</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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button icon={<ReloadOutlined />} loading={loadingTasks} onClick={() => loadTasks()}>
|
||||
刷新文件列表
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -308,68 +329,78 @@ export default function LabelStudioTextEditor() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-stretch gap-3 flex-1 min-h-0">
|
||||
<Card
|
||||
title="文件"
|
||||
className="h-full flex flex-col"
|
||||
style={{ width: 320 }}
|
||||
bodyStyle={{ padding: 0, flex: 1, overflow: "auto" }}
|
||||
{/* 主体区域 */}
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* 左侧文件列表 - 可折叠 */}
|
||||
<div
|
||||
className="border-r border-gray-200 bg-gray-50 flex flex-col transition-all duration-200"
|
||||
style={{ width: sidebarCollapsed ? 0 : 240, overflow: "hidden" }}
|
||||
>
|
||||
<List
|
||||
loading={loadingTasks}
|
||||
dataSource={tasks}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.fileId}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: item.fileId === selectedFileId ? "#f0f5ff" : undefined,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
onClick={() => setSelectedFileId(item.fileId)}
|
||||
>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Typography.Text ellipsis>{item.fileName}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ whiteSpace: "nowrap" }}>
|
||||
{item.hasAnnotation ? "已标注" : "未标注"}
|
||||
<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
|
||||
loading={loadingTasks}
|
||||
size="small"
|
||||
dataSource={tasks}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.fileId}
|
||||
className="cursor-pointer hover:bg-blue-50"
|
||||
style={{
|
||||
background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
}}
|
||||
onClick={() => setSelectedFileId(item.fileId)}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
||||
{item.fileName}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text
|
||||
type={item.hasAnnotation ? "success" : "secondary"}
|
||||
style={{ fontSize: 11 }}
|
||||
>
|
||||
{item.hasAnnotation ? "已标注" : "未标注"}
|
||||
</Typography.Text>
|
||||
{item.annotationUpdatedAt && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 10 }}>
|
||||
{item.annotationUpdatedAt}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item.annotationUpdatedAt && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
更新: {item.annotationUpdatedAt}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="编辑器" className="flex-1 h-full flex flex-col" bodyStyle={{ padding: 0, flex: 1, overflow: "hidden" }}>
|
||||
<div className="relative h-full">
|
||||
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70">
|
||||
<Spin
|
||||
tip={
|
||||
!iframeReady
|
||||
? "编辑器资源加载中..."
|
||||
: loadingTaskDetail
|
||||
? "任务数据加载中..."
|
||||
: "编辑器初始化中..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Label Studio Frontend"
|
||||
src={LSF_IFRAME_SRC}
|
||||
className="w-full h-full border-0"
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧编辑器 - Label Studio iframe */}
|
||||
<div className="flex-1 relative">
|
||||
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/80">
|
||||
<Spin
|
||||
tip={
|
||||
!iframeReady
|
||||
? "编辑器资源加载中..."
|
||||
: loadingTaskDetail
|
||||
? "任务数据加载中..."
|
||||
: "编辑器初始化中..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Label Studio Frontend"
|
||||
src={LSF_IFRAME_SRC}
|
||||
className="w-full h-full border-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user