feat: 添加项目详情/素材管理的MaterialSegment聚合视图功能

- 新增MaterialSegment聚合视图,支持按AI分类和模特聚合展示
- 实现后端MaterialSegmentViewService和相关API命令
- 创建前端React组件:MaterialSegmentView、MaterialSegmentGroup、MaterialSegmentCard等
- 添加MaterialSegment详细信息模态框和批量操作对话框
- 实现搜索、筛选、排序、分页功能
- 集成虚拟滚动和性能优化
- 在ProjectDetails页面添加片段管理选项卡
- 遵循promptx开发规范和UI/UX设计标准
This commit is contained in:
imeepos
2025-07-15 16:49:08 +08:00
parent bab1dfc5fd
commit 590e254fe1
24 changed files with 4340 additions and 3 deletions

View File

@@ -0,0 +1,128 @@
import React, { useMemo, useCallback } from 'react';
import { VariableSizeList as List } from 'react-window';
import {
ClassificationGroup,
ModelGroup,
MaterialSegmentViewMode
} from '../types/materialSegmentView';
import { MaterialSegmentGroup } from './MaterialSegmentGroup';
interface VirtualizedSegmentListProps {
groups: (ClassificationGroup | ModelGroup)[];
viewMode: MaterialSegmentViewMode;
selectedSegmentIds: Set<string>;
expandedGroups: Set<string>;
onToggleExpand: (groupId: string) => void;
onSelectSegment: (segmentId: string) => void;
height: number;
}
/**
* 虚拟化MaterialSegment列表组件
* 遵循 Tauri 开发规范的性能优化组件设计模式
*/
export const VirtualizedSegmentList: React.FC<VirtualizedSegmentListProps> = ({
groups,
viewMode,
selectedSegmentIds,
expandedGroups,
onToggleExpand,
onSelectSegment,
height,
}) => {
// 计算每个分组的高度
const getItemHeight = useCallback((index: number) => {
const group = groups[index];
if (!group) return 120; // 默认折叠高度
const groupId = viewMode === MaterialSegmentViewMode.ByClassification
? (group as ClassificationGroup).category
: (group as ModelGroup).model_id;
const isExpanded = expandedGroups.has(groupId);
if (!isExpanded) {
return 120; // 折叠状态的高度
}
// 展开状态的高度计算
const baseHeight = 120; // 头部高度
const segmentCount = group.segments.length;
const segmentsPerRow = 4; // 每行显示的片段数
const rowCount = Math.ceil(segmentCount / segmentsPerRow);
const segmentRowHeight = 280; // 每行片段的高度
const footerHeight = 60; // 底部统计信息高度
return baseHeight + (rowCount * segmentRowHeight) + footerHeight;
}, [groups, viewMode, expandedGroups]);
// 计算总高度
const totalHeight = useMemo(() => {
return groups.reduce((total, _, index) => total + getItemHeight(index), 0);
}, [groups, getItemHeight]);
// 渲染单个分组项
const renderItem = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
const group = groups[index];
if (!group) return null;
return (
<div style={style}>
<div className="px-6 pb-4">
<MaterialSegmentGroup
key={viewMode === MaterialSegmentViewMode.ByClassification
? (group as ClassificationGroup).category
: (group as ModelGroup).model_id}
group={group}
viewMode={viewMode}
isExpanded={expandedGroups.has(
viewMode === MaterialSegmentViewMode.ByClassification
? (group as ClassificationGroup).category
: (group as ModelGroup).model_id
)}
selectedSegmentIds={selectedSegmentIds}
onToggleExpand={onToggleExpand}
onSelectSegment={onSelectSegment}
/>
</div>
</div>
);
}, [groups, viewMode, expandedGroups, selectedSegmentIds, onToggleExpand, onSelectSegment]);
// 如果分组数量较少,不使用虚拟化
if (groups.length <= 10) {
return (
<div className="space-y-4 px-6">
{groups.map((group, _index) => (
<MaterialSegmentGroup
key={viewMode === MaterialSegmentViewMode.ByClassification
? (group as ClassificationGroup).category
: (group as ModelGroup).model_id}
group={group}
viewMode={viewMode}
isExpanded={expandedGroups.has(
viewMode === MaterialSegmentViewMode.ByClassification
? (group as ClassificationGroup).category
: (group as ModelGroup).model_id
)}
selectedSegmentIds={selectedSegmentIds}
onToggleExpand={onToggleExpand}
onSelectSegment={onSelectSegment}
/>
))}
</div>
);
}
return (
<List
height={Math.min(height, totalHeight)}
itemCount={groups.length}
itemSize={(index) => getItemHeight(index)}
width="100%"
className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
>
{renderItem}
</List>
);
};