# Jointo(jointo)组件设计文档
> **项目名称**:Jointo(jointo)
> **项目域名**:https://www.jointo.ai
> **文档版本**:v1.0
> **创建日期**:2025-01-08
> **参考设计稿**:`https://www.figma.com/make/xgPuMR3GZzHpYh0RZJdiYK/kaidong.ai`
---
## 目录
1. [组件架构总览](#1-组件架构总览)
2. [布局组件](#2-布局组件)
3. [分镜面板组件](#3-分镜面板组件)
4. [预览面板组件](#4-预览面板组件)
5. [分镜看板组件](#5-分镜看板组件)
6. [AI 提示词面板组件](#6-ai-提示词面板组件)
7. [右侧边栏组件](#7-右侧边栏组件)
8. [通用组件](#8-通用组件)
9. [弹窗组件](#9-弹窗组件)
10. [引导组件](#10-引导组件)
11. [组件通信模式](#11-组件通信模式)
---
## 1. 组件架构总览
### 1.1 整体布局结构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ TopBar(应用顶部栏) │
│ 菜单 | 超级视频工作台 | 新建项目/导出项目/AI工具箱 | 协作者/通知/备注/设置 │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────────────┬─────────────────────────────────────────┬──────────────────┐ │
│ │LeftSidebar │ CenterArea │ RightSidebar │ │
│ │ │ ┌────────────┬────────────────────────┐ │ │ │
│ │ [项目] │ │ 分镜描述 │ 预览窗口 │ │ [图片提示词] │ │
│ │ [素材库] │ │ │ │ │ [角度变换] │ │
│ │ │ │ 分镜1 │ ┌────────┬───────┐ │ │ [镜头调整] │ │
│ │ 我的项目 │ │ 文字描述 │ │主预览图│ 预览1 │ │ │ │ │
│ │ > 项目1 │ │ │ │ ├───────┤ │ │ 角色/场景描述 │ │
│ │ > 项目2 │ │ 分镜2 │ │ │ 预览2 │ │ │ │ │
│ │ > ... │ │ 文字描述 │ └────────┴───────┘ │ │ AI输入区 │ │
│ │ │ │ │ │ │ @角色 #场景 │ │
│ │ 协同项目 │ │ [调整] │ 播放控制 | 时间码 │ │ [对话改图 ▼] [➤] │ │
│ │ │ └────────────┴────────────────────────┘ │ │ │
│ └────────────┴─────────────────────────────────────────┴──────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────┤
│ TimelinePanel(分镜看板) │
│ 分镜看板 | - 100% + | 0s ─────────────────────────────────────────────── 60s │
│ 轨道: 分镜 | 资源 | 视频 | 音效 | 台词 | 配音 │
│ + 添加轨道 │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 1.2 React 组件层次结构
```tsx
{/* 顶部导航 */}
{/* 左侧边栏 - 项目/素材库 */}
{/* 项目列表 */}
{/* 素材库 */}
{/* 中间区域 - 分镜描述 + 预览窗口 */}
{/* 分镜描述面板 */}
{/* 预览窗口 */}
{/* 右侧边栏 - 图片提示词/角度变换/镜头调整 + AI输入 */}
{/* 图片提示词 */}
{/* 角度变换 */}
{/* 镜头调整 */}
{/* AI 输入区 */}
{/* 分镜看板 */}
{/* 弹窗层 */}
```
### 1.3 组件分类
| 类别 | 说明 | 示例 |
|------|------|------|
| **布局组件** | 定义页面结构 | `AppLayout`, `TopBar`, `LeftSidebar` |
| **功能组件** | 实现业务功能 | `StoryboardPanel`, `TimelinePanel` |
| **UI 组件** | 基础 UI 元素 | `Button`, `Input`, `Dialog` |
| **通用组件** | 跨模块复用 | `LoadingSpinner`, `EmptyState` |
| **弹窗组件** | 模态交互 | `CreateProjectModal`, `ExportModal` |
---
## 2. 布局组件
### 2.1 AppLayout
应用主布局组件,定义整体页面结构。
```tsx
// src/components/layout/AppLayout.tsx
interface AppLayoutProps {
children: React.ReactNode;
}
export function AppLayout({ children }: AppLayoutProps) {
const { leftSidebarCollapsed, rightSidebarCollapsed, timelineExpanded } = useUIStore();
return (
{/* 顶部导航栏 - 固定 48px */}
{/* 主体区域 */}
{/* 左侧边栏 - 项目/素材库 */}
{/* 中间区域 - 分镜列表 + 预览 + AI输入 */}
{/* 右侧边栏 - 资源/视频/音效/字幕/配音 */}
{/* 分镜看板面板 */}
);
}
```
### 2.2 TopBar
顶部导航栏组件。
```tsx
// src/components/layout/TopBar.tsx
interface TopBarProps {
className?: string;
}
export function TopBar({ className }: TopBarProps) {
return (
{/* 左侧 - Logo & 项目信息 */}
{/* 中间 - 工具栏 */}
{/* 右侧 - 用户操作 */}
);
}
```
**子组件:**
| 组件 | 功能 |
|------|------|
| `Logo` | 产品 Logo,点击返回项目列表 |
| `ProjectSelector` | 项目名称下拉选择器 |
| `UndoRedoButtons` | 撤销/重做按钮组 |
| `ZoomControls` | 预览区缩放控制 |
| `ViewModeToggle` | 视图模式切换(单分镜/多分镜) |
| `CollaboratorsAvatars` | 协作者头像组 |
| `ExportButton` | 导出按钮 |
| `NotificationBell` | 通知铃铛 |
| `UserMenu` | 用户菜单下拉 |
### 2.3 LeftSidebar
左侧边栏容器,包含项目列表和素材库 Tab 切换。
```tsx
// src/components/layout/LeftSidebar.tsx
interface LeftSidebarProps {
collapsed: boolean;
className?: string;
}
export function LeftSidebar({ collapsed, className }: LeftSidebarProps) {
const { leftPanelActiveTab, setLeftPanelTab, toggleLeftSidebar } = useUIStore();
return (
);
}
```
### 2.4 CenterArea
中间区域容器,包含分镜列表、预览窗口和 AI 输入区三列布局。
```tsx
// src/components/layout/CenterArea.tsx
interface CenterAreaProps {
className?: string;
}
export function CenterArea({ className }: CenterAreaProps) {
return (
{/* 分镜描述面板 */}
{/* 预览窗口 */}
);
}
```
**子组件说明**:
| 组件 | 说明 |
|------|------|
| `StoryboardDescPanel` | 分镜描述面板,显示分镜文字描述和调整按钮 |
| `PreviewPanel` | 预览窗口,显示主预览图和小预览图 |
| `PlaybackControls` | 播放控制,时间码显示 |
### 2.5 RightSidebar
右侧边栏容器,包含图片提示词/角度变换/镜头调整 Tab 和 AI 输入区。
```tsx
// src/components/layout/RightSidebar.tsx
interface RightSidebarProps {
collapsed: boolean;
className?: string;
}
export function RightSidebar({ collapsed, className }: RightSidebarProps) {
const { rightPanelActiveTab, setRightPanelTab, toggleRightSidebar } = useUIStore();
if (collapsed) {
return (
);
}
return (
);
}
```
---
## 3. 分镜面板组件
### 3.1 StoryboardPanel
分镜列表面板,支持新建、排序、选择分镜。
```tsx
// src/components/features/storyboard/StoryboardPanel.tsx
interface StoryboardPanelProps {
collapsed?: boolean;
}
export function StoryboardPanel({ collapsed = false }: StoryboardPanelProps) {
const { currentProjectId } = useAppStore();
const { selectedStoryboardId, selectStoryboard } = useEditorStore();
const { data: storyboards, isLoading } = useStoryboards(currentProjectId!);
const createStoryboard = useCreateStoryboard();
// 拖拽排序
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor)
);
const handleDragEnd = (event: DragEndEvent) => {
// 处理拖拽排序
};
const handleAddStoryboard = () => {
createStoryboard.mutate({
projectId: currentProjectId!,
title: `分镜 ${(storyboards?.length || 0) + 1}`,
description: '',
duration: 5,
});
};
if (collapsed) {
return (
{storyboards?.map((storyboard, index) => (
selectStoryboard(storyboard.id)}
/>
))}
);
}
return (
{/* 头部 */}
{/* 分镜列表 */}
{isLoading ? (
) : storyboards?.length === 0 ? (
}
title="暂无分镜"
description="点击上方按钮创建第一个分镜"
/>
) : (
s.id)}>
{storyboards?.map((storyboard, index) => (
))}
)}
);
}
```
### 3.2 StoryboardItem
单个分镜项组件。
```tsx
// src/components/features/storyboard/StoryboardItem.tsx
interface StoryboardItemProps {
storyboard: Storyboard;
index: number;
isSelected: boolean;
onSelect: (id: string) => void;
}
export const StoryboardItem = memo(function StoryboardItem({
storyboard,
index,
isSelected,
onSelect,
}: StoryboardItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: storyboard.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
onSelect(storyboard.id)}
{...attributes}
{...listeners}
>
{/* 序号 */}
{index}
{/* 缩略图 */}
{storyboard.thumbnailUrl ? (

) : (
)}
{/* 信息 */}
{storyboard.title}
{storyboard.description || '无描述'}
{formatDuration(storyboard.duration)}
{/* 操作按钮 */}
);
});
```
### 3.3 StoryboardEditor
分镜详情编辑组件(在预览区或弹窗中使用)。
```tsx
// src/components/features/storyboard/StoryboardEditor.tsx
interface StoryboardEditorProps {
storyboard: Storyboard;
onSave: (data: UpdateStoryboardDto) => void;
}
export function StoryboardEditor({ storyboard, onSave }: StoryboardEditorProps) {
const form = useForm({
resolver: zodResolver(updateStoryboardSchema),
defaultValues: {
title: storyboard.title,
description: storyboard.description,
duration: storyboard.duration,
prompt: storyboard.prompt,
},
});
return (
);
}
```
---
## 4. 预览面板组件
### 4.1 PreviewPanel
预览面板,展示当前分镜的预览内容。
```tsx
// src/components/features/preview/PreviewPanel.tsx
export function PreviewPanel() {
const { selectedStoryboardId } = useEditorStore();
const { data: storyboard } = useStoryboard(selectedStoryboardId!);
if (!selectedStoryboardId) {
return (
}
title="选择分镜"
description="从左侧选择一个分镜进行预览"
/>
);
}
return (
{/* 预览区域 */}
{storyboard?.videoUrl ? (
) : storyboard?.thumbnailUrl ? (

) : (
)}
{/* 分镜信息叠加层 */}
{storyboard?.title}
{storyboard?.description}
);
}
```
### 4.2 VideoPlayer
视频播放器组件。
```tsx
// src/components/features/preview/VideoPlayer.tsx
interface VideoPlayerProps {
src: string;
poster?: string;
onTimeUpdate?: (time: number) => void;
onDurationChange?: (duration: number) => void;
onEnded?: () => void;
}
export function VideoPlayer({
src,
poster,
onTimeUpdate,
onDurationChange,
onEnded,
}: VideoPlayerProps) {
const videoRef = useRef(null);
const { isPlaying, setPlaying, setCurrentTime, setDuration } = useEditorStore();
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (isPlaying) {
video.play();
} else {
video.pause();
}
}, [isPlaying]);
const handleTimeUpdate = () => {
const video = videoRef.current;
if (!video) return;
setCurrentTime(video.currentTime);
onTimeUpdate?.(video.currentTime);
};
const handleLoadedMetadata = () => {
const video = videoRef.current;
if (!video) return;
setDuration(video.duration);
onDurationChange?.(video.duration);
};
const handleEnded = () => {
setPlaying(false);
onEnded?.();
};
return (