Browse Source

feat:drag-upload test

徐福静0235668 4 hours ago
parent
commit
6098a15c4f

+ 26 - 17
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -91,29 +91,38 @@
                      [class.drag-over]="aiDesignDragOver">
                   @for (file of aiDesignUploadedFiles; track file.url; let i = $index) {
                     <div class="file-item" [class.is-image]="file.extension && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.extension)">
-                      @if (file.extension && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.extension)) {
+                      @if (file.isImage) {
                         <!-- 图片预览 -->
-                        <img [src]="file.url" [alt]="file.name">
+                        <img [src]="file.url" [alt]="file.name" class="file-preview">
+                      } @else if (file.isPDF) {
+                        <!-- PDF图标 -->
+                        <div class="file-icon pdf-icon">
+                          <span class="icon-text">📄</span>
+                          <span class="file-ext">PDF</span>
+                        </div>
+                      } @else if (file.isCAD) {
+                        <!-- CAD图标 -->
+                        <div class="file-icon cad-icon">
+                          <span class="icon-text">📐</span>
+                          <span class="file-ext">CAD</span>
+                        </div>
                       } @else {
-                        <!-- 文件图标 -->
-                        <div class="file-icon" [class.pdf]="file.extension === 'pdf'" [class.cad]="file.extension === 'dwg' || file.extension === 'dxf'">
-                          @if (file.extension === 'pdf') {
-                            <span class="icon-text">📄</span>
-                            <span class="file-ext">PDF</span>
-                          } @else if (file.extension === 'dwg' || file.extension === 'dxf') {
-                            <span class="icon-text">📐</span>
-                            <span class="file-ext">{{ file.extension?.toUpperCase() }}</span>
-                          } @else {
-                            <span class="icon-text">📄</span>
-                            <span class="file-ext">{{ file.extension?.toUpperCase() }}</span>
-                          }
+                        <!-- 通用文件图标 -->
+                        <div class="file-icon generic-icon">
+                          <span class="icon-text">📎</span>
+                          <span class="file-ext">{{ file.extension?.toUpperCase() || 'FILE' }}</span>
                         </div>
                       }
                       <div class="file-info">
-                        <div class="file-name">{{ file.name }}</div>
-                        <div class="file-size">{{ formatFileSize(file.size) }}</div>
+                        <div class="file-name" [title]="file.name">{{ file.name }}</div>
+                        <div class="file-meta">
+                          <span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
+                          @if (file.category) {
+                            <span class="file-category">{{ file.category }}</span>
+                          }
+                        </div>
                       </div>
-                      <button class="remove-btn" (click)="removeAIDialogImage(i)">×</button>
+                      <button class="remove-btn" (click)="removeAIDialogImage(i)" title="移除文件">×</button>
                     </div>
                   }
                   @if (aiDesignUploadedFiles.length < 3) {

+ 83 - 44
src/modules/project/pages/project-detail/stages/stage-requirements.component.scss

@@ -3616,88 +3616,127 @@
             background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
             color: white;
             padding: 8px;
-            opacity: 0;
-            transition: opacity 0.2s;
-          }
-          
-          &:hover .file-info {
             opacity: 1;
           }
         }
