fix
This commit is contained in:
parent
231b28d0eb
commit
0b322d06fb
|
|
@ -0,0 +1,200 @@
|
|||
# 素材分类筛选功能测试指南
|
||||
|
||||
## 🧪 测试准备
|
||||
|
||||
### 1. 确保分类数据存在
|
||||
在测试前,确保系统中已有分类数据:
|
||||
- 进入"资源分类管理"页面
|
||||
- 创建几个测试分类(如:商业、教育、科技、娱乐)
|
||||
- 为每个分类设置不同的颜色和AI提示
|
||||
|
||||
### 2. 准备测试素材
|
||||
上传一些测试素材,文件名和标签包含分类关键词:
|
||||
|
||||
**商业类素材**:
|
||||
- 文件名:`商业宣传片.mp4`, `business_intro.mp4`
|
||||
- 标签:["商业", "企业", "宣传"]
|
||||
|
||||
**教育类素材**:
|
||||
- 文件名:`教育培训视频.mp4`, `education_demo.mp4`
|
||||
- 标签:["教育", "培训", "学习"]
|
||||
|
||||
**科技类素材**:
|
||||
- 文件名:`科技产品演示.mp4`, `tech_showcase.mp4`
|
||||
- 标签:["科技", "技术", "创新"]
|
||||
|
||||
## 🔍 测试用例
|
||||
|
||||
### 测试用例1: 基础分类筛选
|
||||
|
||||
**步骤**:
|
||||
1. 进入任意项目详情页面
|
||||
2. 查看素材管理区域的分类筛选器
|
||||
3. 验证"全部"按钮显示总素材数量
|
||||
4. 点击"商业"分类按钮
|
||||
5. 验证只显示商业类素材
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 分类按钮正确显示
|
||||
- ✅ 数量统计准确
|
||||
- ✅ 筛选结果正确
|
||||
- ✅ 按钮状态正确切换
|
||||
|
||||
### 测试用例2: 分类数量统计
|
||||
|
||||
**步骤**:
|
||||
1. 查看每个分类按钮上的数量显示
|
||||
2. 手动计算符合条件的素材数量
|
||||
3. 对比系统显示的数量
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 数量统计准确
|
||||
- ✅ 总数等于各分类数量之和(考虑重复分类)
|
||||
|
||||
### 测试用例3: 组合筛选
|
||||
|
||||
**步骤**:
|
||||
1. 选择"商业"分类
|
||||
2. 再选择"已使用"状态
|
||||
3. 验证结果同时满足两个条件
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 多重筛选正确工作
|
||||
- ✅ 筛选条件叠加生效
|
||||
|
||||
### 测试用例4: 分类颜色显示
|
||||
|
||||
**步骤**:
|
||||
1. 查看各分类按钮的颜色
|
||||
2. 对比分类管理中设置的颜色
|
||||
3. 验证选中状态的颜色变化
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 分类颜色正确显示
|
||||
- ✅ 选中状态高亮显示
|
||||
|
||||
### 测试用例5: 空状态处理
|
||||
|
||||
**步骤**:
|
||||
1. 选择一个没有匹配素材的分类
|
||||
2. 验证空状态显示
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 显示"没有找到匹配的素材"
|
||||
- ✅ 提示调整筛选条件
|
||||
|
||||
### 测试用例6: 分类匹配规则
|
||||
|
||||
**步骤**:
|
||||
1. 创建包含特定标签的素材
|
||||
2. 验证标签匹配是否正确
|
||||
3. 测试文件名匹配
|
||||
4. 测试AI提示关键词匹配
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 标签匹配正确
|
||||
- ✅ 文件名匹配正确
|
||||
- ✅ AI提示匹配正确
|
||||
|
||||
## 📊 性能测试
|
||||
|
||||
### 大量分类测试
|
||||
1. 创建20+个分类
|
||||
2. 验证界面显示是否正常
|
||||
3. 测试筛选性能
|
||||
|
||||
### 大量素材测试
|
||||
1. 上传100+个素材
|
||||
2. 验证分类统计性能
|
||||
3. 测试筛选响应速度
|
||||
|
||||
## 🐛 边界情况测试
|
||||
|
||||
### 特殊字符处理
|
||||
- 测试包含特殊字符的分类名称
|
||||
- 测试包含特殊字符的文件名和标签
|
||||
|
||||
### 空数据处理
|
||||
- 测试没有分类数据的情况
|
||||
- 测试没有素材的情况
|
||||
- 测试分类加载失败的情况
|
||||
|
||||
### 网络异常处理
|
||||
- 测试网络断开时的行为
|
||||
- 测试分类服务异常时的处理
|
||||
|
||||
## 📝 测试记录模板
|
||||
|
||||
```
|
||||
测试日期: ____
|
||||
测试人员: ____
|
||||
浏览器: ____
|
||||
版本: ____
|
||||
|
||||
测试用例1: 基础分类筛选
|
||||
- 分类按钮显示: ✅/❌
|
||||
- 数量统计: ✅/❌
|
||||
- 筛选结果: ✅/❌
|
||||
- 状态切换: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
测试用例2: 分类数量统计
|
||||
- 数量准确性: ✅/❌
|
||||
- 总数计算: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
测试用例3: 组合筛选
|
||||
- 多重筛选: ✅/❌
|
||||
- 条件叠加: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
测试用例4: 分类颜色显示
|
||||
- 颜色正确: ✅/❌
|
||||
- 选中高亮: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
测试用例5: 空状态处理
|
||||
- 空状态显示: ✅/❌
|
||||
- 提示信息: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
测试用例6: 分类匹配规则
|
||||
- 标签匹配: ✅/❌
|
||||
- 文件名匹配: ✅/❌
|
||||
- AI提示匹配: ✅/❌
|
||||
- 备注: ____
|
||||
|
||||
总体评价: ____
|
||||
发现问题: ____
|
||||
改进建议: ____
|
||||
```
|
||||
|
||||
## 🔧 常见问题排查
|
||||
|
||||
### 问题1: 分类不显示
|
||||
**排查步骤**:
|
||||
1. 检查浏览器控制台错误
|
||||
2. 验证分类服务API是否正常
|
||||
3. 检查分类数据是否存在且is_active=true
|
||||
|
||||
### 问题2: 数量统计错误
|
||||
**排查步骤**:
|
||||
1. 检查匹配规则逻辑
|
||||
2. 验证素材标签和文件名数据
|
||||
3. 测试AI提示关键词设置
|
||||
|
||||
### 问题3: 筛选不生效
|
||||
**排查步骤**:
|
||||
1. 检查筛选逻辑实现
|
||||
2. 验证状态管理是否正确
|
||||
3. 测试组合筛选的条件叠加
|
||||
|
||||
### 问题4: 界面显示异常
|
||||
**排查步骤**:
|
||||
1. 检查CSS样式是否正确
|
||||
2. 验证响应式布局
|
||||
3. 测试不同浏览器兼容性
|
||||
|
||||
---
|
||||
|
||||
通过系统的测试,确保素材分类筛选功能稳定可靠,为用户提供良好的使用体验!
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
/**
|
||||
* 素材导入服务
|
||||
* 处理素材导入、模特选择和自动分镜功能
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { VideoSegment } from './mediaService'
|
||||
import { Model } from './modelService'
|
||||
|
||||
export interface ImportMaterialConfig {
|
||||
// 基础配置
|
||||
projectId: string
|
||||
sourceDirectory: string
|
||||
selectedModelIds: string[]
|
||||
|
||||
// 导入选项
|
||||
includeSubdirectories: boolean
|
||||
fileTypes: string[] // 支持的文件类型
|
||||
|
||||
// 自动分镜配置
|
||||
enableAutoSegmentation: boolean
|
||||
segmentationConfig: {
|
||||
method: 'scene' | 'time' | 'smart' // 分镜方法
|
||||
sceneThreshold: number // 场景变化阈值 (0-1)
|
||||
timeInterval: number // 时间间隔分镜(秒)
|
||||
minSegmentDuration: number // 最小片段时长(秒)
|
||||
maxSegmentDuration: number // 最大片段时长(秒)
|
||||
}
|
||||
|
||||
// 处理选项
|
||||
generateThumbnails: boolean
|
||||
extractMetadata: boolean
|
||||
autoTagging: boolean
|
||||
}
|
||||
|
||||
export interface ImportProgress {
|
||||
stage: 'scanning' | 'importing' | 'segmenting' | 'processing' | 'completed' | 'error'
|
||||
progress: number // 0-100
|
||||
message: string
|
||||
currentFile?: string
|
||||
processedFiles: number
|
||||
totalFiles: number
|
||||
segmentedVideos: number
|
||||
generatedSegments: number
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
success: boolean
|
||||
message: string
|
||||
importedMaterials: VideoSegment[]
|
||||
generatedSegments: VideoSegment[]
|
||||
skippedFiles: string[]
|
||||
errors: string[]
|
||||
statistics: {
|
||||
totalFiles: number
|
||||
importedFiles: number
|
||||
segmentedVideos: number
|
||||
totalSegments: number
|
||||
processingTime: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
path: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
duration?: number
|
||||
resolution?: string
|
||||
isSupported: boolean
|
||||
}
|
||||
|
||||
export class MaterialImportService {
|
||||
/**
|
||||
* 获取默认导入配置
|
||||
*/
|
||||
static getDefaultConfig(projectId: string): ImportMaterialConfig {
|
||||
return {
|
||||
projectId,
|
||||
sourceDirectory: '',
|
||||
selectedModelIds: [],
|
||||
includeSubdirectories: true,
|
||||
fileTypes: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'm4v'],
|
||||
enableAutoSegmentation: true,
|
||||
segmentationConfig: {
|
||||
method: 'smart',
|
||||
sceneThreshold: 0.3,
|
||||
timeInterval: 30,
|
||||
minSegmentDuration: 3,
|
||||
maxSegmentDuration: 60
|
||||
},
|
||||
generateThumbnails: true,
|
||||
extractMetadata: true,
|
||||
autoTagging: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描目录获取可导入的文件
|
||||
*/
|
||||
static async scanDirectory(directoryPath: string, config: ImportMaterialConfig): Promise<FileInfo[]> {
|
||||
try {
|
||||
const result = await invoke('scan_import_directory', {
|
||||
directoryPath,
|
||||
includeSubdirectories: config.includeSubdirectories,
|
||||
fileTypes: config.fileTypes
|
||||
})
|
||||
|
||||
return result as FileInfo[]
|
||||
} catch (error) {
|
||||
console.error('Failed to scan directory:', error)
|
||||
throw new Error(`扫描目录失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始导入素材
|
||||
*/
|
||||
static async startImport(config: ImportMaterialConfig): Promise<{ taskId: string }> {
|
||||
try {
|
||||
const result = await invoke('start_material_import', { config })
|
||||
return result as { taskId: string }
|
||||
} catch (error) {
|
||||
console.error('Failed to start import:', error)
|
||||
throw new Error(`开始导入失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入进度
|
||||
*/
|
||||
static async getImportProgress(taskId: string): Promise<ImportProgress> {
|
||||
try {
|
||||
const result = await invoke('get_import_progress', { taskId })
|
||||
return result as ImportProgress
|
||||
} catch (error) {
|
||||
console.error('Failed to get import progress:', error)
|
||||
throw new Error(`获取导入进度失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消导入任务
|
||||
*/
|
||||
static async cancelImport(taskId: string): Promise<boolean> {
|
||||
try {
|
||||
await invoke('cancel_import', { taskId })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel import:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入结果
|
||||
*/
|
||||
static async getImportResult(taskId: string): Promise<ImportResult> {
|
||||
try {
|
||||
const result = await invoke('get_import_result', { taskId })
|
||||
return result as ImportResult
|
||||
} catch (error) {
|
||||
console.error('Failed to get import result:', error)
|
||||
throw new Error(`获取导入结果失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证导入配置
|
||||
*/
|
||||
static validateConfig(config: ImportMaterialConfig): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!config.projectId) {
|
||||
errors.push('项目ID不能为空')
|
||||
}
|
||||
|
||||
if (!config.sourceDirectory) {
|
||||
errors.push('请选择素材目录')
|
||||
}
|
||||
|
||||
if (config.fileTypes.length === 0) {
|
||||
errors.push('请至少选择一种文件类型')
|
||||
}
|
||||
|
||||
if (config.enableAutoSegmentation) {
|
||||
const segConfig = config.segmentationConfig
|
||||
|
||||
if (segConfig.sceneThreshold < 0 || segConfig.sceneThreshold > 1) {
|
||||
errors.push('场景变化阈值必须在0-1之间')
|
||||
}
|
||||
|
||||
if (segConfig.timeInterval <= 0) {
|
||||
errors.push('时间间隔必须大于0')
|
||||
}
|
||||
|
||||
if (segConfig.minSegmentDuration <= 0) {
|
||||
errors.push('最小片段时长必须大于0')
|
||||
}
|
||||
|
||||
if (segConfig.maxSegmentDuration <= segConfig.minSegmentDuration) {
|
||||
errors.push('最大片段时长必须大于最小片段时长')
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算导入时间
|
||||
*/
|
||||
static estimateImportTime(fileCount: number, config: ImportMaterialConfig): number {
|
||||
// 基础导入时间(每个文件)
|
||||
let timePerFile = 5 // 秒
|
||||
|
||||
// 自动分镜增加时间
|
||||
if (config.enableAutoSegmentation) {
|
||||
timePerFile += 15
|
||||
}
|
||||
|
||||
// 生成缩略图增加时间
|
||||
if (config.generateThumbnails) {
|
||||
timePerFile += 3
|
||||
}
|
||||
|
||||
// 元数据提取增加时间
|
||||
if (config.extractMetadata) {
|
||||
timePerFile += 2
|
||||
}
|
||||
|
||||
// 自动标签增加时间
|
||||
if (config.autoTagging) {
|
||||
timePerFile += 5
|
||||
}
|
||||
|
||||
return Math.round(fileCount * timePerFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的文件类型
|
||||
*/
|
||||
static getSupportedFileTypes(): Array<{value: string, label: string, description: string}> {
|
||||
return [
|
||||
{ value: 'mp4', label: 'MP4', description: '最常用的视频格式' },
|
||||
{ value: 'avi', label: 'AVI', description: '经典视频格式' },
|
||||
{ value: 'mov', label: 'MOV', description: 'QuickTime视频格式' },
|
||||
{ value: 'mkv', label: 'MKV', description: '高质量视频格式' },
|
||||
{ value: 'wmv', label: 'WMV', description: 'Windows媒体格式' },
|
||||
{ value: 'flv', label: 'FLV', description: 'Flash视频格式' },
|
||||
{ value: 'm4v', label: 'M4V', description: 'iTunes视频格式' },
|
||||
{ value: 'webm', label: 'WebM', description: 'Web视频格式' },
|
||||
{ value: '3gp', label: '3GP', description: '移动设备格式' }
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分镜方法选项
|
||||
*/
|
||||
static getSegmentationMethods(): Array<{value: string, label: string, description: string}> {
|
||||
return [
|
||||
{
|
||||
value: 'smart',
|
||||
label: '智能分镜',
|
||||
description: '结合场景变化和时间间隔的智能分镜'
|
||||
},
|
||||
{
|
||||
value: 'scene',
|
||||
label: '场景分镜',
|
||||
description: '根据场景变化自动分镜'
|
||||
},
|
||||
{
|
||||
value: 'time',
|
||||
label: '时间分镜',
|
||||
description: '按固定时间间隔分镜'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览分镜结果
|
||||
*/
|
||||
static async previewSegmentation(
|
||||
filePath: string,
|
||||
config: ImportMaterialConfig['segmentationConfig']
|
||||
): Promise<Array<{start: number, end: number, thumbnail?: string}>> {
|
||||
try {
|
||||
const result = await invoke('preview_segmentation', { filePath, config })
|
||||
return result as Array<{start: number, end: number, thumbnail?: string}>
|
||||
} catch (error) {
|
||||
console.error('Failed to preview segmentation:', error)
|
||||
throw new Error(`预览分镜失败: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue