|
|
@@ -1924,7 +1924,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 提取拖拽内容
|
|
|
+ * 提取拖拽内容(支持企业微信特殊格式)
|
|
|
*/
|
|
|
private extractDragContent(event: DragEvent): {
|
|
|
files: File[];
|
|
|
@@ -1941,18 +1941,41 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
const files: File[] = dt.files ? Array.from(dt.files) : [];
|
|
|
const images = files.filter(f => f.type.startsWith('image/'));
|
|
|
|
|
|
- // 提取文字(过滤掉[图片]占位符)
|
|
|
+ // 提取文字(智能处理企业微信格式)
|
|
|
let text = dt.getData('text/plain') || '';
|
|
|
- text = text.replace(/\[图片\]/g, '').trim();
|
|
|
+
|
|
|
+ // 🔥 企业微信特殊处理:检测文件名模式(如 4e370f418f0671be8a4fc68674266f3c.jpg)
|
|
|
+ const wechatFilePattern = /\b[a-f0-9]{32}\.(jpg|jpeg|png|gif|webp)\b/gi;
|
|
|
+ const matches = text.match(wechatFilePattern);
|
|
|
+
|
|
|
+ if (matches && matches.length > 0) {
|
|
|
+ console.log('🔍 检测到企业微信文件名格式:', matches);
|
|
|
+ // 如果文本主要是文件名,清空文本内容(避免显示为文件名)
|
|
|
+ if (text.replace(wechatFilePattern, '').trim().length < 20) {
|
|
|
+ text = '';
|
|
|
+ } else {
|
|
|
+ // 否则只移除文件名部分
|
|
|
+ text = text.replace(wechatFilePattern, '').replace(/\[图片\]/g, '').trim();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 普通处理:移除[图片]占位符
|
|
|
+ text = text.replace(/\[图片\]/g, '').replace(/\[文件\]/g, '').trim();
|
|
|
+ }
|
|
|
|
|
|
// 提取HTML
|
|
|
const html = dt.getData('text/html') || '';
|
|
|
|
|
|
- // 提取URL
|
|
|
+ // 提取URL(过滤掉文件名形式的假URL)
|
|
|
const uriList = dt.getData('text/uri-list') || '';
|
|
|
const urls = uriList.split('\n')
|
|
|
.map(url => url.trim())
|
|
|
- .filter(url => url && !url.startsWith('#'));
|
|
|
+ .filter(url => {
|
|
|
+ // 过滤条件:必须是有效URL且不是#开头
|
|
|
+ if (!url || url.startsWith('#')) return false;
|
|
|
+ // 过滤掉纯文件名(没有协议头的)
|
|
|
+ if (!url.includes('://')) return false;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
|
|
|
const hasContent = files.length > 0 || text.length > 0 || urls.length > 0;
|
|
|
|
|
|
@@ -1983,6 +2006,8 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
async onAIFileDrop(event: DragEvent): Promise<void> {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
+
|
|
|
+ // 🔥 立即重置拖拽状态,确保可以连续拖拽
|
|
|
this.aiDesignDragOver = false;
|
|
|
|
|
|
console.log('📥 [AI对话拖拽] 放下');
|
|
|
@@ -1997,6 +2022,8 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
if (!content.hasContent) {
|
|
|
console.warn('⚠️ [AI对话拖拽] 未检测到有效内容');
|
|
|
+ // 🔥 确保状态正确重置
|
|
|
+ this.cdr.markForCheck();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -2035,6 +2062,9 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
window?.fmode?.alert?.('处理拖拽内容失败,请重试');
|
|
|
} finally {
|
|
|
this.aiDesignUploading = false;
|
|
|
+ // 🔥 确保拖拽状态完全重置,支持连续拖拽
|
|
|
+ this.aiDesignDragOver = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -3835,22 +3865,53 @@ ${context}
|
|
|
|
|
|
/**
|
|
|
* 处理AI文件上传(统一处理点击和拖拽)
|
|
|
- * 🔥 修复:直接转base64,不上传到云存储,避免631错误
|
|
|
+ * 🔥 扩展支持:图片、PDF、CAD文件
|
|
|
*/
|
|
|
private async handleAIFileUpload(files: File[]): Promise<void> {
|
|
|
const maxFiles = 20; // 扩展至20个文件
|
|
|
- const remainingSlots = maxFiles - this.aiDesignUploadedImages.length;
|
|
|
+ const remainingSlots = maxFiles - this.aiDesignUploadedImages.length - this.aiDesignUploadedFiles.filter(f => !f.isImage).length;
|
|
|
|
|
|
if (remainingSlots <= 0) {
|
|
|
window?.fmode?.alert(`最多只能上传${maxFiles}个文件`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 🔥 只支持图片格式进行AI分析
|
|
|
- const supportedImageTypes = [
|
|
|
- 'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
|
|
|
- 'image/webp', 'image/bmp', 'image/tiff'
|
|
|
- ];
|
|
|
+ // 🔥 扩展支持的文件类型
|
|
|
+ const fileTypeConfig = {
|
|
|
+ images: {
|
|
|
+ types: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff'],
|
|
|
+ extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff'],
|
|
|
+ category: 'image',
|
|
|
+ maxSize: 50 * 1024 * 1024, // 50MB
|
|
|
+ compressThreshold: 5 * 1024 * 1024 // 5MB
|
|
|
+ },
|
|
|
+ pdf: {
|
|
|
+ types: ['application/pdf'],
|
|
|
+ extensions: ['pdf'],
|
|
|
+ category: 'document',
|
|
|
+ maxSize: 100 * 1024 * 1024 // 100MB
|
|
|
+ },
|
|
|
+ cad: {
|
|
|
+ types: ['application/acad', 'application/dxf', 'application/dwg', 'application/x-autocad'],
|
|
|
+ extensions: ['dwg', 'dxf', 'dwt', 'dws', 'dwf', 'dwfx'],
|
|
|
+ category: 'cad',
|
|
|
+ maxSize: 200 * 1024 * 1024 // 200MB
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 🔥 打印文件结构
|
|
|
+ console.log('\n========== 📁 文件上传分析 ==========');
|
|
|
+ console.log(`📊 待处理文件数量: ${files.length}`);
|
|
|
+ files.forEach((file, index) => {
|
|
|
+ console.log(`\n📄 文件 ${index + 1}:`);
|
|
|
+ console.log(` ├─ 名称: ${file.name}`);
|
|
|
+ console.log(` ├─ 类型: ${file.type || '未知'}`);
|
|
|
+ console.log(` ├─ 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB (${file.size} bytes)`);
|
|
|
+ console.log(` ├─ 最后修改: ${new Date(file.lastModified).toLocaleString()}`);
|
|
|
+ const ext = file.name.split('.').pop()?.toLowerCase();
|
|
|
+ console.log(` └─ 扩展名: .${ext}`);
|
|
|
+ });
|
|
|
+ console.log('========================================\n');
|
|
|
|
|
|
const filesToProcess = files.slice(0, remainingSlots);
|
|
|
this.aiDesignUploading = true;
|
|
|
@@ -3858,78 +3919,117 @@ ${context}
|
|
|
|
|
|
try {
|
|
|
for (const file of filesToProcess) {
|
|
|
- // 检查文件类型
|
|
|
- const fileExt = file.name.split('.').pop()?.toLowerCase();
|
|
|
- const supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff'];
|
|
|
- const isSupported = supportedImageTypes.includes(file.type) ||
|
|
|
- supportedExtensions.includes(fileExt || '');
|
|
|
-
|
|
|
- if (!isSupported) {
|
|
|
- console.warn(`文件 ${file.name} 格式不支持,跳过`);
|
|
|
- window?.fmode?.alert(`文件格式不支持: ${file.name}\n只支持图片格式: JPG、PNG、GIF、WebP等`);
|
|
|
+ const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
|
|
|
+ let fileCategory = '';
|
|
|
+ let fileConfig: any = null;
|
|
|
+
|
|
|
+ // 🔥 判断文件类型类别
|
|
|
+ if (fileTypeConfig.images.types.includes(file.type) ||
|
|
|
+ fileTypeConfig.images.extensions.includes(fileExt)) {
|
|
|
+ fileCategory = 'image';
|
|
|
+ fileConfig = fileTypeConfig.images;
|
|
|
+ } else if (fileTypeConfig.pdf.types.includes(file.type) ||
|
|
|
+ fileTypeConfig.pdf.extensions.includes(fileExt)) {
|
|
|
+ fileCategory = 'pdf';
|
|
|
+ fileConfig = fileTypeConfig.pdf;
|
|
|
+ } else if (fileTypeConfig.cad.extensions.includes(fileExt)) {
|
|
|
+ // CAD文件的MIME类型可能不准确,主要通过扩展名判断
|
|
|
+ fileCategory = 'cad';
|
|
|
+ fileConfig = fileTypeConfig.cad;
|
|
|
+ } else {
|
|
|
+ console.warn(`⚠️ 文件 ${file.name} 格式不支持,跳过`);
|
|
|
+ console.log(` 检测到的MIME类型: ${file.type}`);
|
|
|
+ console.log(` 检测到的扩展名: .${fileExt}`);
|
|
|
+ window?.fmode?.alert(`文件格式不支持: ${file.name}\n支持格式:\n• 图片:JPG、PNG、GIF、WebP等\n• 文档:PDF\n• CAD:DWG、DXF等`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- // 🔥 智能处理大文件:自动压缩
|
|
|
- let processedFile = file;
|
|
|
- const maxSize = 50 * 1024 * 1024; // 50MB硬限制
|
|
|
- const compressThreshold = 5 * 1024 * 1024; // 5MB开始压缩
|
|
|
-
|
|
|
- if (file.size > maxSize) {
|
|
|
- console.warn(`文件 ${file.name} 超过50MB硬限制,跳过`);
|
|
|
- window?.fmode?.alert(`文件超过50MB限制: ${file.name}\n请使用专业工具压缩后再上传`);
|
|
|
+ console.log(`\n🔍 文件分类识别:`);
|
|
|
+ console.log(` 文件: ${file.name}`);
|
|
|
+ console.log(` 类别: ${fileCategory}`);
|
|
|
+ console.log(` 配置: `, fileConfig);
|
|
|
+
|
|
|
+ // 🔥 检查文件大小
|
|
|
+ if (file.size > fileConfig.maxSize) {
|
|
|
+ console.warn(`⚠️ 文件 ${file.name} 超过${(fileConfig.maxSize / 1024 / 1024).toFixed(0)}MB限制`);
|
|
|
+ window?.fmode?.alert(`文件超过大小限制: ${file.name}\n${fileCategory}文件最大支持${(fileConfig.maxSize / 1024 / 1024).toFixed(0)}MB`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- // 🔥 关键修复:直接转base64,不上传到云存储
|
|
|
- console.log(`📤 准备处理文件: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
-
|
|
|
- // 如果文件大于5MB,自动压缩
|
|
|
- if (file.size > compressThreshold) {
|
|
|
- console.log(`🔄 文件较大,开始压缩...`);
|
|
|
- try {
|
|
|
- processedFile = await this.compressImage(file);
|
|
|
- console.log(`✅ 压缩完成,压缩后大小: ${(processedFile.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
- } catch (compressError) {
|
|
|
- console.warn('⚠️ 压缩失败,使用原文件:', compressError);
|
|
|
- // 压缩失败,继续使用原文件
|
|
|
- }
|
|
|
- }
|
|
|
+ console.log(`📤 准备处理${fileCategory}文件: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
|
|
|
- console.log(`🔄 将图片转换为base64格式...`);
|
|
|
+ let processedFile = file;
|
|
|
+ let base64 = '';
|
|
|
|
|
|
try {
|
|
|
- // 使用FileReader转换为base64
|
|
|
- const base64 = await new Promise<string>((resolve, reject) => {
|
|
|
- const reader = new FileReader();
|
|
|
- reader.onloadend = () => {
|
|
|
- const result = reader.result as string;
|
|
|
- resolve(result);
|
|
|
- };
|
|
|
- reader.onerror = () => {
|
|
|
- reject(new Error('文件读取失败'));
|
|
|
- };
|
|
|
- reader.readAsDataURL(processedFile);
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`✅ 图片已转换为base64,大小: ${(base64.length / 1024).toFixed(2)}KB`);
|
|
|
+ // 🔥 根据文件类型进行不同处理
|
|
|
+ if (fileCategory === 'image') {
|
|
|
+ // 图片处理:可能需要压缩
|
|
|
+ if (file.size > fileConfig.compressThreshold) {
|
|
|
+ console.log(`🔄 图片较大,尝试压缩...`);
|
|
|
+ try {
|
|
|
+ processedFile = await this.compressImage(file);
|
|
|
+ console.log(`✅ 压缩完成,压缩后大小: ${(processedFile.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
+ } catch (compressError) {
|
|
|
+ console.warn('⚠️ 压缩失败,使用原文件:', compressError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为base64
|
|
|
+ console.log(`🔄 将图片转换为base64格式...`);
|
|
|
+ base64 = await this.fileToBase64(processedFile);
|
|
|
+ console.log(`✅ 图片已转换为base64,大小: ${(base64.length / 1024).toFixed(2)}KB`);
|
|
|
+
|
|
|
+ // 保存到图片数组(用于AI分析)
|
|
|
+ this.aiDesignUploadedImages.push(base64);
|
|
|
+
|
|
|
+ } else if (fileCategory === 'pdf' || fileCategory === 'cad') {
|
|
|
+ // PDF/CAD文件处理:直接转base64
|
|
|
+ console.log(`🔄 将${fileCategory.toUpperCase()}文件转换为base64格式...`);
|
|
|
+ base64 = await this.fileToBase64(file);
|
|
|
+ console.log(`✅ ${fileCategory.toUpperCase()}已转换为base64,大小: ${(base64.length / 1024).toFixed(2)}KB`);
|
|
|
+
|
|
|
+ // 🔥 打印文件详细信息
|
|
|
+ console.log(`\n📋 ${fileCategory.toUpperCase()}文件详情:`);
|
|
|
+ console.log(` ├─ 原始大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
+ console.log(` ├─ Base64大小: ${(base64.length / 1024 / 1024).toFixed(2)}MB`);
|
|
|
+ console.log(` ├─ 数据URL前缀: ${base64.substring(0, 50)}...`);
|
|
|
+ console.log(` └─ 可用于: AI分析、文件存储、预览`);
|
|
|
+ }
|
|
|
|
|
|
- // 🔥 保存base64数据(AI分析时使用)
|
|
|
- this.aiDesignUploadedImages.push(base64);
|
|
|
+ // 🔥 保存文件信息到统一数组
|
|
|
this.aiDesignUploadedFiles.push({
|
|
|
- url: base64, // base64字符串
|
|
|
+ url: base64,
|
|
|
name: file.name,
|
|
|
- type: file.type,
|
|
|
+ type: file.type || `application/${fileExt}`,
|
|
|
size: file.size,
|
|
|
extension: fileExt,
|
|
|
- isBase64: true // 标记为base64数据
|
|
|
+ category: fileCategory,
|
|
|
+ isBase64: true,
|
|
|
+ isImage: fileCategory === 'image',
|
|
|
+ isPDF: fileCategory === 'pdf',
|
|
|
+ isCAD: fileCategory === 'cad',
|
|
|
+ uploadedAt: new Date().toISOString()
|
|
|
});
|
|
|
|
|
|
- console.log(`💾 已保存图片: ${file.name}`);
|
|
|
+ console.log(`💾 已保存${fileCategory}文件: ${file.name}`);
|
|
|
+
|
|
|
+ // 🔥 保存到ProjectFile表(所有类型文件都保存)
|
|
|
+ try {
|
|
|
+ const categoryMap = {
|
|
|
+ 'image': 'ai_design_reference',
|
|
|
+ 'pdf': 'ai_document_reference',
|
|
|
+ 'cad': 'ai_cad_reference'
|
|
|
+ };
|
|
|
+ await this.saveFileToProjectFile(processedFile, base64, categoryMap[fileCategory] || 'ai_file_reference');
|
|
|
+ console.log(`✅ 文件已持久化存储到ProjectFile表`);
|
|
|
+ } catch (saveError) {
|
|
|
+ console.warn('⚠️ 保存到ProjectFile表失败,但不影响AI分析:', saveError);
|
|
|
+ }
|
|
|
|
|
|
- } catch (convertError) {
|
|
|
- console.error(`❌ 转换文件失败: ${file.name}`, convertError);
|
|
|
- window?.fmode?.alert(`处理文件失败: ${file.name}`);
|
|
|
+ } catch (convertError: any) {
|
|
|
+ console.error(`❌ 处理${fileCategory}文件失败: ${file.name}`, convertError);
|
|
|
+ window?.fmode?.alert(`处理文件失败: ${file.name}\n${convertError?.message || '未知错误'}`);
|
|
|
continue;
|
|
|
}
|
|
|
}
|
|
|
@@ -3947,6 +4047,101 @@ ${context}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 🔥 将文件转换为base64格式
|
|
|
+ */
|
|
|
+ private async fileToBase64(file: File): Promise<string> {
|
|
|
+ return new Promise<string>((resolve, reject) => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onloadend = () => {
|
|
|
+ const result = reader.result as string;
|
|
|
+ resolve(result);
|
|
|
+ };
|
|
|
+ reader.onerror = () => {
|
|
|
+ reject(new Error('文件读取失败'));
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🔥 保存文件到ProjectFile表(支持图片、PDF、CAD等)
|
|
|
+ */
|
|
|
+ private async saveFileToProjectFile(file: File, base64: string, fileCategory: string = 'ai_design_reference'): Promise<void> {
|
|
|
+ try {
|
|
|
+ if (!this.projectId) {
|
|
|
+ console.warn('⚠️ 没有项目ID,无法保存到ProjectFile表');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`💾 [ProjectFile] 开始保存文件到数据库: ${file.name},类型: ${file.type}`);
|
|
|
+
|
|
|
+ // 创建Parse File(使用base64)
|
|
|
+ const base64Data = base64.split(',')[1]; // 移除data URL前缀
|
|
|
+ const parseFile = new Parse.File(file.name, { base64: base64Data });
|
|
|
+
|
|
|
+ // 🔥 修复:使用正确的save方法(类型断言确保方法存在)
|
|
|
+ const savedFile = await (parseFile as any).save();
|
|
|
+ const fileUrl = savedFile ? savedFile.url() : '';
|
|
|
+
|
|
|
+ console.log(`✅ [ProjectFile] Parse File保存成功:`, fileUrl);
|
|
|
+
|
|
|
+ // 创建ProjectFile记录
|
|
|
+ const ProjectFile = Parse.Object.extend('ProjectFile');
|
|
|
+ const projectFile = new ProjectFile();
|
|
|
+
|
|
|
+ // 设置项目关联
|
|
|
+ projectFile.set('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: this.projectId
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置文件信息(使用attach字段)
|
|
|
+ projectFile.set('attach', {
|
|
|
+ name: file.name,
|
|
|
+ originalName: file.name,
|
|
|
+ url: fileUrl,
|
|
|
+ mime: file.type,
|
|
|
+ size: file.size,
|
|
|
+ source: 'ai_design_analysis',
|
|
|
+ description: 'AI设计分析参考图'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置其他字段
|
|
|
+ projectFile.set('key', `ai-design-analysis/${this.projectId}/${file.name}`);
|
|
|
+ projectFile.set('uploadedAt', new Date());
|
|
|
+ projectFile.set('category', 'ai_design_reference');
|
|
|
+ projectFile.set('fileType', 'reference_image');
|
|
|
+ projectFile.set('stage', 'requirements');
|
|
|
+
|
|
|
+ // 如果有选择空间,保存关联
|
|
|
+ if (this.aiDesignCurrentSpace?.id) {
|
|
|
+ projectFile.set('product', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Product',
|
|
|
+ objectId: this.aiDesignCurrentSpace.id
|
|
|
+ });
|
|
|
+
|
|
|
+ projectFile.set('data', {
|
|
|
+ spaceId: this.aiDesignCurrentSpace.id,
|
|
|
+ spaceName: this.aiDesignCurrentSpace.name || this.aiDesignCurrentSpace.productName,
|
|
|
+ uploadedFor: 'ai_design_analysis',
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存到数据库
|
|
|
+ await projectFile.save();
|
|
|
+ console.log(`✅ [ProjectFile] 记录已创建:`, projectFile.id);
|
|
|
+ console.log(`📂 [ProjectFile] 文件已保存到ProjectFile表,可在文件管理中查看`);
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ [ProjectFile] 保存失败:', error);
|
|
|
+ // 不抛出错误,允许继续AI分析
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 开始AI分析(直接调用AI进行真实分析)
|
|
|
*/
|