Przeglądaj źródła

fix: stage-requirement

Future 1 dzień temu
rodzic
commit
06119ab445

+ 7 - 6
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -162,7 +162,7 @@
                     @if (image.spaceId) {
                       <span class="badge badge-outline">{{ getProductDisplayNameById(image.spaceId || '') }}</span>
                     }
-                    @if (aiAnalysisResults.imageAnalysis?.find(a => a.imageId == image.id)) {
+                    @if (hasImageAnalysis(image.id)) {
                       <span class="badge badge-success">
                         <ion-icon name="sparkles"></ion-icon>
                         已分析
@@ -326,7 +326,7 @@
                       @if (file.spaceId) {
                         <span class="badge badge-outline">{{ getProductDisplayNameById(file.spaceId || '') }}</span>
                       }
-                      @if (aiAnalysisResults.cadAnalysis?.find(a => a.fileId == file.id)) {
+                      @if (hasCadAnalysis(file.id)) {
                         <span class="badge badge-success">
                           <ion-icon name="sparkles"></ion-icon>
                           已分析
@@ -930,14 +930,15 @@
                 <input
                   type="text"
                   class="form-input"
-                  [(ngModel)]="aiChatInput"
+                  [ngModel]="aiChatInput"
+                  (ngModelChange)="onAiChatInputChange($event)"
                   (keydown.enter)="sendAIChatMessage()"
                   placeholder="询问家装设计相关问题..."
                   [disabled]="aiAnalyzing" />
                 <button
                   class="btn btn-primary"
                   (click)="sendAIChatMessage()"
-                  [disabled]="!aiChatInput.trim() || aiAnalyzing">
+                  [disabled]="isAiChatSendDisabled()">
                   <ion-icon name="send"></ion-icon>
                 </button>
               </div>
@@ -953,7 +954,7 @@
         <button
           class="btn btn-outline"
           (click)="saveDraft()"
-          [disabled]="saving">
+          [disabled]="isSaving()">
           <ion-icon name="save"></ion-icon>
           保存草稿
         </button>
@@ -961,7 +962,7 @@
         <button
           class="btn btn-primary"
           (click)="submitRequirements()"
-          [disabled]="saving || !aiSolution">
+          [disabled]="isSubmitDisabled()">
           <ion-icon name="checkmark"></ion-icon>
           确认需求
         </button>

+ 26 - 1
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -818,7 +818,7 @@ ${context}
   private async convertImageToBase64(_imageUrl: string): Promise<string> {
     // 实际项目中,图片上传时应该已经保存了Base64格式
     // 这里返回一个模拟的Base64字符串
-    return 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/wA==';
+    return 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/wA==';
   }
 
   /**
@@ -1350,6 +1350,31 @@ ${context}
     return this.cadFiles.find(file => file.id === fileId);
   }
 
+  // === 模板辅助方法:避免在模板中使用箭头函数/复杂表达式 ===
+  onAiChatInputChange(value: string): void {
+    this.aiChatInput = value;
+  }
+
+  isAiChatSendDisabled(): boolean {
+    return this.aiAnalyzing || !this.aiChatInput?.trim();
+  }
+
+  isSaving(): boolean {
+    return this.saving;
+  }
+
+  isSubmitDisabled(): boolean {
+    return this.saving || this.generating || this.aiAnalyzing;
+  }
+
+  hasImageAnalysis(imageId: string): boolean {
+    return !!this.aiAnalysisResults.imageAnalysis?.some(x => x.imageId === imageId);
+  }
+
+  hasCadAnalysis(fileId: string): boolean {
+    return !!this.aiAnalysisResults.cadAnalysis?.some(x => x.fileId === fileId);
+  }
+
   /**
    * 获取风险等级样式类名
    */

+ 0 - 224
src/modules/project/services/upload.service.ts

@@ -1,224 +0,0 @@
-import { Injectable } from '@angular/core';
-import { NovaUploadService as FmodeUploadService } from 'fmode-ng/storage';
-
-/**
- * 文件上传服务
- * 用于处理图片、视频、文档等文件的上传
- * 基于 fmode-ng 的 NovaUploadService
- */
-@Injectable({
-  providedIn: 'root'
-})
-export class ProjectUploadService {
-  constructor(private uploadServ: FmodeUploadService) {}
-
-  /**
-   * 上传单个文件
-   * @param file File对象
-   * @param options 上传选项
-   * @returns 上传后的文件URL
-   */
-  async uploadFile(
-    file: File,
-    options?: {
-      onProgress?: (progress: number) => void;
-      compress?: boolean;
-      maxWidth?: number;
-      maxHeight?: number;
-    }
-  ): Promise<string> {
-    try {
-      let fileToUpload = file;
-
-      // 如果是图片且需要压缩
-      if (options?.compress && file.type.startsWith('image/')) {
-        fileToUpload = await this.compressImage(file, {
-          maxWidth: options.maxWidth || 1920,
-          maxHeight: options.maxHeight || 1920,
-          quality: 0.8
-        });
-      }
-
-      // 使用 fmode-ng 的上传服务
-      const fileResult = await this.uploadServ.upload(fileToUpload, (progress: any) => {
-        if (options?.onProgress && progress?.percent) {
-          options.onProgress(progress.percent);
-        }
-      });
-
-      // 返回文件URL
-      return fileResult.url || '';
-    } catch (error: any) {
-      console.error('文件上传失败:', error);
-      throw new Error('文件上传失败: ' + (error?.message || '未知错误'));
-    }
-  }
-
-  /**
-   * 批量上传文件
-   * @param files File数组
-   * @param options 上传选项
-   * @returns 上传后的文件URL数组
-   */
-  async uploadFiles(
-    files: File[],
-    options?: {
-      onProgress?: (current: number, total: number) => void;
-      compress?: boolean;
-    }
-  ): Promise<string[]> {
-    const urls: string[] = [];
-    const total = files.length;
-
-    for (let i = 0; i < files.length; i++) {
-      const url = await this.uploadFile(files[i], {
-        compress: options?.compress
-      });
-      urls.push(url);
-      options?.onProgress?.(i + 1, total);
-    }
-
-    return urls;
-  }
-
-  /**
-   * 压缩图片
-   * @param file 原始文件
-   * @param options 压缩选项
-   * @returns 压缩后的File对象
-   */
-  private compressImage(
-    file: File,
-    options: {
-      maxWidth: number;
-      maxHeight: number;
-      quality: number;
-    }
-  ): Promise<File> {
-    return new Promise((resolve, reject) => {
-      const reader = new FileReader();
-
-      reader.onload = (e) => {
-        const img = new Image();
-
-        img.onload = () => {
-          const canvas = document.createElement('canvas');
-          let { width, height } = img;
-
-          // 计算压缩后的尺寸
-          if (width > options.maxWidth || height > options.maxHeight) {
-            const ratio = Math.min(
-              options.maxWidth / width,
-              options.maxHeight / height
-            );
-            width = width * ratio;
-            height = height * ratio;
-          }
-
-          canvas.width = width;
-          canvas.height = height;
-
-          const ctx = canvas.getContext('2d');
-          ctx?.drawImage(img, 0, 0, width, height);
-
-          canvas.toBlob(
-            (blob) => {
-              if (blob) {
-                const compressedFile = new File([blob], file.name, {
-                  type: file.type,
-                  lastModified: Date.now()
-                });
-                resolve(compressedFile);
-              } else {
-                reject(new Error('图片压缩失败'));
-              }
-            },
-            file.type,
-            options.quality
-          );
-        };
-
-        img.onerror = () => reject(new Error('图片加载失败'));
-        img.src = e.target?.result as string;
-      };
-
-      reader.onerror = () => reject(new Error('文件读取失败'));
-      reader.readAsDataURL(file);
-    });
-  }
-
-  /**
-   * 生成缩略图
-   * @param file 原始图片文件
-   * @param size 缩略图尺寸
-   * @returns 缩略图URL
-   */
-  async generateThumbnail(file: File, size: number = 200): Promise<string> {
-    const thumbnailFile = await this.compressImage(file, {
-      maxWidth: size,
-      maxHeight: size,
-      quality: 0.7
-    });
-
-    return await this.uploadFile(thumbnailFile);
-  }
-
-  /**
-   * 从URL下载文件
-   * @param url 文件URL
-   * @param filename 保存的文件名
-   */
-  async downloadFile(url: string, filename: string): Promise<void> {
-    try {
-      const response = await fetch(url);
-      const blob = await response.blob();
-      const link = document.createElement('a');
-      link.href = URL.createObjectURL(blob);
-      link.download = filename;
-      link.click();
-      URL.revokeObjectURL(link.href);
-    } catch (error) {
-      console.error('文件下载失败:', error);
-      throw new Error('文件下载失败');
-    }
-  }
-
-  /**
-   * 验证文件类型
-   * @param file 文件
-   * @param allowedTypes 允许的MIME类型数组
-   * @returns 是否合法
-   */
-  validateFileType(file: File, allowedTypes: string[]): boolean {
-    return allowedTypes.some(type => {
-      if (type.endsWith('/*')) {
-        const prefix = type.replace('/*', '');
-        return file.type.startsWith(prefix);
-      }
-      return file.type === type;
-    });
-  }
-
-  /**
-   * 验证文件大小
-   * @param file 文件
-   * @param maxSizeMB 最大大小(MB)
-   * @returns 是否合法
-   */
-  validateFileSize(file: File, maxSizeMB: number): boolean {
-    const maxSizeBytes = maxSizeMB * 1024 * 1024;
-    return file.size <= maxSizeBytes;
-  }
-
-  /**
-   * 格式化文件大小
-   * @param bytes 字节数
-   * @returns 格式化后的字符串
-   */
-  formatFileSize(bytes: number): string {
-    if (bytes < 1024) return bytes + ' B';
-    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
-    if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
-    return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
-  }
-}