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.
 

13 KiB

共享资源选择器优化

日期:2026-02-08 类型:功能增强 影响范围:前端 - ResourceSelectorPanel


变更概述

全面优化共享资源选择器的 UI/UX,新增智能级联选择、搜索、过滤、快捷操作等功能,大幅提升可用性。

背景

初版资源选择器存在以下问题:

  1. 交互逻辑不清晰:选择父级不会自动选择子级,不符合用户心理模型
  2. 视觉层级不够:缩进仅 16px,层级关系不明显
  3. 缺少关键功能:无搜索、无过滤、无快捷操作
  4. 可用性问题:资源多时难以找到目标,操作效率低

核心优化

1. 智能级联选择

选择文件夹 → 自动选择其下所有项目及资源

选择"西游记系列"文件夹
  ↓ 自动选择
《西游记》第一季 + 其下所有角色/场景/道具
《西游记》第二季 + 其下所有角色/场景/道具

选择项目 → 自动选择其下所有资源

选择《西游记》第一季
  ↓ 自动选择
孙悟空、猪八戒、唐僧、沙和尚(角色)
花果山、高老庄、流沙河(场景)
金箍棒、九齿钉耙(道具)

取消父级 → 自动取消所有子级

取消选择《西游记》第一季
  ↓ 自动取消
其下所有角色/场景/道具全部取消选中

子级全选 → 父级自动全选;部分选 → 父级半选

[ ] 《西游记》第一季      ← 未选
[◐] 《西游记》第一季      ← 半选(部分子级选中)
[✓] 《西游记》第一季      ← 全选(所有子级选中)

实现算法

// 向下级联:选择父级 → 选择所有子孙
const descendantIds = getAllDescendantIds(node);
const allIds = [nodeId, ...descendantIds];

// 向上级联:根据子级状态计算父级状态
function getNodeCheckState(node, selectedIds) {
  if (selectedIds.has(node.id)) return 'checked';
  
  const childStates = node.children.map(child => 
    getNodeCheckState(child, selectedIds)
  );
  const checkedCount = childStates.filter(s => s === 'checked').length;
  
  if (checkedCount === node.children.length) return 'checked';
  if (checkedCount > 0) return 'indeterminate'; // 半选
  return 'unchecked';
}

2. 搜索功能 🔍

实时搜索 + 关键词高亮

┌─────────────────────────────────────┐
│ [🔍 搜索资源...] [×]                │
├─────────────────────────────────────┤
│ 搜索"孙悟空",找到 2 项:            │
│ [✓] 👤 孙悟空 (《西游记》第一季)    │
│     └─ 关键词高亮显示                │
│ [✓] 👤 孙悟空 (《西游记》第二季)    │
└─────────────────────────────────────┘

特性

  • 实时搜索(输入即搜)
  • 关键词黄色高亮
  • 显示父级路径(便于区分同名资源)
  • 空状态友好提示
  • 一键清空搜索(× 按钮)

3. 资源类型过滤 🎯

按钮组快速切换

┌─────────────────────────────────────┐
│ [全部] [角色] [场景] [道具]         │
└─────────────────────────────────────┘

使用场景

  • 只想选择角色时,过滤其他类型
  • 与搜索组合使用(如:搜索"孙悟空" + 过滤"角色")
  • 文件夹和项目节点始终显示(保持层级结构)

4. 快捷操作

工具栏提供批量操作

┌─────────────────────────────────────┐
│ [全选] | [取消全选] | [展开全部] | [收起] │
└─────────────────────────────────────┘

操作说明

  • 全选:选择所有资源(文件夹、项目、角色、场景、道具)
  • 取消全选:清空所有选择
  • 展开全部:展开所有层级(快速浏览全部内容)
  • 收起:收起到顶层(仅保留"我的项目"展开)

5. 改进的视觉设计 🎨

5.1 更清晰的层级缩进

变更前:16px 缩进

[ ] ▶ 📁 我的项目
[ ] ▶ 📁 西游记系列
[ ] ▶ 📂 《西游记》第一季 (角色 15, 场景 8, 道具 12)
[ ] 👤 孙悟空

变更后:24px 缩进 + 统计信息独立行