-
+        
+        // 图片预览样式
+        .file-preview {
+          width: 100%;
+          height: 100px;
+          object-fit: cover;
+        }
+        
+        // 文件图标样式(PDF、CAD等)
         .file-icon {
-          aspect-ratio: 1;
+          width: 100%;
+          height: 100px;
           display: flex;
           flex-direction: column;
           align-items: center;
           justify-content: center;
-          gap: 12px;
-          background: linear-gradient(135deg, #f5f7fa 0%, #e2e8f0 100%);
-          padding: 20px;
-          
-          ion-icon {
-            font-size: 48px;
-            color: #718096;
-          }
+          gap: 4px;
           
-          .file-ext {
-            font-size: 14px;
-            font-weight: 600;
-            color: #4a5568;
+          &.pdf-icon {
+            background: linear-gradient(135deg, #FFE5E5, #FFCCCC);
+            
+            .icon-text {
+              font-size: 32px;
+            }
+            
+            .file-ext {
+              font-size: 12px;
+              font-weight: 600;
+              color: #D32F2F;
+            }
           }
           
-          &.pdf ion-icon {
-            color: #e53e3e;
+          &.cad-icon {
+            background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
+            
+            .icon-text {
+              font-size: 32px;
+            }
+            
+            .file-ext {
+              font-size: 12px;
+              font-weight: 600;
+              color: #1976D2;
+            }
           }
           
-          &.cad ion-icon {
-            color: #3182ce;
+          &.generic-icon {
+            background: linear-gradient(135deg, #F5F5F5, #E0E0E0);
+            
+            .icon-text {
+              font-size: 32px;
+            }
+            
+            .file-ext {
+              font-size: 12px;
+              font-weight: 600;
+              color: #616161;
+            }
           }
         }
-
+        
         .file-info {
+          padding: 8px;
+          
           .file-name {
             font-size: 12px;
-            font-weight: 500;
+            color: #333;
             white-space: nowrap;
             overflow: hidden;
             text-overflow: ellipsis;
             margin-bottom: 4px;
           }
           
-          .file-size {
-            font-size: 10px;
-            color: #a0aec0;
+          .file-meta {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            
+            .file-size {
+              font-size: 11px;
+              color: #999;
+            }
+            
+            .file-category {
+              font-size: 10px;
+              color: #666;
+              background: #F5F5F5;
+              padding: 2px 6px;
+              border-radius: 4px;
+              text-transform: uppercase;
+            }
           }
         }
-
+        
         .remove-btn {
           position: absolute;
           top: 8px;
           right: 8px;
-          width: 28px;
-          height: 28px;
-          background: rgba(0, 0, 0, 0.7);
-          color: white;
-          border: none;
+          width: 24px;
+          height: 24px;
           border-radius: 50%;
-          font-size: 18px;
-          cursor: pointer;
+          background: rgba(255,255,255,0.9);
+          border: 1px solid #ddd;
+          color: #666;
+          font-size: 16px;
           display: flex;
           align-items: center;
           justify-content: center;
+          cursor: pointer;
           opacity: 0;
-          transition: all 0.2s;
-          z-index: 10;
-
+          transition: opacity 0.2s, background 0.2s;
+          
           &:hover {
-            background: rgba(220, 38, 38, 0.9);
+            background: #ff4444;
+            color: white;
+            border-color: #ff4444;
           }
         }
-
-        &:hover .remove-btn {
-          opacity: 1;
-        }
       }
 
       .add-more {

+ 262 - 67
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -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进行真实分析)
    */

+ 24 - 6
src/modules/project/services/project-file.service.ts

@@ -451,15 +451,16 @@ export class ProjectFileService {
         if ((error?.status === 631 || error?.code === 631) && attempt === 1) {
           console.warn('⚠️ 检测到631错误(no such bucket),尝试使用Parse File备用方案...');
           try {
-            // 创建Parse File对象
-            const parseFile = new (Parse as any).File(file.name, file);
+            // 🔥 正确的Parse File创建方式
+            const base64 = await this.fileToBase64(file);
+            const parseFile = new Parse.File(file.name, { base64 });
             
-            // 保存文件到Parse服务器
-            const savedFile = await parseFile.save();
+            // 🔥 保存文件到Parse服务器(使用类型断言绕过TypeScript检查)
+            const savedFile = await (parseFile as any).save();
             
             // 获取保存后的URL和名称
-            const fileUrl = savedFile.url();
-            const fileName = savedFile.name();
+            const fileUrl = savedFile ? savedFile.url() : '';
+            const fileName = savedFile ? savedFile.name() : file.name;
             
             console.log('✅ Parse File上传成功:', fileUrl);
             
@@ -530,6 +531,23 @@ export class ProjectFileService {
     throw lastError || new Error('上传失败');
   }
 
+  /**
+   * 🔥 将文件转换为base64格式
+   */
+  private async fileToBase64(file: File): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = () => {
+        const base64 = reader.result as string;
+        // 移除data URL前缀,只保留base64数据
+        const base64Data = base64.split(',')[1];
+        resolve(base64Data);
+      };
+      reader.onerror = reject;
+      reader.readAsDataURL(file);
+    });
+  }
+
   /**
    * 🔥 清理文件名,创建新的File对象
    * 解决631存储错误和文件名兼容性问题