You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

11 KiB

新建项目流程优化

日期:2026-02-08 类型:功能优化 影响范围:前端 - CreateProjectModal


变更概述

优化新建项目流程,移除项目类型选择,固定显示集数字段,新增共享资源选择功能。

背景

根据 ADR 02: 跨项目资源共享 的设计,用户在创建项目时:

  • 项目内容类型(content_type)选择增加了用户认知负担
  • 用户更关心资源共享而非项目分类
  • 需要支持跨项目复用资源(角色、场景、道具)

变更内容

1. 移除项目类型选择

变更前

<FormItem label="项目类型">
  <Select value={projectType}>
    <SelectItem value="ad">广告片</SelectItem>
    <SelectItem value="movie">电影</SelectItem>
    <SelectItem value="series">剧集</SelectItem>
    {/* ... */}
  </Select>
</FormItem>

变更后

  • 完全移除项目类型选择字段
  • 表单 schema 不再包含 projectType
  • 后端 API 调用时不再传递 contentType 参数

影响

  • 简化用户操作流程
  • 减少认知负担
  • 项目创建更聚焦于实际需求(集数、时长、资源)

2. 集数字段固定显示

变更前

// 仅当项目类型为 series 或 anime 时显示
{showEpisodes && (
  <FormItem label="集数">
    <InputNumber value={episodes} />
  </FormItem>
)}

变更后

// 始终显示集数字段
<FormItem label="集数" required>
  <InputNumber min={1} value={episodes ?? 1} />
</FormItem>

影响

  • 所有项目默认支持多集结构
  • 单集项目设置为 1 集即可
  • 统一项目数据模型

3. 新增共享资源选择功能

3.1 UI 结构

新增两个组件:

  • ResourceSelectorPanel.tsx - 资源选择面板(树形结构)
  • SelectedResourcesDisplay.tsx - 已选资源展示(Tag 列表)

3.2 交互流程

┌─────────────────────────────────────────────────────────────────────┐
│ 项目属性                                                    [关闭 ✕] │
├─────────────────────────────────────────────────────────────────────┤
│ 项目名称: [_____________________]                                   │
│ 集数: [1]  单集计划时长: [60] [分钟▾]                              │
│                                                                     │
│ 共享资源                                         [选择资源] ◄─────┐ │
│ ┌─────────────────────────────────────────────────────────────┐   │ │
│ │ [角色] 孙悟空 (西游记第一季) [✕]                            │   │ │
│ │ [场景] 花果山 (西游记第一季) [✕]                            │   │ │
│ │ [道具] 金箍棒 (西游记第一季) [✕]                            │   │ │
│ └─────────────────────────────────────────────────────────────┘   │ │
│                                           [取消]  [立即创建]       │ │
└─────────────────────────────────────────────────────────────────────┘
                                                                      │
                点击"选择资源"展开右侧面板 ─────────────────────────┘
                                                                      │
┌─────────────────────────────────────────────────────────────────────┤
│                                              ┌────────────────────┐ │
│                                              │ 共享资源           │ │
│                                              ├────────────────────┤ │
│                                              │ [ ] ▶ 我的项目     │ │
│                                              │ [✓] ▼ 西游记系列   │ │
│                                              │   [✓] 孙悟空       │ │
│                                              │   [✓] 花果山       │ │
│                                              │   [✓] 金箍棒       │ │
│                                              │                    │ │
│                                              │ 已选 3 项资源      │ │
│                                              └────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

3.3 数据结构

interface SharedResource {
  id: string;
  type: 'project' | 'folder' | 'character' | 'scene' | 'prop';
  name: string;
  parentName?: string;
  count?: number;
}

3.4 核心特性

ResourceSelectorPanel

  • 树形结构展示项目/文件夹层级
  • 支持多选(Checkbox)
  • 显示资源统计(角色数、场景数、道具数)
  • 展开/收起控制
  • 滚动区域(ScrollArea)

SelectedResourcesDisplay

  • Tag 列表展示已选资源
  • 不同资源类型不同颜色标识
    • 项目/文件夹:蓝色/紫色
    • 角色:绿色
    • 场景:橙色
    • 道具:粉色
  • 支持删除单个资源
  • 空状态提示

3.5 状态管理

// 共享资源面板展开状态
const [resourcePanelOpen, setResourcePanelOpen] = useState(false);

// 已选资源列表
const [selectedResources, setSelectedResources] = useState<SharedResource[]>([]);