[ ] ▶ 📁 我的项目
[◐] ▼ 📁 西游记系列
  [◐] ▼ 📂 《西游记》第一季
    │  └─ 角色 15  场景 8  道具 12
    [✓] 👤 孙悟空
    [ ] 👤 猪八戒

改进点

  • 缩进增加到 24px(层级更清晰)
  • 统计信息独立一行(不挤在标题旁)
  • 使用 └─ 连接线(视觉引导)
  • 半选状态用 图标表示

5.2 Checkbox 半选状态

新增 indeterminate 支持

// Checkbox 组件支持三态
<Checkbox 
  checked={checkState === 'checked'}
  indeterminate={checkState === 'indeterminate'}
/>

// 渲染不同图标
checked:  (Check)
indeterminate: - (Minus)
unchecked: 

6. 增强的统计信息 📊

底部统计栏优化

变更前

已选 5 项资源

变更后

已选 5 项资源
角色 3  场景 1  道具 1

统计维度

  • 总数
  • 按类型分类(角色/场景/道具)
  • 实时更新

7. 宽度调整

面板宽度:320px → 380px

  • 搜索框更宽,输入体验更好
  • 统计信息显示更完整
  • 按钮组不拥挤

弹窗自适应宽度

  • 风格面板:600px + 320px = 920px
  • 资源面板:600px + 380px = 980px

技术实现

核心算法

1. 扁平化树结构

function flattenTree(nodes: ResourceTreeNode[]): Map<string, ResourceTreeNode> {
  const map = new Map();
  const traverse = (node) => {
    map.set(node.id, node);
    if (node.children) node.children.forEach(traverse);
  };
  nodes.forEach(traverse);
  return map;
}

2. 获取所有子孙节点

function getAllDescendantIds(node: ResourceTreeNode): string[] {
  if (!node.children) return [];
  const ids = [];
  for (const child of node.children) {
    ids.push(child.id);
    ids.push(...getAllDescendantIds(child));
  }
  return ids;
}

3. 搜索匹配

function searchNodes(
  nodes: ResourceTreeNode[],
  query: string,
  typeFilter: ResourceTypeFilter
): ResourceTreeNode[] {
  const results = [];
  const lowerQuery = query.toLowerCase();

  const traverse = (node) => {
    const typeMatch = typeFilter === 'all' || 
                      node.type === typeFilter ||
                      node.type === 'folder' || 
                      node.type === 'project';
    const nameMatch = node.name.toLowerCase().includes(lowerQuery);

    if (typeMatch && nameMatch) results.push(node);
    if (node.children) node.children.forEach(traverse);
  };

  nodes.forEach(traverse);
  return results;
}

4. 关键词高亮

const highlightedName = useMemo(() => {
  if (!searchQuery) return node.name;
  
  const lowerName = node.name.toLowerCase();
  const lowerQuery = searchQuery.toLowerCase();
  const index = lowerName.indexOf(lowerQuery);
  
  if (index === -1) return node.name;

  const before = node.name.slice(0, index);
  const match = node.name.slice(index, index + searchQuery.length);
  const after = node.name.slice(index + searchQuery.length);

  return (
    <>
      {before}
      <mark className="bg-yellow-200">{match}</mark>
      {after}
    </>
  );
}, [node.name, searchQuery]);

性能优化

  1. 使用 useMemo 缓存计算
const nodeMap = useMemo(() => flattenTree(mockResourceTree), []);
const selectedIds = useMemo(() => new Set(selectedResources.map(r => r.id)), [selectedResources]);
const filteredTree = useMemo(() => searchNodes(...), [searchQuery, typeFilter]);
  1. 使用 useCallback 稳定回调
const handleToggleSelect = useCallback((nodeId, checked) => { ... }, [nodeMap, selectedResources, onSelect]);
const handleExpandAll = useCallback(() => { ... }, [nodeMap]);
  1. 使用 memo 避免不必要的重渲染
const TreeNode = memo(function TreeNode({ ... }) { ... });
export const ResourceSelectorPanel = memo(function ResourceSelectorPanel({ ... }) { ... });

文件变更

