# 共享资源选择器优化
> **日期**:2026-02-08
> **类型**:功能增强
> **影响范围**:前端 - ResourceSelectorPanel
---
## 变更概述
全面优化共享资源选择器的 UI/UX,新增智能级联选择、搜索、过滤、快捷操作等功能,大幅提升可用性。
## 背景
初版资源选择器存在以下问题:
1. **交互逻辑不清晰**:选择父级不会自动选择子级,不符合用户心理模型
2. **视觉层级不够**:缩进仅 16px,层级关系不明显
3. **缺少关键功能**:无搜索、无过滤、无快捷操作
4. **可用性问题**:资源多时难以找到目标,操作效率低
## 核心优化
### 1. 智能级联选择 ✨
**选择文件夹 → 自动选择其下所有项目及资源**
```
选择"西游记系列"文件夹
↓ 自动选择
《西游记》第一季 + 其下所有角色/场景/道具
《西游记》第二季 + 其下所有角色/场景/道具
```
**选择项目 → 自动选择其下所有资源**
```
选择《西游记》第一季
↓ 自动选择
孙悟空、猪八戒、唐僧、沙和尚(角色)
花果山、高老庄、流沙河(场景)
金箍棒、九齿钉耙(道具)
```
**取消父级 → 自动取消所有子级**
```
取消选择《西游记》第一季
↓ 自动取消
其下所有角色/场景/道具全部取消选中
```
**子级全选 → 父级自动全选;部分选 → 父级半选**
```
[ ] 《西游记》第一季 ← 未选
[◐] 《西游记》第一季 ← 半选(部分子级选中)
[✓] 《西游记》第一季 ← 全选(所有子级选中)
```
**实现算法**:
```typescript
// 向下级联:选择父级 → 选择所有子孙
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` 支持**
```typescript
// Checkbox 组件支持三态
// 渲染不同图标
checked: ✓ (Check)
indeterminate: - (Minus)
unchecked: 空
```
### 6. 增强的统计信息 📊
**底部统计栏优化**
**变更前**:
```
已选 5 项资源
```
**变更后**:
```
已选 5 项资源
角色 3 场景 1 道具 1
```
**统计维度**:
- 总数
- 按类型分类(角色/场景/道具)
- 实时更新
### 7. 宽度调整
**面板宽度**:320px → 380px
- 搜索框更宽,输入体验更好
- 统计信息显示更完整
- 按钮组不拥挤
**弹窗自适应宽度**:
- 风格面板:600px + 320px = 920px
- 资源面板:600px + 380px = 980px
## 技术实现
### 核心算法
#### 1. 扁平化树结构
```typescript
function flattenTree(nodes: ResourceTreeNode[]): Map {
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. 获取所有子孙节点
```typescript
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. 搜索匹配
```typescript
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. 关键词高亮
```typescript
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}
{match}
{after}
>
);
}, [node.name, searchQuery]);
```
### 性能优化
1. **使用 useMemo 缓存计算**
```typescript
const nodeMap = useMemo(() => flattenTree(mockResourceTree), []);
const selectedIds = useMemo(() => new Set(selectedResources.map(r => r.id)), [selectedResources]);
const filteredTree = useMemo(() => searchNodes(...), [searchQuery, typeFilter]);
```
2. **使用 useCallback 稳定回调**
```typescript
const handleToggleSelect = useCallback((nodeId, checked) => { ... }, [nodeMap, selectedResources, onSelect]);
const handleExpandAll = useCallback(() => { ... }, [nodeMap]);
```
3. **使用 memo 避免不必要的重渲染**
```typescript
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+ 层)渲染
- [ ] 搜索无结果场景
- [ ] 空文件夹场景
### 视觉测试
- [ ] 缩进层级清晰
- [ ] 半选图标显示正确
- [ ] 关键词高亮可见
- [ ] 统计信息对齐
## 相关文档
- [ADR 02: 跨项目资源共享](../../server/adrs/02-cross-project-resource-sharing.md)
- [新建项目流程优化 Changelog](./2026-02-08-create-project-optimization.md)
## 变更记录
- 2026-02-08:完整优化版本 - 智能级联、搜索、过滤、快捷操作