// 资源选择回调
const handleResourceSelect = useCallback((resources: SharedResource[]) => {
  setSelectedResources(resources);
}, []);

// 删除单个资源
const handleRemoveResource = useCallback((resourceId: string) => {
  setSelectedResources(prev => prev.filter(r => r.id !== resourceId));
}, []);

4. 面板互斥逻辑

风格选择面板和资源选择面板互斥:

onClick={() => {
  setResourcePanelOpen(prev => !prev);
  setStylePanelOpen(false); // 关闭风格面板
}}

技术实现

文件变更

修改

  • client/src/components/features/project/CreateProjectModal.tsx

新增

  • client/src/components/features/project/ResourceSelectorPanel.tsx
  • client/src/components/features/project/SelectedResourcesDisplay.tsx

依赖组件

  • @/components/ui/checkbox - 复选框
  • @/components/ui/scroll-area - 滚动区域
  • lucide-react - 图标
    • Share2 - 共享资源按钮
    • Folder - 项目/文件夹图标
    • Users - 角色图标
    • MapPin - 场景图标
    • Package - 道具图标
    • ChevronRight/ChevronDown - 展开/收起图标

表单验证调整

// 移除 projectType 字段
const createProjectSchema = z.object({
  name: z.string().min(1).max(50),
  folderId: z.string().optional(),
  episodes: z.number().min(1), // 必填,不再可选
  aspectRatio: z.enum([...]),
  duration: z.string().min(1),
  durationUnit: z.enum(['seconds', 'minutes']),
  styleAndRoles: z.string().max(500).optional(),
});

API 调用调整

// 创建项目
const newProject = await createProject.mutateAsync({
  name: data.name,
  folderId: data.folderId?.startsWith('virtual-') ? undefined : data.folderId,
  aspectRatio: data.aspectRatio,
  plannedDuration: durationInSeconds,
  styleAndCharacters,
  // TODO: 待后端 API 支持
  // sharedResources: selectedResources.map(r => ({ id: r.id, type: r.type })),
});

后续工作

Phase 1:Mock 数据替换为真实 API

当前状态

  • 资源树使用硬编码的 mock 数据
  • 位置:ResourceSelectorPanel.tsx 第 26-87 行

待实现

// 1. 创建 API 接口
// GET /api/v1/projects/shareable-resources
export async function getShareableResources(): Promise<ResourceTreeNode[]> {
  const response = await apiClient.get('/projects/shareable-resources');
  return response.data;
}

// 2. 使用 React Query 获取数据
const { data: resourceTree } = useQuery({
  queryKey: ['shareable-resources'],
  queryFn: getShareableResources,
  enabled: resourcePanelOpen, // 仅展开时加载
});

Phase 2:保存共享关系

待实现

// 创建项目时传递共享资源
const newProject = await createProject.mutateAsync({
  // ... 其他字段
  sharedResources: selectedResources.map(r => ({
    resourceId: r.id,
    resourceType: r.type,
  })),
});

后端需求

  • 参考 ADR 02 第 6.1 节:新建项目时共享资源
  • 接口:POST /api/v1/projects
  • 请求体新增字段:shared_resources: Array<{resource_id, resource_type, share_type}>

Phase 3:性能优化

优化方向

  1. 虚拟滚动(资源多时)

    • 使用 react-virtualreact-window
    • 优化大列表渲染性能
  2. 搜索过滤

    • 添加资源搜索框
    • 支持按名称过滤
  3. 批量操作

    • 全选/取消全选项目
    • 全选/取消全选文件夹下所有资源

测试建议

1. 功能测试

  • 创建项目时集数字段始终显示且默认为 1
  • 点击"选择资源"按钮展开右侧面板
  • 资源树正确展开/收起
  • 多选资源后在左侧 Tag 列表正确显示
  • 删除单个资源 Tag 正常工作
  • 风格面板和资源面板互斥显示
  • 弹窗宽度根据面板展开状态自适应

2. 边界测试

  • 未选择资源时显示空状态提示
  • 选择大量资源时滚动正常
  • 关闭弹窗后清空选择状态
  • 资源名称过长时截断显示

3. 样式测试

  • 不同资源类型颜色区分明显
  • 资源树缩进层级清晰
  • Hover 状态过渡自然
  • 暗色模式适配(如果有)

相关文档

变更记录

  • 2026-02-08:初始版本 - 移除项目类型、新增共享资源选择