修改

  • client/src/components/features/project/ResourceSelectorPanel.tsx - 完全重构
  • client/src/components/features/project/CreateProjectModal.tsx - 调整弹窗宽度
  • client/src/components/ui/checkbox.tsx - 新增半选状态支持

新增依赖

  • lucide-react 图标:
    • Minus - 半选图标
    • Search - 搜索图标
    • CheckSquare - 全选图标
    • Square - 取消全选图标
    • ChevronUp - 收起图标

交互演示

场景 1:选择整个文件夹

1. 点击"西游记系列"的 Checkbox
   ↓
2. 自动选中:
   - 《西游记》第一季 + 15个角色 + 8个场景 + 12个道具
   - 《西游记》第二季 + 18个角色 + 10个场景 + 15个道具
   ↓
3. 底部统计:已选 70 项资源
   角色 33  场景 18  道具 27

场景 2:搜索 + 过滤

1. 输入搜索:"悟空"
   ↓ 显示
   - 👤 孙悟空 (《西游记》第一季)
   - 👤 孙悟空 (《西游记》第二季)

2. 点击过滤"角色"
   ↓ 仅显示角色类型,场景和道具被过滤

3. 勾选两个孙悟空
   ↓ 底部统计:已选 2 项资源
   角色 2

场景 3:半选状态

初始状态:
[ ] 《西游记》第一季(未选)
  [ ] 孙悟空
  [ ] 猪八戒
  [ ] 唐僧

选择孙悟空后:
[◐] 《西游记》第一季(半选 - 部分子级选中)
  [✓] 孙悟空
  [ ] 猪八戒
  [ ] 唐僧

全部选中后:
[✓] 《西游记》第一季(全选 - 所有子级选中)
  [✓] 孙悟空
  [✓] 猪八戒
  [✓] 唐僧

用户体验提升

操作效率提升

操作 变更前 变更后 提升
选择文件夹下所有资源 需逐个展开并勾选(~50次点击) 1次点击 98% ↑
找到"孙悟空" 需手动展开查找(~10次点击) 搜索框输入(1次) 90% ↑
只选角色资源 需逐个跳过场景和道具 过滤"角色"(1次点击) 80% ↑
展开全部层级 需逐层点击展开(~8次) "展开全部"(1次点击) 87.5% ↑

认知负担降低

方面 变更前 变更后
层级关系 缩进小,难以区分 缩进明显,连接线清晰
选择反馈 不知道选了哪些子级 半选状态明确提示
统计信息 仅总数 总数 + 分类统计
空状态 无内容提示 友好提示"未找到匹配的资源"

后续优化方向

Phase 1:性能优化

  • 虚拟滚动(资源数 > 100 时)
  • 搜索防抖(300ms)
  • 记忆展开状态(LocalStorage)

Phase 2:高级功能

  • 批量操作(如:全选某文件夹下的角色)
  • 拖拽排序(调整资源优先级)
  • 最近使用/推荐资源(智能推荐)

Phase 3:数据集成

  • 替换 mock 数据为真实 API
  • 加载状态优化(骨架屏)
  • 错误处理(网络失败重试)

测试建议

功能测试

  • 级联选择

    • 选择文件夹自动选择所有子级
    • 选择项目自动选择所有资源
    • 取消父级自动取消所有子级
    • 半选状态正确显示
  • 搜索功能

    • 实时搜索生效
    • 关键词高亮正确
    • 空状态提示显示
    • 清空搜索恢复原始列表
  • 资源类型过滤

    • 过滤"角色"仅显示角色
    • 过滤"场景"仅显示场景
    • 过滤"道具"仅显示道具
    • "全部"显示所有类型
  • 快捷操作

    • 全选选中所有资源
    • 取消全选清空选择
    • 展开全部展开所有层级
    • 收起恢复默认状态

边界测试

  • 大数据量(100+ 资源)性能
  • 深层级嵌套(5+ 层)渲染
  • 搜索无结果场景
  • 空文件夹场景

视觉测试

  • 缩进层级清晰
  • 半选图标显示正确
  • 关键词高亮可见
  • 统计信息对齐

相关文档

变更记录

  • 2026-02-08:完整优化版本 - 智能级联、搜索、过滤、快捷操作