Appearance
拖拽导入增强功能
概述
拖拽导入增强功能(Enhanced Drag and Drop)是 Eagle2Ae AE 扩展 v2.4.0 引入的重要功能模块,它显著增强了传统的拖拽导入体验,新增了文件夹拖拽支持、序列帧自动检测、项目内文件检测等多项功能。这一功能模块使用户能够更高效、更智能地将素材从Eagle或其他来源导入到After Effects中。
核心特性
文件夹拖拽支持
- 支持直接拖拽整个文件夹到扩展面板
- 保持文件夹结构,便于组织素材
- 提供实时状态反馈和进度显示
序列帧自动检测
- 自动识别图片序列(如 frame001.png, frame002.png, ...)
- 将序列帧作为单个序列导入到AE中
- 支持多种命名模式和数字格式
项目内文件检测
- 检测拖拽的文件是否已在当前AE项目中
- 防止重复导入,提高工作效率
- 提供清晰的警告提示和选择选项
增强的视觉反馈
- 拖拽过程中的实时状态提示
- 智能文件类型识别和图标显示
- 详细的文件信息预览
技术实现
核心类结构
javascript
/**
* 拖拽导入增强功能
* 负责处理文件夹拖拽、序列帧检测、项目内文件检测等增强功能
*/
class EnhancedDragAndDrop {
/**
* 构造函数
* @param {Object} aeExtension - AE扩展实例
*/
constructor(aeExtension) {
this.aeExtension = aeExtension;
this.csInterface = aeExtension.csInterface;
this.settingsManager = aeExtension.settingsManager;
this.logManager = aeExtension.logManager;
this.projectStatusChecker = aeExtension.projectStatusChecker;
// 初始化状态
this.isDragging = false;
this.draggedFiles = [];
this.dragPreviewState = {
hasProjectFiles: false,
projectFiles: [],
externalFiles: [],
totalFiles: 0,
sequenceCount: 0
};
// 绑定方法上下文
this.setupDragAndDrop = this.setupDragAndDrop.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleDrop = this.handleDrop.bind(this);
this.handleFileDrop = this.handleFileDrop.bind(this);
this.showDropMessage = this.showDropMessage.bind(this);
this.log = this.log.bind(this);
this.log('📁 拖拽导入增强功能已初始化', 'debug');
}
}拖拽监听实现
javascript
/**
* 设置拖拽监听
*/
setupDragAndDrop() {
try {
// 防止默认拖拽行为并进行预检查
document.addEventListener('dragover', async (e) => {
e.preventDefault();
e.stopPropagation();
// 添加基础视觉反馈
document.body.classList.add('drag-over');
// 进行项目内文件预检查
await this.handleDragPreview(e);
});
document.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragleave', (e) => {
// 只有当拖拽完全离开窗口时才移除样式
if (e.clientX === 0 && e.clientY === 0) {
document.body.classList.remove('drag-over');
// 重置拖拽提示状态
this.resetDragPreviewState();
}
});
// 处理文件拖拽
document.addEventListener('drop', this.handleFileDrop.bind(this));
this.log('📁 拖拽事件监听器已设置', 'debug');
} catch (error) {
this.log(`设置拖拽监听失败: ${error.message}`, 'error');
}
}拖拽预览实现
javascript
/**
* 拖拽预检查处理
* @param {DragEvent} event - 拖拽事件
*/
async handleDragPreview(event) {
try {
// 防抖处理,避免频繁检查
if (this.dragPreviewTimeout) {
clearTimeout(this.dragPreviewTimeout);
}
this.dragPreviewTimeout = setTimeout(async () => {
await this.performDragPreviewCheck(event);
}, 200); // 200ms 防抖延迟
} catch (error) {
this.log(`拖拽预检查失败: ${error.message}`, 'error');
}
}
/**
* 执行拖拽预检查
* @param {DragEvent} event - 拖拽事件
*/
async performDragPreviewCheck(event) {
try {
this.log('🔍 开始拖拽预检查...', 'debug');
// 尝试获取拖拽的文件信息
let files = Array.from(event.dataTransfer.files);
this.log(`📁 从dataTransfer.files获取到 ${files.length} 个文件`, 'debug');
// 检查文件路径信息
if (files.length > 0) {
files.forEach((file, index) => {
this.log(`📄 文件 ${index + 1}: ${file.name}, 路径: ${file.path || '无路径信息'}`, 'debug');
});
}
// 如果files为空,尝试从items获取文件信息
if (files.length === 0 && event.dataTransfer.items) {
const items = Array.from(event.dataTransfer.items);
const fileItems = items.filter(item => item.kind === 'file');
if (fileItems.length > 0) {
// 显示检测中的提示
this.updateDragHint(`🔍 正在检测 ${fileItems.length} 个文件...`, 'default');
// 尝试获取文件对象(这在某些浏览器中可能不可用)
try {
files = fileItems.map(item => item.getAsFile()).filter(file => file !== null);
} catch (e) {
// 如果无法获取文件对象,显示通用提示
this.updateDragHint(`📁 准备检测拖拽的文件`, 'default');
return;
}
}
}
if (files.length === 0) {
// 如果仍然无法获取文件信息,显示默认提示
this.updateDragHint('拖拽文件到此处');
return;
}
// 检查是否为项目内文件
const projectFileCheck = await this.isProjectInternalFile(files);
if (projectFileCheck.hasProjectFiles) {
// 项目内文件,显示警告提示
const projectFileCount = projectFileCheck.projectFiles.length;
const externalFileCount = projectFileCheck.externalFiles.length;
let hintText = `⚠️ 检测到 ${projectFileCount} 个项目内文件`;
if (externalFileCount > 0) {
hintText += `,${externalFileCount} 个外部文件`;
}
hintText += '\n项目内文件无法导入';
this.updateDragHint(hintText, 'warning');
document.body.classList.add('drag-project-files');
} else {
// 外部文件,显示正常提示
const fileCount = files.length;
this.updateDragHint(`✅ 准备导入 ${fileCount} 个文件`, 'success');
document.body.classList.remove('drag-project-files');
document.body.classList.add('drag-success');
}
} catch (error) {
this.log(`拖拽预检查执行失败: ${error.message}`, 'error');
this.updateDragHint('拖拽文件到此处');
}
}拖拽处理实现
javascript
/**
* 处理文件拖拽
* @param {DragEvent} event - 拖拽事件
*/
async handleFileDrop(event) {
event.preventDefault();
event.stopPropagation();
// 移除视觉反馈
document.body.classList.remove('drag-over');
try {
const files = Array.from(event.dataTransfer.files);
const items = Array.from(event.dataTransfer.items);
if (files.length === 0 && items.length === 0) {
this.log('❌ 拖拽中没有检测到文件', 'warning');
this.showDropMessage('未检测到文件', 'warning');
return;
}
this.log(`📁 检测到拖拽内容: ${files.length} 个文件, ${items.length} 个项目`, 'info');
// 检查是否包含文件夹
const hasDirectories = items.some(item => item.webkitGetAsEntry && item.webkitGetAsEntry()?.isDirectory);
if (hasDirectories) {
// 处理文件夹拖拽(可能包含序列帧)
await this.handleDirectoryDrop(items, files);
} else {
// 处理普通文件拖拽
await this.handleFilesDrop(files, event.dataTransfer);
}
} catch (error) {
this.log(`❌ 处理拖拽失败: ${error.message}`, 'error');
this.showDropMessage('拖拽处理失败', 'error');
}
}
/**
* 处理文件夹拖拽
* @param {Array} items - DataTransferItem数组
* @param {Array} files - File数组
*/
async handleDirectoryDrop(items, files) {
this.log('📁 检测到文件夹拖拽,开始处理...', 'info');
const allFiles = [];
// 递归读取文件夹内容
for (const item of items) {
const entry = item.webkitGetAsEntry();
if (entry) {
const entryFiles = await this.readDirectoryEntry(entry);
allFiles.push(...entryFiles);
}
}
// 添加直接拖拽的文件
allFiles.push(...files);
if (allFiles.length === 0) {
this.showDropMessage('❌ 文件夹中没有找到可导入的文件', 'warning');
return;
}
this.log(`📁 文件夹中找到 ${allFiles.length} 个文件`, 'info');
// 首先检查是否包含项目文件
const projectFileCheck = await this.isProjectInternalFile(allFiles);
if (projectFileCheck.hasProjectFiles) {
// 显示项目内文件警告
this.showProjectInternalFileWarning(projectFileCheck);
return;
}
// 分析文件类型和序列帧
const analysis = this.analyzeDroppedFiles(allFiles);
// 显示导入选项对话框(跳过项目文件检查,因为上面已经检查过了)
await this.showFileImportDialog(allFiles, analysis, true);
}
/**
* 递归读取文件夹内容
* @param {FileSystemEntry} entry - 文件系统条目
* @returns {Promise<Array>} 文件数组
*/
async readDirectoryEntry(entry) {
const files = [];
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
// 添加路径信息
file.fullPath = entry.fullPath;
file.relativePath = entry.fullPath;
resolve([file]);
}, () => resolve([]));
});
} else if (entry.isDirectory) {
const reader = entry.createReader();
// 修复:循环读取所有文件,因为readEntries可能不会一次性返回所有文件
const allEntries = [];
let entries;
do {
entries = await new Promise((resolve) => {
reader.readEntries(resolve, () => resolve([]));
});
allEntries.push(...entries);
this.log(`📁 读取目录 ${entry.fullPath}: 本次获取 ${entries.length} 个条目,累计 ${allEntries.length} 个`, 'debug');
} while (entries.length > 0);
this.log(`📁 目录 ${entry.fullPath} 总共包含 ${allEntries.length} 个条目`, 'debug');
for (const childEntry of allEntries) {
const childFiles = await this.readDirectoryEntry(childEntry);
files.push(...childFiles);
}
}
return files;
}文件分析实现
javascript
/**
* 分析拖拽的文件
* @param {Array} files - 文件数组
* @returns {Object} 分析结果
*/
analyzeDroppedFiles(files) {
this.log(`🔍 开始分析拖拽文件,总数: ${files.length}`, 'info');
const analysis = {
total: files.length,
categories: {
image: [],
video: [],
audio: [],
design: [],
project: [],
unknown: []
},
sequences: [],
folders: new Set()
};
// 按文件夹分组
const folderGroups = {};
const folderFullPaths = {}; // 存储文件夹的完整路径映射
files.forEach(file => {
const category = this.getFileCategory(file);
analysis.categories[category].push(file);
// 提取文件夹路径
const path = file.fullPath || file.relativePath || file.webkitRelativePath || '';
const folderPath = path.substring(0, path.lastIndexOf('/'));
if (folderPath) {
analysis.folders.add(folderPath);
if (!folderGroups[folderPath]) {
folderGroups[folderPath] = [];
}
folderGroups[folderPath].push(file);
// 尝试获取完整的文件夹路径
if (file.originalFile && file.originalFile.path) {
// 从完整文件路径中提取文件夹路径
const fullFilePath = file.originalFile.path;
const fullFolderPath = fullFilePath.substring(0, fullFilePath.lastIndexOf('\\'));
if (fullFolderPath && !folderFullPaths[folderPath]) {
folderFullPaths[folderPath] = fullFolderPath;
}
} else if (file.path && file.path.includes('\\')) {
// 直接从文件路径提取
const fullFolderPath = file.path.substring(0, file.path.lastIndexOf('\\'));
if (fullFolderPath && !folderFullPaths[folderPath]) {
folderFullPaths[folderPath] = fullFolderPath;
}
}
}
});
this.log(`📊 文件分类统计: 图像${analysis.categories.image.length}, 视频${analysis.categories.video.length}, 音频${analysis.categories.audio.length}, 设计${analysis.categories.design.length}, 项目${analysis.categories.project.length}, 未知${analysis.categories.unknown.length}`, 'info');
this.log(`📁 检测到 ${analysis.folders.size} 个文件夹`, 'info');
// 检测序列帧
let totalSequenceFiles = 0;
for (const [folderPath, folderFiles] of Object.entries(folderGroups)) {
this.log(`🔍 检查文件夹: ${folderPath}, 文件数: ${folderFiles.length}`, 'debug');
const sequence = this.detectImageSequence(folderFiles);
if (sequence) {
// 使用完整路径或回退到相对路径
const fullFolderPath = folderFullPaths[folderPath] || folderPath;
analysis.sequences.push({
folder: fullFolderPath, // 使用完整路径
files: sequence.files,
pattern: sequence.pattern,
start: sequence.start,
end: sequence.end,
step: sequence.step,
totalFiles: sequence.totalFiles
});
totalSequenceFiles += sequence.totalFiles;
this.log(`✅ 文件夹 ${folderPath} 识别为序列帧: ${sequence.pattern}`, 'info');
} else {
this.log(`❌ 文件夹 ${folderPath} 未识别为序列帧`, 'debug');
}
}
this.log(`🎉 序列帧检测完成: 发现 ${analysis.sequences.length} 个序列帧文件夹,共 ${totalSequenceFiles} 个序列帧文件`, 'info');
return analysis;
}
/**
* 检测图片序列
* @param {Array} files - 文件数组
* @returns {Object|null} 序列帧信息或null
*/
detectImageSequence(files) {
// 只检测图片文件
const imageFiles = files.filter(file => this.getFileCategory(file) === 'image');
this.log(`🔍 检测序列帧: 图像文件数 ${imageFiles.length}`, 'debug');
if (imageFiles.length < 2) return null; // 至少需要2个文件才算序列帧
// 按文件名排序
imageFiles.sort((a, b) => a.name.localeCompare(b.name));
// 尝试找到数字模式
const patterns = [];
for (const file of imageFiles) {
const name = file.name;
const nameWithoutExt = name.substring(0, name.lastIndexOf('.'));
// 查找数字模式 - 支持多种数字格式,优先匹配最后一个数字序列
const numberMatches = nameWithoutExt.match(/(.*?)(\d+)([^\d]*)$/);
if (numberMatches) {
const [, prefix, number, suffix] = numberMatches;
patterns.push({
prefix: prefix || '',
number: parseInt(number),
suffix: suffix || '',
numberLength: number.length,
originalNumber: number,
file
});
this.log(`🔍 文件 ${file.name} 匹配模式: 前缀="${prefix}", 数字=${number}, 后缀="${suffix}"`, 'debug');
} else {
this.log(`🔍 文件 ${file.name} 未匹配数字模式`, 'debug');
}
}
this.log(`🔍 找到 ${patterns.length} 个符合数字模式的文件`, 'debug');
if (patterns.length < 2) {
this.log('🔍 数字模式文件数量不足,不构成序列', 'debug');
return null;
}
// 找到最一致的模式
const patternGroups = {};
patterns.forEach(p => {
const key = `${p.prefix}_${p.suffix}_${p.numberLength}`;
if (!patternGroups[key]) {
patternGroups[key] = [];
}
patternGroups[key].push(p);
});
this.log(`🔍 找到 ${Object.keys(patternGroups).length} 个不同的模式组`, 'debug');
// 找到最大的组
let bestGroup = null;
let maxSize = 0;
for (const [key, group] of Object.entries(patternGroups)) {
this.log(`🔍 模式组 ${key}: ${group.length} 个文件`, 'debug');
if (group.length > maxSize) {
maxSize = group.length;
bestGroup = group;
}
}
// 对于大量文件,降低要求;对于少量文件,保持较高要求
const minGroupSize = imageFiles.length >= 10 ? Math.max(2, Math.floor(imageFiles.length * 0.8)) : 2;
if (!bestGroup || bestGroup.length < minGroupSize) {
this.log(`🔍 没有找到足够大的模式组,需要至少${minGroupSize}个文件,实际最大组${bestGroup ? bestGroup.length : 0}个`, 'debug');
return null;
}
this.log(`🔍 选择最佳模式组,包含 ${bestGroup.length} 个文件`, 'debug');
// 排序并检查连续性
bestGroup.sort((a, b) => a.number - b.number);
const numbers = bestGroup.map(p => p.number);
const start = numbers[0];
const end = numbers[numbers.length - 1];
// 检测步长
let step = 1;
if (numbers.length > 1) {
const diffs = [];
for (let i = 1; i < numbers.length; i++) {
diffs.push(numbers[i] - numbers[i - 1]);
}
// 找到最常见的差值作为步长
const diffCounts = {};
diffs.forEach(diff => {
diffCounts[diff] = (diffCounts[diff] || 0) + 1;
});
let maxCount = 0;
for (const [diff, count] of Object.entries(diffCounts)) {
if (count > maxCount) {
maxCount = count;
step = parseInt(diff);
}
}
}
// 构建模式字符串
const firstPattern = bestGroup[0];
const pattern = `${firstPattern.prefix}[${start}-${end}]${firstPattern.suffix}`;
const result = {
files: bestGroup.map(p => p.file),
pattern,
start,
end,
step,
totalFiles: bestGroup.length,
detectedRange: `${start}-${end}`,
prefix: firstPattern.prefix,
suffix: firstPattern.suffix,
numberLength: firstPattern.numberLength
};
this.log(`🎉 检测到序列帧: ${pattern}, 范围: ${start}-${end}, 步长: ${step}, 文件数: ${bestGroup.length}`, 'info');
return result;
}项目内文件检测实现
javascript
/**
* 检查文件是否已在当前AE项目中导入
* @param {Array} files - 文件数组
* @returns {Promise<Object>} 检测结果
*/
async isProjectInternalFile(files) {
try {
this.log(`🔍 开始检查 ${files.length} 个文件的项目状态`, 'info');
// 快速提取文件路径数组
const filePaths = [];
for (const file of files) {
const filePath = file.path || file.webkitRelativePath || file.name;
if (filePath) {
filePaths.push(filePath);
}
}
if (filePaths.length === 0) {
this.log('❌ 无法获取任何文件路径,允许导入', 'warning');
return {
hasProjectFiles: false,
projectFiles: [],
externalFiles: files
};
}
// 首先快速检查是否有AE项目文件(这个检查很快)
const aeProjectCheck = await this.executeExtendScript('checkAEProjectFiles', filePaths);
if (aeProjectCheck && aeProjectCheck.success && aeProjectCheck.aeProjectFiles.length > 0) {
this.log(`⚠️ 检测到 ${aeProjectCheck.aeProjectFiles.length} 个AE项目文件,禁止导入`, 'warning');
// 使用哈希表快速匹配AE项目文件
const aeProjectPathsSet = new Set();
aeProjectCheck.aeProjectFiles.forEach(path => {
aeProjectPathsSet.add(path.toLowerCase().replace(/\\/g, '/'));
});
const projectFiles = [];
const externalFiles = [];
for (const file of files) {
const currentPath = file.path || file.webkitRelativePath || file.name;
const normalizedCurrent = currentPath.toLowerCase().replace(/\\/g, '/');
// 检查是否为AE项目文件
const isAEProject = aeProjectPathsSet.has(normalizedCurrent) ||
Array.from(aeProjectPathsSet).some(projectPath =>
normalizedCurrent.includes(projectPath) || projectPath.includes(normalizedCurrent)
);
if (isAEProject) {
projectFiles.push({
name: file.name,
path: currentPath,
type: 'ae_project'
});
} else {
externalFiles.push(file);
}
}
return {
hasProjectFiles: true,
projectFiles: projectFiles,
externalFiles: externalFiles
};
}
// 如果没有AE项目文件,检查是否已在项目中导入
this.log('📡 检查文件是否已在项目中...', 'info');
// 对于大量文件,进行分批处理以提高性能
let checkResult;
if (filePaths.length > 100) {
this.log(`📦 文件数量较多(${filePaths.length}),使用分批处理`, 'info');
const batchSize = 50;
const allImportedFiles = [];
const allExternalFiles = [];
for (let i = 0; i < filePaths.length; i += batchSize) {
const batch = filePaths.slice(i, i + batchSize);
const batchResult = await this.executeExtendScript('checkProjectImportedFiles', batch);
if (batchResult && batchResult.success) {
allImportedFiles.push(...(batchResult.importedFiles || []));
allExternalFiles.push(...(batchResult.externalFiles || []));
}
}
checkResult = {
success: true,
importedFiles: allImportedFiles,
externalFiles: allExternalFiles
};
} else {
checkResult = await this.executeExtendScript('checkProjectImportedFiles', filePaths);
}
if (!checkResult || !checkResult.success) {
const errorMsg = checkResult?.error || '未知错误';
this.log(`💥 项目文件检查失败: ${errorMsg}`, 'error');
// 检测失败时允许导入,避免阻止正常功能
return {
hasProjectFiles: false,
projectFiles: [],
externalFiles: files
};
}
const importedFiles = checkResult.importedFiles || [];
if (importedFiles.length > 0) {
this.log(`⚠️ 检测到 ${importedFiles.length} 个已在项目中的文件`, 'warning');
// 使用哈希表快速匹配已导入文件
const importedPathsSet = new Set();
importedFiles.forEach(path => {
importedPathsSet.add(path.toLowerCase().replace(/\\/g, '/'));
});
const projectFiles = [];
const externalFiles = [];
for (const file of files) {
const currentPath = file.path || file.webkitRelativePath || file.name;
const normalizedCurrent = currentPath.toLowerCase().replace(/\\/g, '/');
if (importedPathsSet.has(normalizedCurrent)) {
projectFiles.push({
name: file.name,
path: currentPath
});
} else {
externalFiles.push(file);
}
}
return {
hasProjectFiles: true,
projectFiles: projectFiles,
externalFiles: externalFiles
};
}
this.log('✅ 所有文件都不在项目中,允许导入', 'info');
return {
hasProjectFiles: false,
projectFiles: [],
externalFiles: files
};
} catch (error) {
this.log(`❌ 项目内文件检测失败: ${error.message}`, 'error');
// 检测失败时允许导入,避免阻止正常功能
return {
hasProjectFiles: false,
projectFiles: [],
externalFiles: files
};
}
}导入确认对话框实现
javascript
/**
* 显示文件导入对话框
* @param {Array} files - 文件数组
* @param {Object} analysis - 分析结果
* @param {boolean} skipProjectCheck - 是否跳过项目文件检查
*/
async showFileImportDialog(files, analysis, skipProjectCheck = false) {
// 移除现有对话框
const existingDialog = document.querySelector('.eagle-confirm-dialog');
if (existingDialog) {
existingDialog.remove();
}
// 检查是否包含项目文件或AE项目文件(仅在未跳过检查时执行)
if (!skipProjectCheck) {
try {
this.log('🔍 检查文件类型和项目状态...', 'info');
const projectFileCheck = await this.isProjectInternalFile(files);
if (projectFileCheck.hasProjectFiles) {
this.log('⚠️ 检测到项目文件,显示警告并阻止导入', 'warning');
this.showProjectInternalFileWarning(projectFileCheck);
return; // 阻止继续显示导入对话框
}
this.log('✅ 文件检查通过,继续显示导入对话框', 'info');
} catch (error) {
this.log(`⚠️ 项目文件检查失败: ${error.message},继续显示导入对话框`, 'warning');
// 检查失败时继续显示对话框,避免阻止正常功能
}
}
// 检测是否包含序列帧文件夹
const hasSequences = analysis.sequences && analysis.sequences.length > 0;
const folderCount = analysis.folders ? analysis.folders.size : 0;
// 生成检测统计信息
let detectionInfo = '';
if (hasSequences) {
// 计算实际序列帧文件数量
const totalSequenceFiles = analysis.sequences.reduce((sum, seq) => sum + seq.files.length, 0);
detectionInfo = `检测到 ${analysis.sequences.length} 个序列帧文件夹,共 ${totalSequenceFiles} 个文件`;
} else if (folderCount > 0) {
detectionInfo = `检测到 ${folderCount} 个文件夹,共 ${files.length} 个文件`;
} else {
detectionInfo = `检测到 ${files.length} 个文件`;
}
// 确定要显示的文件列表
let displayFiles = files;
let totalDisplayFiles = files.length;
if (hasSequences) {
// 对于序列帧,显示序列帧中的文件
displayFiles = [];
analysis.sequences.forEach(seq => {
displayFiles = displayFiles.concat(seq.files.slice(0, Math.max(1, Math.floor(5 / analysis.sequences.length))));
});
totalDisplayFiles = analysis.sequences.reduce((sum, seq) => sum + seq.files.length, 0);
}
// 生成文件信息HTML
let fileInfoHtml = '';
if (hasSequences) {
// 序列帧显示为单行
analysis.sequences.forEach(seq => {
// 计算序列帧总大小
const totalSize = seq.files.reduce((sum, file) => sum + (file.size || 0), 0);
const sizeText = this.formatFileSize(totalSize);
fileInfoHtml += `
<div class="file-item-simple">
<span class="file-icon">🎞️</span>
<span class="file-name">${seq.pattern}</span>
<span class="file-size">${sizeText}</span>
<span class="file-type">序列帧</span>
</div>
`;
});
} else {
// 普通文件显示
fileInfoHtml = displayFiles.slice(0, 5).map(file => {
const icon = this.getFileIcon(file);
const size = this.formatFileSize(file.size);
const type = this.getFileType(file);
return `
<div class="file-item-simple">
<span class="file-icon">${icon}</span>
<span class="file-name">${file.name}</span>
<span class="file-size">${size}</span>
<span class="file-type">${type}</span>
</div>
`;
}).join('');
}
// 如果文件数量超过5个,显示省略提示(仅对非序列帧)
const moreFilesHtml = (!hasSequences && totalDisplayFiles > 5) ?
`<div class="file-item-simple"><span class="file-name">... 还有 ${totalDisplayFiles - Math.min(5, displayFiles.length)} 个文件</span></div>` : '';
// 获取当前设置并确定导入模式和行为
const settings = this.settingsManager.getSettings();
// 导入模式映射
const importModeText = {
'direct': '直接导入',
'project_adjacent': '项目旁复制',
'custom_folder': '自定义文件夹'
}[settings.mode] || settings.mode;
// 根据是否自动添加到合成来确定导入行为
let importBehavior;
if (settings.addToComposition) {
// 如果自动添加到合成,显示时间轴放置位置
const timelinePlacement = {
'current_time': '当前时间',
'timeline_start': '时间轴开始'
}[settings.timelineOptions?.placement] || '当前时间';
importBehavior = timelinePlacement;
} else {
// 如果不自动添加到合成,显示"不导入合成"
importBehavior = (window.i18n?.getText('common.doNotImportToComp') || '不导入合成');
}
let importMode = importModeText;
// 检查是否是序列帧或文件夹,并根据情况调整导入行为显示
// 只有当用户没有明确设置导入行为时,才显示特殊的序列帧/文件夹导入提示
if (hasSequences && settings.mode === 'direct') { // 假设直接导入模式下,序列帧导入是特殊行为
importBehavior = '序列帧导入';
} else if (folderCount > 0 && settings.mode === 'direct') { // 假设直接导入模式下,文件夹导入是特殊行为
importBehavior = '文件夹导入';
}
// 创建对话框
const dialog = document.createElement('div');
dialog.className = 'eagle-confirm-dialog';
dialog.innerHTML = `
<div class="eagle-confirm-content">
<div class="eagle-confirm-header">
<h3>拖拽导入确认</h3>
</div>
<div class="eagle-confirm-body">
<p>${detectionInfo}</p>
<div class="file-list">
${fileInfoHtml}
${moreFilesHtml}
</div>
<div class="import-settings-dark">
<div class="setting-item"><span class="setting-label">导入模式:</span><span class="setting-value">${importMode}</span></div>
<div class="setting-item"><span class="setting-label">导入行为:</span><span class="setting-value">${importBehavior}</span></div>
</div>
</div>
<div class="eagle-confirm-actions-flex">
<button id="drag-confirm-yes" class="btn-outline-primary">确认导入</button>
<button id="drag-confirm-no" class="btn-outline-secondary">取消</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 绑定事件
document.getElementById('drag-confirm-yes').onclick = async () => {
dialog.remove();
try {
// 根据检测结果选择导入方式
if (hasSequences) {
await this.handleImportAction(files, analysis, 'sequences');
} else if (folderCount > 0) {
await this.handleImportAction(files, analysis, 'folders');
} else {
await this.handleImportAction(files, analysis, 'all');
}
} catch (error) {
this.log(`❌ 导入操作失败: ${error.message}`, 'error');
// 显示错误弹窗
this.showErrorDialog('导入失败', error.message);
}
};
document.getElementById('drag-confirm-no').onclick = () => {
dialog.remove();
};
// 样式已统一使用剪贴板导入确认面板的样式
}
/**
* 处理导入操作
* @param {Array} files - 文件数组
* @param {Object} analysis - 分析结果
* @param {string} action - 操作类型
*/
async handleImportAction(files, analysis, action) {
let filesToImport = [];
switch (action) {
case 'all':
filesToImport = files;
break;
case 'sequences':
// 导入所有序列帧(以序列为单位)
this.log(`📁 检测到 ${analysis.sequences.length} 个序列帧文件夹`, 'info');
await this.handleSequenceImport(analysis.sequences);
return;
case 'folders':
// 导入文件夹(以文件夹为单位)
this.log(`📁 检测到 ${analysis.folders.size} 个文件夹`, 'info');
await this.handleFolderImport(analysis, files);
return;
case 'images':
filesToImport = analysis.categories.image;
break;
case 'videos':
filesToImport = analysis.categories.video;
break;
default:
filesToImport = files;
}
if (filesToImport.length === 0) {
this.showDropMessage('❌ 没有文件需要导入', 'warning');
return;
}
this.log(`📁 开始导入 ${filesToImport.length} 个文件 (${action} 模式)`, 'info');
// 普通文件导入
await this.handleNonEagleDragImport(filesToImport);
}
/**
* 处理序列帧导入
* @param {Array} sequences - 序列帧数组
*/
async handleSequenceImport(sequences) {
let successCount = 0;
let totalSequences = sequences.length;
for (const sequence of sequences) {
try {
this.log(`📁 导入序列帧文件夹: ${sequence.folder} (${sequence.pattern})`, 'info');
this.log(`📊 序列帧范围: ${sequence.start}-${sequence.end}, 步长: ${sequence.step}, 文件数: ${sequence.files.length}`, 'info');
// 构造序列帧导入消息
const message = {
type: 'import_sequence',
sequence: {
files: sequence.files.map(file => ({
name: file.name,
path: file.fullPath || file.relativePath || file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
})),
pattern: sequence.pattern,
start: sequence.start,
end: sequence.end,
step: sequence.step,
folder: sequence.folder,
totalFiles: sequence.files.length
},
source: 'sequence_drag_drop',
timestamp: Date.now(),
isDragImport: true
};
// 调用序列帧导入处理
const result = await this.handleImportFiles(message);
if (result && result.success) {
successCount++;
this.log(`✅ 序列帧文件夹导入成功: ${sequence.folder}`, 'success');
} else {
const errorMessage = result ? result.error : '序列帧导入失败';
this.log(`❌ 序列帧文件夹导入失败: ${sequence.folder}`, 'error');
// 如果只有一个序列帧且失败,抛出异常以触发弹窗
if (sequences.length === 1) {
throw new Error(errorMessage);
}
}
} catch (error) {
this.log(`❌ 序列帧导入失败: ${sequence.folder} - ${error.message}`, 'error');
// 如果只有一个序列帧且失败,重新抛出异常以触发弹窗
if (sequences.length === 1) {
throw error;
}
}
}
if (successCount === totalSequences) {
this.showDropMessage(`✅ 所有序列帧文件夹导入完成 (${successCount}/${totalSequences})`, 'success');
} else {
this.showDropMessage(`⚠️ 序列帧导入完成 (${successCount}/${totalSequences})`, 'warning');
}
}
/**
* 处理文件夹导入
* @param {Object} analysis - 分析结果
* @param {Array} allFiles - 所有文件
*/
async handleFolderImport(analysis, allFiles) {
const folderGroups = {};
// 按文件夹分组文件
allFiles.forEach(file => {
const path = file.fullPath || file.relativePath || file.webkitRelativePath || '';
const folderPath = path.substring(0, path.lastIndexOf('/'));
if (folderPath) {
if (!folderGroups[folderPath]) {
folderGroups[folderPath] = [];
}
folderGroups[folderPath].push(file);
}
});
let successCount = 0;
let totalFolders = Object.keys(folderGroups).length;
// 逐个文件夹导入
for (const [folderPath, folderFiles] of Object.entries(folderGroups)) {
try {
this.log(`📁 导入文件夹: ${folderPath} (${folderFiles.length} 个文件)`, 'info');
// 构造文件夹导入消息
const message = {
type: 'import_folder',
folder: {
path: folderPath,
files: folderFiles.map(file => ({
name: file.name,
path: file.fullPath || file.relativePath || file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
})),
totalFiles: folderFiles.length
},
source: 'folder_drag_drop',
timestamp: Date.now(),
isDragImport: true
};
// 调用文件夹导入处理
const result = await this.handleImportFiles(message);
if (result && result.success) {
successCount++;
this.log(`✅ 文件夹导入成功: ${folderPath}`, 'success');
} else {
const errorMessage = result ? result.error : '文件夹导入失败';
this.log(`❌ 文件夹导入失败: ${folderPath}`, 'error');
// 如果只有一个文件夹且失败,抛出异常以触发弹窗
if (totalFolders === 1) {
throw new Error(errorMessage);
}
}
} catch (error) {
this.log(`❌ 文件夹导入失败: ${folderPath} - ${error.message}`, 'error');
}
}
if (successCount === totalFolders) {
this.showDropMessage(`✅ 所有文件夹导入完成 (${successCount}/${totalFolders})`, 'success');
} else {
this.showDropMessage(`⚠️ 文件夹导入完成 (${successCount}/${totalFolders})`, 'warning');
}
}
/**
* 处理Eagle拖拽导入
* @param {Array} files - 文件数组
*/
async handleEagleDragImport(files) {
try {
// 优先检查项目状态 - 确保AE项目已打开
const projectStatusValid = await this.projectStatusChecker.validateProjectStatus({
requireProject: true,
requireActiveComposition: false, // 拖拽时不强制要求合成,后续会检查
showWarning: true
});
if (!projectStatusValid) {
this.log('❌ 拖拽导入被阻止:项目状态不满足要求', 'warning');
return;
}
// 转换文件格式以匹配现有的导入接口
const fileData = files.map(file => {
// 尝试获取完整路径信息
let fullPath = file.path || file.webkitRelativePath || file.name;
// 如果是拖拽导入且有完整路径信息,尝试提取目录路径
if (file.path && file.path.includes('\\')) {
// Windows路径格式,保持完整路径
fullPath = file.path;
} else if (file.webkitRelativePath) {
// 相对路径,需要结合其他信息构建完整路径
fullPath = file.webkitRelativePath;
}
return {
name: file.name,
path: fullPath,
size: file.size,
type: file.type,
lastModified: file.lastModified,
isDragImport: true,
// 添加原始文件对象引用,用于后续路径解析
originalFile: file
};
});
// 构造消息对象,模拟Eagle扩展发送的消息格式
const message = {
type: 'export',
files: fileData,
source: 'drag_drop',
timestamp: Date.now(),
isDragImport: true
};
// 调用现有的文件处理流程
const result = await this.handleImportFiles(message);
// 只有导入成功时才播放音效和显示提示
if (result && result.success) {
// 播放Eagle导入音效
try {
if (this.soundPlayer && typeof this.soundPlayer.playEagleSound === 'function') {
this.soundPlayer.playEagleSound();
}
} catch (soundError) {
// 忽略音效播放错误,不影响主要功能
console.warn('❌ 播放Eagle音效失败:', soundError);
}
// 显示简洁的成功提示
this.showDropMessage(`✅ 导入成功`, 'success');
}
} catch (error) {
this.log(`❌ Eagle拖拽导入失败: ${error.message}`, 'error');
this.showDropMessage('❌ 导入失败', 'error');
}
}
/**
* 处理非Eagle文件拖拽导入
* @param {Array} files - 文件数组
*/
async handleNonEagleDragImport(files) {
try {
// 转换文件格式以匹配现有的导入接口
const fileData = files.map(file => ({
name: file.name,
path: file.path || file.webkitRelativePath || file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
isDragImport: true,
isNonEagleFile: true // 标记为非Eagle文件
}));
// 构造消息对象,模拟文件导入消息格式
const message = {
type: 'import',
files: fileData,
source: 'file_drag_drop',
timestamp: Date.now(),
isDragImport: true
};
// 调用现有的文件处理流程
const result = await this.handleImportFiles(message);
// 只有导入成功时才显示提示
if (result && result.success) {
// 显示简洁的成功提示
this.showDropMessage(`✅ 导入成功`, 'success');
}
} catch (error) {
this.log(`❌ 文件拖拽导入失败: ${error.message}`, 'error');
this.showDropMessage('❌ 导入失败', 'error');
// 显示详细错误弹窗
this.showErrorDialog('文件导入失败', error.message);
}
}API参考
核心方法
setupDragAndDrop()
设置拖拽监听
javascript
/**
* 设置拖拽监听
*/
setupDragAndDrop()handleDragPreview()
处理拖拽预览
javascript
/**
* 处理拖拽预览
* @param {DragEvent} event - 拖拽事件
*/
async handleDragPreview(event)performDragPreviewCheck()
执行拖拽预览检查
javascript
/**
* 执行拖拽预览检查
* @param {DragEvent} event - 拖拽事件
*/
async performDragPreviewCheck(event)updateDragHint()
更新拖拽提示文本
javascript
/**
* 更新拖拽提示文本
* @param {string} text - 提示文本
* @param {string} type - 提示类型
*/
updateDragHint(text, type = 'default')resetDragPreviewState()
重置拖拽预览状态
javascript
/**
* 重置拖拽预览状态
*/
resetDragPreviewState()handleFileDrop()
处理文件拖拽
javascript
/**
* 处理文件拖拽
* @param {DragEvent} event - 拖拽事件
*/
async handleFileDrop(event)handleDirectoryDrop()
处理文件夹拖拽
javascript
/**
* 处理文件夹拖拽
* @param {Array} items - DataTransferItem数组
* @param {Array} files - File数组
*/
async handleDirectoryDrop(items, files)readDirectoryEntry()
递归读取文件夹内容
javascript
/**
* 递归读取文件夹内容
* @param {FileSystemEntry} entry - 文件系统条目
* @returns {Promise<Array>} 文件数组
*/
async readDirectoryEntry(entry)analyzeDroppedFiles()
分析拖拽的文件
javascript
/**
* 分析拖拽的文件
* @param {Array} files - 文件数组
* @returns {Object} 分析结果
*/
analyzeDroppedFiles(files)detectImageSequence()
检测图片序列
javascript
/**
* 检测图片序列
* @param {Array} files - 文件数组
* @returns {Object|null} 序列帧信息或null
*/
detectImageSequence(files)isProjectInternalFile()
检查文件是否已在当前AE项目中导入
javascript
/**
* 检查文件是否已在当前AE项目中导入
* @param {Array} files - 文件数组
* @returns {Promise<Object>} 检测结果
*/
async isProjectInternalFile(files)showFileImportDialog()
显示文件导入对话框
javascript
/**
* 显示文件导入对话框
* @param {Array} files - 文件数组
* @param {Object} analysis - 分析结果
* @param {boolean} skipProjectCheck - 是否跳过项目文件检查
*/
async showFileImportDialog(files, analysis, skipProjectCheck = false)handleImportAction()
处理导入操作
javascript
/**
* 处理导入操作
* @param {Array} files - 文件数组
* @param {Object} analysis - 分析结果
* @param {string} action - 操作类型
*/
async handleImportAction(files, analysis, action)handleSequenceImport()
处理序列帧导入
javascript
/**
* 处理序列帧导入
* @param {Array} sequences - 序列帧数组
*/
async handleSequenceImport(sequences)handleFolderImport()
处理文件夹导入
javascript
/**
* 处理文件夹导入
* @param {Object} analysis - 分析结果
* @param {Array} allFiles - 所有文件
*/
async handleFolderImport(analysis, allFiles)handleEagleDragImport()
处理Eagle拖拽导入
javascript
/**
* 处理Eagle拖拽导入
* @param {Array} files - 文件数组
*/
async handleEagleDragImport(files)handleNonEagleDragImport()
处理非Eagle文件拖拽导入
javascript
/**
* 处理非Eagle文件拖拽导入
* @param {Array} files - 文件数组
*/
async handleNonEagleDragImport(files)辅助方法
getFileCategory()
获取文件分类
javascript
/**
* 获取文件分类
* @param {File} file - 文件对象
* @returns {string} 文件分类
*/
getFileCategory(file)getFileIcon()
获取文件图标
javascript
/**
* 获取文件图标
* @param {File} file - 文件对象
* @returns {string} 文件图标
*/
getFileIcon(file)getFileType()
获取文件类型
javascript
/**
* 获取文件类型
* @param {File} file - 文件对象
* @returns {string} 文件类型
*/
getFileType(file)formatFileSize()
格式化文件大小
javascript
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化的文件大小
*/
formatFileSize(bytes)showDropMessage()
显示拖拽消息
javascript
/**
* 显示拖拽消息
* @param {string} message - 消息内容
* @param {string} type - 消息类型
*/
showDropMessage(message, type = 'info')showErrorDialog()
显示错误对话框
javascript
/**
* 显示错误对话框
* @param {string} title - 错误标题
* @param {string} message - 错误消息
*/
showErrorDialog(title, message)使用示例
基本使用
javascript
// 初始化拖拽导入增强功能
const enhancedDragDrop = new EnhancedDragAndDrop(aeExtension);
// 设置拖拽监听
enhancedDragDrop.setupDragAndDrop();
// 处理文件拖拽
document.addEventListener('drop', async (event) => {
await enhancedDragDrop.handleFileDrop(event);
});高级使用
javascript
// 自定义拖拽预览
enhancedDragDrop.updateDragHint('拖拽文件到此处', 'default');
// 处理文件夹拖拽
const folderItems = [...]; // FileSystemItem 数组
const files = [...]; // File 数组
await enhancedDragDrop.handleDirectoryDrop(folderItems, files);
// 分析拖拽文件
const analysis = enhancedDragDrop.analyzeDroppedFiles(files);
console.log('文件分析结果:', analysis);
// 检测序列帧
const sequences = files.filter(file => enhancedDragDrop.getFileCategory(file) === 'image');
const sequenceInfo = enhancedDragDrop.detectImageSequence(sequences);
if (sequenceInfo) {
console.log('检测到序列帧:', sequenceInfo.pattern);
}
// 检查项目内文件
const projectCheck = await enhancedDragDrop.isProjectInternalFile(files);
if (projectCheck.hasProjectFiles) {
console.log('检测到项目内文件:', projectCheck.projectFiles);
}
// 显示导入确认对话框
await enhancedDragDrop.showFileImportDialog(files, analysis);最佳实践
性能优化
防抖处理
javascript// 使用防抖避免频繁的拖拽预检查 const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; const debouncedDragPreview = debounce(handleDragPreview, 200);批量处理
javascript// 对于大量文件,使用批量处理提高性能 async function batchProcessFiles(filePaths, batchSize = 50) { const results = []; for (let i = 0; i < filePaths.length; i += batchSize) { const batch = filePaths.slice(i, i + batchSize); const batchResult = await processFileBatch(batch); results.push(...batchResult); } return results; }缓存机制
javascript// 缓存检测结果避免重复计算 const fileCache = new Map(); function getCachedFileCategory(file) { const cacheKey = `${file.name}_${file.size}_${file.lastModified}`; if (fileCache.has(cacheKey)) { return fileCache.get(cacheKey); } const category = getFileCategory(file); fileCache.set(cacheKey, category); // 限制缓存大小 if (fileCache.size > 1000) { const firstKey = fileCache.keys().next().value; fileCache.delete(firstKey); } return category; }
用户体验优化
视觉反馈
css/* 拖拽悬停时的整体效果 */ body.drag-over { transition: all 0.3s ease; } /* 拖拽时的背景蒙版 */ body.drag-over::before { content: ''; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 999; pointer-events: none; animation: fadeIn 0.3s ease; } /* 拖拽时的边框效果 */ body.drag-over::after { content: ''; position: fixed; top: 8px; left: 8px; right: 8px; bottom: 8px; border: 2px dashed #3498db; border-radius: 12px; z-index: 1000; pointer-events: none; animation: dragPulse 1.5s ease-in-out infinite alternate; }响应式设计
css/* 响应式断点 */ @media (max-width: 768px) { .virtual-dialog { margin: 16px; min-width: auto; max-width: calc(100% - 32px); } } @media (max-width: 480px) { .virtual-dialog { margin: 8px; max-width: calc(100% - 16px); } }动画效果
css@keyframes dragPulse { from { opacity: 0.3; } to { opacity: 0.7; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
故障排除
常见问题
拖拽无响应
- 症状:拖拽文件到面板无任何反应
- 解决:
- 检查拖拽事件监听器是否正确绑定
- 验证event.preventDefault()是否正确调用
- 检查CSS pointer-events属性
文件夹拖拽失败
- 症状:拖拽文件夹时无法正确读取内容
- 解决:
- 检查webkitGetAsEntry API支持情况
- 验证递归读取文件夹权限
- 检查文件路径解析逻辑
序列帧检测不准
- 症状:序列帧文件未被正确识别
- 解决:
- 检查文件命名是否符合标准格式
- 验证数字模式匹配正则表达式
- 调整最小序列帧数量阈值
项目内文件检测失败
- 症状:已在项目中的文件被错误标记为新文件
- 解决:
- 检查AE连接状态
- 验证文件路径匹配算法
- 确认ExtendScript脚本执行结果
调试技巧
启用详细日志
javascript
// 在控制台中启用详细日志
localStorage.setItem('debugLogLevel', '0');
// 监控拖拽事件
document.addEventListener('dragover', (e) => {
console.log('dragover事件触发:', e);
});
document.addEventListener('drop', (e) => {
console.log('drop事件触发:', e);
console.log('文件数量:', e.dataTransfer.files.length);
});检查文件信息
javascript
// 检查拖拽文件的详细信息
function inspectDraggedFiles(event) {
const files = Array.from(event.dataTransfer.files);
files.forEach((file, index) => {
console.log(`文件 ${index + 1}:`, {
name: file.name,
path: file.path || file.webkitRelativePath,
size: file.size,
type: file.type,
lastModified: file.lastModified
});
});
}性能分析
javascript
// 记录拖拽处理性能
const startTime = performance.now();
await enhancedDragDrop.handleFileDrop(event);
const endTime = performance.now();
console.log(`拖拽处理耗时: ${endTime - startTime}ms`);扩展性
自定义扩展
扩展拖拽处理逻辑
javascript
// 创建自定义拖拽处理类
class CustomDragHandler extends EnhancedDragAndDrop {
constructor(aeExtension) {
super(aeExtension);
this.customHandlers = new Map();
}
/**
* 注册自定义拖拽处理程序
* @param {string} mimeType - MIME类型
* @param {Function} handler - 处理函数
*/
registerCustomHandler(mimeType, handler) {
this.customHandlers.set(mimeType, handler);
}
/**
* 处理自定义MIME类型
* @param {string} mimeType - MIME类型
* @param {Object} data - 数据
*/
async handleCustomMimeType(mimeType, data) {
const handler = this.customHandlers.get(mimeType);
if (handler && typeof handler === 'function') {
return await handler(data);
}
throw new Error(`未找到处理程序: ${mimeType}`);
}
}
// 注册自定义处理程序
const customDragHandler = new CustomDragHandler(aeExtension);
customDragHandler.registerCustomHandler('application/x-eagle-item', async (data) => {
// 处理Eagle项目文件
console.log('处理Eagle项目文件:', data);
return { success: true };
});插件化架构
javascript
// 创建拖拽插件
class DragDropPlugin {
constructor(enhancedDragDrop) {
this.dragDrop = enhancedDragDrop;
this.init();
}
init() {
// 添加自定义功能
this.dragDrop.addCustomFeature('batchImport', this.batchImport.bind(this));
this.dragDrop.addCustomFeature('smartSort', this.smartSort.bind(this));
// 监听拖拽事件
this.dragDrop.addEventListener('fileDrop', this.handleFileDrop.bind(this));
}
/**
* 批量导入功能
* @param {Array} files - 文件数组
*/
async batchImport(files) {
// 实现批量导入逻辑
console.log('批量导入文件:', files.length);
}
/**
* 智能排序功能
* @param {Array} files - 文件数组
*/
smartSort(files) {
// 实现智能排序逻辑
return files.sort((a, b) => {
// 按文件类型排序
const typeA = this.dragDrop.getFileCategory(a);
const typeB = this.dragDrop.getFileCategory(b);
if (typeA !== typeB) {
return typeA.localeCompare(typeB);
}
// 按文件名排序
return a.name.localeCompare(b.name);
});
}
/**
* 处理文件拖拽事件
* @param {CustomEvent} event - 拖拽事件
*/
async handleFileDrop(event) {
const { files, analysis } = event.detail;
// 应用智能排序
const sortedFiles = this.smartSort(files);
// 触发批量导入
await this.batchImport(sortedFiles);
}
}
// 应用插件
const plugin = new DragDropPlugin(enhancedDragDrop);