import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, OnChanges, OnDestroy, SimpleChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ImageAnalysisService, ImageAnalysisResult } from '../../services/image-analysis.service'; /** * 上传文件接口(增强版) */ export interface UploadFile { file: File; id: string; name: string; size: number; type: string; preview?: string; fileUrl?: string; // 🔥 添加:上传后的文件URL status: 'pending' | 'analyzing' | 'uploading' | 'success' | 'error'; progress: number; error?: string; analysisResult?: ImageAnalysisResult; // 图片分析结果 suggestedStage?: string; // AI建议的阶段分类 suggestedSpace?: string; // AI建议的空间(暂未实现) imageLoadError?: boolean; // 🔥 图片加载错误标记 // 用户选择的空间和阶段(可修改) selectedSpace?: string; selectedStage?: string; } /** * 上传结果接口(增强版) */ export interface UploadResult { files: Array<{ file: UploadFile; spaceId: string; spaceName: string; stageType: string; stageName: string; // 新增:提交信息跟踪字段 analysisResult?: ImageAnalysisResult; submittedAt?: string; submittedBy?: string; submittedByName?: string; deliveryListId?: string; }>; } /** * 空间选项接口 */ export interface SpaceOption { id: string; name: string; } /** * 阶段选项接口 */ export interface StageOption { id: string; name: string; } @Component({ selector: 'app-drag-upload-modal', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './drag-upload-modal.component.html', styleUrls: ['./drag-upload-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class DragUploadModalComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { @Input() visible: boolean = false; @Input() droppedFiles: File[] = []; @Input() availableSpaces: SpaceOption[] = []; // 可用空间列表 @Input() availableStages: StageOption[] = []; // 可用阶段列表 @Input() targetSpaceId: string = ''; // 拖拽目标空间ID @Input() targetSpaceName: string = ''; // 拖拽目标空间名称 @Input() targetStageType: string = ''; // 拖拽目标阶段类型 @Input() targetStageName: string = ''; // 拖拽目标阶段名称 @Output() close = new EventEmitter(); @Output() confirm = new EventEmitter(); @Output() cancel = new EventEmitter(); // 上传文件列表 uploadFiles: UploadFile[] = []; // 上传状态 isUploading: boolean = false; uploadProgress: number = 0; uploadSuccess: boolean = false; uploadMessage: string = ''; // 图片分析状态 isAnalyzing: boolean = false; analysisProgress: string = ''; analysisComplete: boolean = false; // JSON格式预览模式 showJsonPreview: boolean = false; jsonPreviewData: any[] = []; // 🔥 图片查看器 viewingImage: UploadFile | null = null; constructor( private cdr: ChangeDetectorRef, private imageAnalysisService: ImageAnalysisService ) {} ngOnInit() { console.log('🚀 DragUploadModal 初始化', { visible: this.visible, droppedFilesCount: this.droppedFiles.length, targetSpace: this.targetSpaceName, targetStage: this.targetStageName }); } ngOnChanges(changes: SimpleChanges) { // 🔥 优化:只在关键变化时输出日志,避免控制台刷屏 if (changes['visible'] || changes['droppedFiles']) { console.log('🔄 ngOnChanges (关键变化)', { visible: this.visible, droppedFilesCount: this.droppedFiles.length }); } // 当弹窗显示或文件发生变化时处理 if (changes['visible'] && this.visible && this.droppedFiles.length > 0) { console.log('📎 弹窗显示,开始处理文件'); this.processDroppedFiles(); } else if (changes['droppedFiles'] && this.droppedFiles.length > 0 && this.visible) { console.log('📎 文件变化,开始处理文件'); this.processDroppedFiles(); } } ngAfterViewInit() { // AI分析将在图片预览生成完成后自动开始 // 不需要在这里手动启动 } /** * 处理拖拽的文件 */ private async processDroppedFiles() { console.log('📎 开始处理拖拽文件:', { droppedFilesCount: this.droppedFiles.length, files: this.droppedFiles.map(f => ({ name: f.name, type: f.type, size: f.size })) }); if (this.droppedFiles.length === 0) { console.warn('⚠️ 没有文件需要处理'); return; } this.uploadFiles = this.droppedFiles.map((file, index) => ({ file, id: `upload_${Date.now()}_${index}`, name: file.name, size: file.size, type: file.type, status: 'pending' as const, progress: 0, // 初始化选择的空间和阶段为空,等待AI分析或用户选择 selectedSpace: '', selectedStage: '' })); console.log('🖼️ 开始生成图片预览...', { uploadFilesCount: this.uploadFiles.length, imageFiles: this.uploadFiles.filter(f => this.isImageFile(f.file)).map(f => f.name) }); // 为图片文件生成预览 const previewPromises = []; for (const uploadFile of this.uploadFiles) { if (this.isImageFile(uploadFile.file)) { console.log(`🖼️ 开始为 ${uploadFile.name} 生成预览`); previewPromises.push(this.generatePreview(uploadFile)); } else { console.log(`📄 ${uploadFile.name} 不是图片文件,跳过预览生成`); } } try { // 等待所有预览生成完成 await Promise.all(previewPromises); console.log('✅ 所有图片预览生成完成'); // 检查预览生成结果 this.uploadFiles.forEach(file => { if (this.isImageFile(file.file)) { console.log(`🖼️ ${file.name} 预览状态:`, { hasPreview: !!file.preview, previewLength: file.preview ? file.preview.length : 0 }); } }); } catch (error) { console.error('❌ 图片预览生成失败:', error); } this.cdr.markForCheck(); // 预览生成完成后,延迟一点开始AI分析 setTimeout(() => { this.startAutoAnalysis(); }, 300); } /** * 生成图片预览 * 🔥 企业微信环境优先使用ObjectURL,避免CSP策略限制base64 */ private generatePreview(uploadFile: UploadFile): Promise { return new Promise((resolve, reject) => { try { // 🔥 企业微信环境检测 const isWxWork = this.isWxWorkEnvironment(); if (isWxWork) { // 🔥 企业微信环境:直接使用ObjectURL(更快、更可靠) try { const objectUrl = URL.createObjectURL(uploadFile.file); uploadFile.preview = objectUrl; console.log(`✅ 图片预览生成成功 (ObjectURL): ${uploadFile.name}`, { objectUrl: objectUrl, environment: 'wxwork' }); this.cdr.markForCheck(); resolve(); } catch (error) { console.error(`❌ ObjectURL生成失败: ${uploadFile.name}`, error); uploadFile.preview = undefined; this.cdr.markForCheck(); resolve(); } } else { // 🔥 非企业微信环境:使用base64 dataURL(兼容性更好) const reader = new FileReader(); reader.onload = (e) => { try { const result = e.target?.result as string; if (result && result.startsWith('data:image')) { uploadFile.preview = result; console.log(`✅ 图片预览生成成功 (Base64): ${uploadFile.name}`, { previewLength: result.length, isBase64: result.includes('base64'), mimeType: result.substring(5, result.indexOf(';')) }); this.cdr.markForCheck(); resolve(); } else { console.error(`❌ 预览数据格式错误: ${uploadFile.name}`, result?.substring(0, 50)); uploadFile.preview = undefined; // 清除无效预览 this.cdr.markForCheck(); resolve(); // 仍然resolve,不阻塞流程 } } catch (error) { console.error(`❌ 处理预览数据失败: ${uploadFile.name}`, error); uploadFile.preview = undefined; this.cdr.markForCheck(); resolve(); } }; reader.onerror = (error) => { console.error(`❌ FileReader读取失败: ${uploadFile.name}`, error); uploadFile.preview = undefined; this.cdr.markForCheck(); resolve(); // 不要reject,避免中断整个流程 }; reader.readAsDataURL(uploadFile.file); } } catch (error) { console.error(`❌ 图片预览生成初始化失败: ${uploadFile.name}`, error); uploadFile.preview = undefined; this.cdr.markForCheck(); resolve(); } }); } /** * 检查是否为图片文件 */ isImageFile(file: File): boolean { return file.type.startsWith('image/'); } /** * 自动开始AI分析 */ private async startAutoAnalysis(): Promise { console.log('🤖 开始自动AI分析...'); // 🔥 使用真实AI分析(豆包1.6视觉识别) await this.startImageAnalysis(); // 分析完成后,自动设置空间和阶段 this.autoSetSpaceAndStage(); } /** * 自动设置空间和阶段(增强版,支持AI智能分类) */ private autoSetSpaceAndStage(): void { for (const file of this.uploadFiles) { // 🤖 优先使用AI分析结果进行智能分类 if (file.analysisResult) { // 使用AI推荐的空间 if (this.targetSpaceId) { // 如果有指定的目标空间,使用指定空间 file.selectedSpace = this.targetSpaceId; console.log(`🎯 使用指定空间: ${this.targetSpaceName}`); } else { // 否则使用AI推荐的空间 const suggestedSpace = this.inferSpaceFromAnalysis(file.analysisResult); const spaceOption = this.availableSpaces.find(space => space.name === suggestedSpace || space.name.includes(suggestedSpace) ); if (spaceOption) { file.selectedSpace = spaceOption.id; console.log(`🤖 AI推荐空间: ${suggestedSpace}`); } else if (this.availableSpaces.length > 0) { file.selectedSpace = this.availableSpaces[0].id; } } // 🎯 使用AI推荐的阶段(这是核心功能) if (file.suggestedStage) { file.selectedStage = file.suggestedStage; console.log(`🤖 AI推荐阶段: ${file.name} -> ${file.suggestedStage} (置信度: ${file.analysisResult.content.confidence}%)`); } } else { // 如果没有AI分析结果,使用默认值 if (this.targetSpaceId) { file.selectedSpace = this.targetSpaceId; } else if (this.availableSpaces.length > 0) { file.selectedSpace = this.availableSpaces[0].id; } if (this.targetStageType) { file.selectedStage = this.targetStageType; } else { file.selectedStage = 'white_model'; // 默认白模阶段 } } } console.log('✅ AI智能分类完成'); this.cdr.markForCheck(); } /** * 生成JSON格式预览数据 */ private generateJsonPreview(): void { this.jsonPreviewData = this.uploadFiles.map((file, index) => ({ id: file.id, fileName: file.name, fileSize: this.getFileSizeDisplay(file.size), fileType: this.getFileTypeFromName(file.name), status: "待分析", space: "客厅", // 默认空间,后续AI分析会更新 stage: "白模", // 默认阶段,后续AI分析会更新 confidence: 0, preview: file.preview || null, analysis: { quality: "未知", dimensions: "分析中...", category: "识别中...", suggestedStage: "分析中..." } })); this.showJsonPreview = true; console.log('JSON预览数据:', this.jsonPreviewData); } /** * 根据文件名获取文件类型 */ private getFileTypeFromName(fileName: string): string { const ext = fileName.toLowerCase().split('.').pop(); switch (ext) { case 'jpg': case 'jpeg': case 'png': case 'gif': case 'webp': return '图片'; case 'pdf': return 'PDF文档'; case 'dwg': case 'dxf': return 'CAD图纸'; case 'skp': return 'SketchUp模型'; case 'max': return '3ds Max文件'; default: return '其他文件'; } } /** * 获取文件大小显示 */ getFileSizeDisplay(size: number): string { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; return `${(size / (1024 * 1024)).toFixed(1)} MB`; } /** * 获取文件类型图标 */ getFileTypeIcon(file: UploadFile): string { if (this.isImageFile(file.file)) return '🖼️'; if (file.name.endsWith('.pdf')) return '📄'; if (file.name.endsWith('.dwg') || file.name.endsWith('.dxf')) return '📐'; if (file.name.endsWith('.skp')) return '🏗️'; if (file.name.endsWith('.max')) return '🎨'; return '📁'; } /** * 移除文件 */ removeFile(fileId: string) { this.uploadFiles = this.uploadFiles.filter(f => f.id !== fileId); this.cdr.markForCheck(); } /** * 添加更多文件 */ addMoreFiles(event: Event) { const input = event.target as HTMLInputElement; if (input.files) { const newFiles = Array.from(input.files); const newUploadFiles = newFiles.map((file, index) => ({ file, id: `upload_${Date.now()}_${this.uploadFiles.length + index}`, name: file.name, size: file.size, type: file.type, status: 'pending' as const, progress: 0 })); // 为新的图片文件生成预览 newUploadFiles.forEach(uploadFile => { if (this.isImageFile(uploadFile.file)) { this.generatePreview(uploadFile); } }); this.uploadFiles.push(...newUploadFiles); this.cdr.markForCheck(); } // 重置input input.value = ''; } /** * 确认上传 */ async confirmUpload(): Promise { if (this.uploadFiles.length === 0 || this.isUploading) return; try { // 设置上传状态 this.isUploading = true; this.uploadSuccess = false; this.uploadMessage = '正在上传文件...'; this.cdr.markForCheck(); // 生成交付清单ID const deliveryListId = `delivery_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 自动确认所有已分析的文件 const result: UploadResult = { files: this.uploadFiles.map(file => ({ file: file, // 传递完整的UploadFile对象 spaceId: file.selectedSpace || (this.availableSpaces.length > 0 ? this.availableSpaces[0].id : ''), spaceName: this.getSpaceName(file.selectedSpace || (this.availableSpaces.length > 0 ? this.availableSpaces[0].id : '')), stageType: file.selectedStage || file.suggestedStage || 'white_model', stageName: this.getStageName(file.selectedStage || file.suggestedStage || 'white_model'), // 添加AI分析结果和提交信息 analysisResult: file.analysisResult, submittedAt: new Date().toISOString(), submittedBy: 'current_user', // TODO: 获取当前用户ID submittedByName: 'current_user_name', // TODO: 获取当前用户名称 deliveryListId: deliveryListId })) }; console.log('📤 确认上传文件:', result); // 发送上传事件 this.confirm.emit(result); // 模拟上传过程(实际上传完成后由父组件调用成功方法) await new Promise(resolve => setTimeout(resolve, 1000)); this.uploadSuccess = true; this.uploadMessage = `上传成功!共上传 ${this.uploadFiles.length} 个文件`; // 2秒后自动关闭弹窗 setTimeout(() => { this.close.emit(); }, 2000); } catch (error) { console.error('上传失败:', error); this.uploadMessage = '上传失败,请重试'; } finally { this.isUploading = false; this.cdr.markForCheck(); } } /** * 取消上传 */ cancelUpload(): void { this.cleanupObjectURLs(); this.cancel.emit(); } /** * 关闭弹窗 */ closeModal(): void { this.cleanupObjectURLs(); this.close.emit(); } /** * 🔥 清理ObjectURL资源 */ private cleanupObjectURLs(): void { this.uploadFiles.forEach(file => { if (file.preview && file.preview.startsWith('blob:')) { try { URL.revokeObjectURL(file.preview); } catch (error) { console.error(`❌ 释放ObjectURL失败: ${file.name}`, error); } } }); } /** * 阻止事件冒泡 */ preventDefault(event: Event): void { event.stopPropagation(); } /** * 🔥 增强的快速分析(推荐,更准确且不阻塞界面) */ private async startEnhancedMockAnalysis(): Promise { const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file)); if (imageFiles.length === 0) { this.analysisComplete = true; return; } console.log('🚀 开始增强快速分析...', { imageCount: imageFiles.length, targetSpace: this.targetSpaceName, targetStage: this.targetStageName }); // 不显示全屏覆盖层,直接在表格中显示分析状态 this.isAnalyzing = false; // 不显示全屏覆盖 this.analysisComplete = false; this.analysisProgress = '正在分析图片...'; this.cdr.markForCheck(); try { // 并行处理所有图片,提高速度 const analysisPromises = imageFiles.map(async (uploadFile, index) => { // 设置分析状态 uploadFile.status = 'analyzing'; this.cdr.markForCheck(); // 模拟短暂分析过程(200-500ms) await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300)); try { // 使用增强的分析算法 const analysisResult = this.generateEnhancedAnalysisResult(uploadFile.file); // 保存分析结果 uploadFile.analysisResult = analysisResult; uploadFile.suggestedStage = analysisResult.suggestedStage; uploadFile.selectedStage = analysisResult.suggestedStage; uploadFile.status = 'pending'; // 更新JSON预览数据 this.updateJsonPreviewData(uploadFile, analysisResult); console.log(`✨ ${uploadFile.name} 增强分析完成:`, { suggestedStage: analysisResult.suggestedStage, confidence: analysisResult.content.confidence, quality: analysisResult.quality.level, reason: analysisResult.suggestedReason }); } catch (error) { console.error(`分析 ${uploadFile.name} 失败:`, error); uploadFile.status = 'pending'; } this.cdr.markForCheck(); }); // 等待所有分析完成 await Promise.all(analysisPromises); this.analysisProgress = `分析完成!共分析 ${imageFiles.length} 张图片`; this.analysisComplete = true; console.log('✅ 所有图片增强分析完成'); } catch (error) { console.error('增强分析过程出错:', error); this.analysisProgress = '分析过程出错'; this.analysisComplete = true; } finally { this.isAnalyzing = false; setTimeout(() => { this.analysisProgress = ''; this.cdr.markForCheck(); }, 2000); this.cdr.markForCheck(); } } /** * 生成增强的分析结果(更准确的分类) */ private generateEnhancedAnalysisResult(file: File): ImageAnalysisResult { const fileName = file.name.toLowerCase(); const fileSize = file.size; // 获取目标空间信息 const targetSpaceName = this.targetSpaceName || '客厅'; console.log(`🔍 分析文件: ${fileName}`, { targetSpace: targetSpaceName, targetStage: this.targetStageName, fileSize: fileSize }); // 增强的阶段分类算法 let suggestedStage: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model'; let confidence = 75; let reason = '基于文件名和特征分析'; // 1. 文件名关键词分析 if (fileName.includes('白模') || fileName.includes('white') || fileName.includes('model') || fileName.includes('毛坯') || fileName.includes('空间') || fileName.includes('结构')) { suggestedStage = 'white_model'; confidence = 90; reason = '文件名包含白模相关关键词'; } else if (fileName.includes('软装') || fileName.includes('soft') || fileName.includes('decor') || fileName.includes('家具') || fileName.includes('furniture') || fileName.includes('装饰')) { suggestedStage = 'soft_decor'; confidence = 88; reason = '文件名包含软装相关关键词'; } else if (fileName.includes('渲染') || fileName.includes('render') || fileName.includes('效果') || fileName.includes('effect') || fileName.includes('光照')) { suggestedStage = 'rendering'; confidence = 92; reason = '文件名包含渲染相关关键词'; } else if (fileName.includes('后期') || fileName.includes('post') || fileName.includes('final') || fileName.includes('最终') || fileName.includes('完成') || fileName.includes('成品')) { suggestedStage = 'post_process'; confidence = 95; reason = '文件名包含后期处理相关关键词'; } // 2. 文件大小分析(辅助判断) if (fileSize > 5 * 1024 * 1024) { // 大于5MB if (suggestedStage === 'white_model') { // 大文件更可能是渲染或后期 suggestedStage = 'rendering'; confidence = Math.min(confidence + 10, 95); reason += ',大文件更可能是高质量渲染图'; } } // 3. 根据目标空间调整置信度 if (this.targetStageName) { const targetStageMap: Record = { '白模': 'white_model', '软装': 'soft_decor', '渲染': 'rendering', '后期': 'post_process' }; const targetStage = targetStageMap[this.targetStageName]; if (targetStage && targetStage === suggestedStage) { confidence = Math.min(confidence + 15, 98); reason += `,与目标阶段一致`; } } // 生成质量评分 const qualityScore = this.calculateQualityScore(suggestedStage, fileSize); const result: ImageAnalysisResult = { fileName: file.name, fileSize: file.size, dimensions: { width: 1920, height: 1080 }, quality: { score: qualityScore, level: this.getQualityLevel(qualityScore), sharpness: qualityScore + 5, brightness: qualityScore - 5, contrast: qualityScore, detailLevel: qualityScore >= 90 ? 'ultra_detailed' : qualityScore >= 75 ? 'detailed' : qualityScore >= 60 ? 'basic' : 'minimal', pixelDensity: qualityScore >= 90 ? 'ultra_high' : qualityScore >= 75 ? 'high' : qualityScore >= 60 ? 'medium' : 'low', textureQuality: qualityScore, colorDepth: qualityScore }, content: { category: suggestedStage, confidence: confidence, description: `${targetSpaceName}${this.getStageName(suggestedStage)}图`, tags: [this.getStageName(suggestedStage), targetSpaceName, '设计'], isArchitectural: true, hasInterior: true, hasFurniture: suggestedStage !== 'white_model', hasLighting: suggestedStage === 'rendering' || suggestedStage === 'post_process' }, technical: { format: file.type, colorSpace: 'sRGB', dpi: 72, aspectRatio: '16:9', megapixels: 2.07 }, suggestedStage: suggestedStage, suggestedReason: reason, analysisTime: 100, analysisDate: new Date().toISOString() }; return result; } /** * 计算质量评分 */ private calculateQualityScore(stage: string, fileSize: number): number { const baseScores = { 'white_model': 75, 'soft_decor': 82, 'rendering': 88, 'post_process': 95 }; let score = baseScores[stage as keyof typeof baseScores] || 75; // 根据文件大小调整 if (fileSize > 10 * 1024 * 1024) score += 5; // 大于10MB else if (fileSize < 1024 * 1024) score -= 5; // 小于1MB return Math.max(60, Math.min(100, score)); } /** * 获取质量等级 */ private getQualityLevel(score: number): 'low' | 'medium' | 'high' | 'ultra' { if (score >= 90) return 'ultra'; if (score >= 80) return 'high'; if (score >= 70) return 'medium'; return 'low'; } /** * 获取质量等级显示文本 */ getQualityLevelText(level: 'low' | 'medium' | 'high' | 'ultra'): string { const levelMap = { 'ultra': '优秀', 'high': '良好', 'medium': '中等', 'low': '较差' }; return levelMap[level] || '未知'; } /** * 获取质量等级颜色 */ getQualityLevelColor(level: 'low' | 'medium' | 'high' | 'ultra'): string { const colorMap = { 'ultra': '#52c41a', // 绿色 - 优秀 'high': '#1890ff', // 蓝色 - 良好 'medium': '#faad14', // 橙色 - 中等 'low': '#ff4d4f' // 红色 - 较差 }; return colorMap[level] || '#d9d9d9'; } /** * 🔥 真实AI图片分析(使用豆包1.6视觉识别) */ private async startImageAnalysis(): Promise { const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file)); if (imageFiles.length === 0) { this.analysisComplete = true; return; } // 🔥 不显示全屏遮罩,直接在表格中显示分析状态 this.isAnalyzing = false; // 改为false,避免全屏阻塞 this.analysisComplete = false; this.analysisProgress = '正在启动AI快速分析...'; this.cdr.markForCheck(); try { for (let i = 0; i < imageFiles.length; i++) { const uploadFile = imageFiles[i]; // 更新文件状态为分析中 uploadFile.status = 'analyzing'; this.analysisProgress = `正在分析 ${uploadFile.name} (${i + 1}/${imageFiles.length})`; this.cdr.markForCheck(); try { // 🔥 使用真正的AI分析(豆包1.6视觉识别) // 确保有预览URL,如果没有则跳过分析 // 🔥 使用真实的AI分析服务(快速模式) const analysisResult = await this.imageAnalysisService.analyzeImage( uploadFile.preview, // 图片预览URL(Base64或ObjectURL) uploadFile.file, // 文件对象 (progress) => { // 在表格行内显示进度,不阻塞界面 this.analysisProgress = `[${i + 1}/${imageFiles.length}] ${progress}`; this.cdr.markForCheck(); }, true // 🔥 快速模式:跳过专业分析 ); // 保存分析结果 uploadFile.analysisResult = analysisResult; uploadFile.suggestedStage = analysisResult.suggestedStage; // 自动设置为AI建议的阶段 uploadFile.selectedStage = analysisResult.suggestedStage; uploadFile.status = 'pending'; // 更新JSON预览数据 this.updateJsonPreviewData(uploadFile, analysisResult); // 🔥 详细日志输出 console.log(`✅ [${i + 1}/${imageFiles.length}] ${uploadFile.name}:`, { 建议阶段: analysisResult.suggestedStage, 置信度: `${analysisResult.content.confidence}%`, 空间类型: analysisResult.content.spaceType || '未识别', 有颜色: analysisResult.content.hasColor, 有纹理: analysisResult.content.hasTexture, 有灯光: analysisResult.content.hasLighting, 质量分数: analysisResult.quality.score, 分析耗时: `${analysisResult.analysisTime}ms` }); } catch (error: any) { console.error(`❌ 分析 ${uploadFile.name} 失败 - 详细错误:`, { 错误类型: error?.constructor?.name, 错误信息: error?.message, 错误代码: error?.code || error?.status, 文件名: uploadFile.name, 文件大小: uploadFile.file.size }); uploadFile.status = 'pending'; // 分析失败仍可上传 // 分析失败时,设置为默认的渲染阶段 uploadFile.selectedStage = 'rendering'; uploadFile.suggestedStage = 'rendering'; } this.cdr.markForCheck(); } this.analysisProgress = 'AI分析完成,已生成智能建议'; this.analysisComplete = true; } catch (error) { console.error('图片分析过程出错:', error); this.analysisProgress = '分析过程出错'; this.analysisComplete = true; } finally { this.isAnalyzing = false; setTimeout(() => { this.analysisProgress = ''; this.cdr.markForCheck(); }, 2000); this.cdr.markForCheck(); } } /** * 🔥 增强的快速分析(已废弃,仅保留作为参考) */ private async startEnhancedMockAnalysis_DEPRECATED(): Promise { const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file)); if (imageFiles.length === 0) { this.analysisComplete = true; return; } this.isAnalyzing = true; this.analysisComplete = false; this.analysisProgress = '准备分析图片...'; this.cdr.markForCheck(); try { for (let i = 0; i < imageFiles.length; i++) { const uploadFile = imageFiles[i]; // 更新文件状态为分析中 uploadFile.status = 'analyzing'; this.analysisProgress = `正在分析 ${uploadFile.name} (${i + 1}/${imageFiles.length})`; this.cdr.markForCheck(); try { // 使用预览URL进行分析 if (uploadFile.preview) { const analysisResult = await this.imageAnalysisService.analyzeImage( uploadFile.preview, uploadFile.file, (progress) => { this.analysisProgress = `${uploadFile.name}: ${progress}`; this.cdr.markForCheck(); } ); // 保存分析结果 uploadFile.analysisResult = analysisResult; uploadFile.suggestedStage = analysisResult.suggestedStage; // 自动设置为AI建议的阶段 uploadFile.selectedStage = analysisResult.suggestedStage; uploadFile.status = 'pending'; // 更新JSON预览数据 this.updateJsonPreviewData(uploadFile, analysisResult); console.log(`${uploadFile.name} 分析完成:`, analysisResult); } } catch (error) { console.error(`分析 ${uploadFile.name} 失败:`, error); uploadFile.status = 'pending'; // 分析失败仍可上传 } this.cdr.markForCheck(); } this.analysisProgress = '图片分析完成'; this.analysisComplete = true; } catch (error) { console.error('图片分析过程出错:', error); this.analysisProgress = '分析过程出错'; this.analysisComplete = true; } finally { this.isAnalyzing = false; setTimeout(() => { this.analysisProgress = ''; this.cdr.markForCheck(); }, 2000); this.cdr.markForCheck(); } } /** * 更新JSON预览数据 */ private updateJsonPreviewData(uploadFile: UploadFile, analysisResult: ImageAnalysisResult): void { const jsonItem = this.jsonPreviewData.find(item => item.id === uploadFile.id); if (jsonItem) { // 根据AI分析结果更新空间和阶段 jsonItem.stage = this.getSuggestedStageText(analysisResult.suggestedStage); jsonItem.space = this.inferSpaceFromAnalysis(analysisResult); jsonItem.confidence = analysisResult.content.confidence; jsonItem.status = "分析完成"; jsonItem.analysis = { quality: analysisResult.quality.level, dimensions: `${analysisResult.dimensions.width}x${analysisResult.dimensions.height}`, category: analysisResult.content.category, suggestedStage: this.getSuggestedStageText(analysisResult.suggestedStage) }; } } /** * 从AI分析结果推断空间类型 */ inferSpaceFromAnalysis(analysisResult: ImageAnalysisResult): string { const tags = analysisResult.content.tags; const description = analysisResult.content.description.toLowerCase(); // 基于标签和描述推断空间类型 if (tags.includes('客厅') || description.includes('客厅') || description.includes('living')) { return '客厅'; } else if (tags.includes('卧室') || description.includes('卧室') || description.includes('bedroom')) { return '卧室'; } else if (tags.includes('厨房') || description.includes('厨房') || description.includes('kitchen')) { return '厨房'; } else if (tags.includes('卫生间') || description.includes('卫生间') || description.includes('bathroom')) { return '卫生间'; } else if (tags.includes('餐厅') || description.includes('餐厅') || description.includes('dining')) { return '餐厅'; } else { return '客厅'; // 默认空间 } } /** * 获取分析状态显示文本 */ getAnalysisStatusText(file: UploadFile): string { if (file.status === 'analyzing') { return '分析中...'; } if (file.analysisResult) { const result = file.analysisResult; const categoryText = this.getSuggestedStageText(result.content.category); const qualityText = this.getQualityLevelText(result.quality.level); return `${categoryText} (${qualityText}, ${result.content.confidence}%置信度)`; } return ''; } /** * 获取建议阶段的显示文本 */ getSuggestedStageText(stageType: string): string { const stageMap: { [key: string]: string } = { 'white_model': '白模', 'soft_decor': '软装', 'rendering': '渲染', 'post_process': '后期' }; return stageMap[stageType] || stageType; } /** * 计算文件总大小 */ getTotalSize(): number { try { return this.uploadFiles?.reduce((sum, f) => sum + (f?.size || 0), 0) || 0; } catch { let total = 0; for (const f of this.uploadFiles || []) total += f?.size || 0; return total; } } /** * 更新文件的选择空间 */ updateFileSpace(fileId: string, spaceId: string) { const file = this.uploadFiles.find(f => f.id === fileId); if (file) { file.selectedSpace = spaceId; this.cdr.markForCheck(); } } /** * 更新文件的选择阶段 */ updateFileStage(fileId: string, stageId: string) { const file = this.uploadFiles.find(f => f.id === fileId); if (file) { file.selectedStage = stageId; this.cdr.markForCheck(); } } /** * 获取空间名称 */ getSpaceName(spaceId: string): string { const space = this.availableSpaces.find(s => s.id === spaceId); return space?.name || ''; } /** * 获取阶段名称 */ getStageName(stageId: string): string { const stage = this.availableStages.find(s => s.id === stageId); return stage?.name || ''; } /** * 获取文件总数 */ getFileCount(): number { return this.uploadFiles.length; } /** * 检查是否可以确认上传 */ canConfirm(): boolean { if (this.uploadFiles.length === 0) return false; if (this.isAnalyzing) return false; // 检查是否所有文件都已选择空间和阶段 return this.uploadFiles.every(f => f.selectedSpace && f.selectedStage); } /** * 获取分析进度百分比 */ getAnalysisProgressPercent(): number { if (this.uploadFiles.length === 0) return 0; const processedCount = this.uploadFiles.filter(f => f.status !== 'pending').length; return Math.round((processedCount / this.uploadFiles.length) * 100); } /** * 获取已分析文件数量 */ getAnalyzedFilesCount(): number { return this.uploadFiles.filter(f => f.analysisResult).length; } /** * 🔥 查看完整图片 */ viewFullImage(file: UploadFile): void { if (file.preview) { this.viewingImage = file; this.cdr.markForCheck(); console.log('🖼️ 打开图片查看器:', file.name); } } /** * 🔥 关闭图片查看器 */ closeImageViewer(): void { this.viewingImage = null; this.cdr.markForCheck(); console.log('❌ 关闭图片查看器'); } /** * 🔥 图片加载错误处理 */ onImageError(event: Event, file: UploadFile): void { console.error('❌ 图片加载失败:', file.name, { preview: file.preview ? file.preview.substring(0, 100) + '...' : 'null', fileUrl: file.fileUrl, isWxWork: this.isWxWorkEnvironment() }); // 🔥 设置错误标记,让HTML显示placeholder而不是破损图标 file.imageLoadError = true; // 标记视图需要更新 this.cdr.markForCheck(); // 在企业微信环境中,尝试使用ObjectURL作为备选方案 if (this.isWxWorkEnvironment() && this.isImageFile(file.file)) { try { const objectUrl = URL.createObjectURL(file.file); // 清除错误标记 file.imageLoadError = false; file.preview = objectUrl; console.log('🔄 使用ObjectURL作为预览:', objectUrl); this.cdr.markForCheck(); } catch (error) { console.error('❌ 生成ObjectURL失败:', error); file.imageLoadError = true; // 确保显示placeholder this.cdr.markForCheck(); } } } /** * 检测是否在企业微信环境 */ private isWxWorkEnvironment(): boolean { const ua = navigator.userAgent.toLowerCase(); return ua.includes('wxwork') || ua.includes('micromessenger'); } /** * 🔥 组件销毁时清理ObjectURL,避免内存泄漏 */ ngOnDestroy(): void { console.log('🧹 组件销毁,清理ObjectURL资源...'); this.cleanupObjectURLs(); } }