| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318 |
- 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<void>();
- @Output() confirm = new EventEmitter<UploadResult>();
- @Output() cancel = new EventEmitter<void>();
- // 上传文件列表
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<string, 'white_model' | 'soft_decor' | 'rendering' | 'post_process'> = {
- '白模': '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视觉分析(基于图片内容)
- * 专为交付执行阶段优化,根据图片真实内容判断阶段
- */
- private async startImageAnalysis(): Promise<void> {
- const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
- if (imageFiles.length === 0) {
- this.analysisComplete = true;
- return;
- }
- console.log('🤖 [真实AI分析] 开始分析...', {
- 文件数量: imageFiles.length,
- 目标空间: this.targetSpaceName,
- 目标阶段: this.targetStageName
- });
- // 🔥 不显示全屏遮罩,直接在表格中显示分析状态
- this.isAnalyzing = false;
- this.analysisComplete = false;
- this.analysisProgress = '正在启动AI视觉分析...';
- this.cdr.markForCheck();
- try {
- // 🚀 并行分析图片(提高速度,适合多图场景)
- this.analysisProgress = `正在分析 ${imageFiles.length} 张图片...`;
- this.cdr.markForCheck();
- const analysisPromises = imageFiles.map(async (uploadFile, i) => {
- // 更新文件状态为分析中
- uploadFile.status = 'analyzing';
- this.cdr.markForCheck();
- try {
- // 🤖 使用真实AI视觉分析(基于图片内容)
- console.log(`🤖 [${i + 1}/${imageFiles.length}] 开始AI视觉分析: ${uploadFile.name}`);
-
- if (!uploadFile.preview) {
- console.warn(`⚠️ ${uploadFile.name} 没有预览,跳过AI分析`);
- uploadFile.selectedStage = this.targetStageType || 'rendering';
- uploadFile.suggestedStage = this.targetStageType || 'rendering';
- uploadFile.status = 'pending';
- return;
- }
- // 🔥 调用真实的AI分析服务(analyzeImage)
- const analysisResult = await this.imageAnalysisService.analyzeImage(
- uploadFile.preview, // 图片预览URL(Base64或ObjectURL)
- uploadFile.file, // 文件对象
- (progress) => {
- // 在表格行内显示进度
- console.log(`[${i + 1}/${imageFiles.length}] ${progress}`);
- },
- true // 🔥 快速模式:跳过专业分析,加快速度
- );
- console.log(`✅ [${i + 1}/${imageFiles.length}] AI分析完成: ${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`
- });
- // 保存分析结果
- uploadFile.analysisResult = analysisResult;
- uploadFile.suggestedStage = analysisResult.suggestedStage;
- uploadFile.selectedStage = analysisResult.suggestedStage; // 🔥 自动使用AI建议的阶段
- uploadFile.status = 'pending';
- // 更新JSON预览数据
- this.updateJsonPreviewData(uploadFile, analysisResult);
- } catch (error: any) {
- console.error(`❌ AI分析 ${uploadFile.name} 失败:`, error);
- uploadFile.status = 'pending';
- // 分析失败时,使用拖拽目标阶段或默认值
- uploadFile.selectedStage = this.targetStageType || 'rendering';
- uploadFile.suggestedStage = this.targetStageType || 'rendering';
- }
- this.cdr.markForCheck();
- });
- // 🚀 并行等待所有分析完成
- await Promise.all(analysisPromises);
- this.analysisProgress = `✅ AI视觉分析完成!共分析 ${imageFiles.length} 张图片`;
- this.analysisComplete = true;
- console.log('✅ [真实AI分析] 所有文件分析完成(并行)');
- } catch (error) {
- console.error('❌ [真实AI分析] 过程出错:', error);
- this.analysisProgress = '分析过程出错';
- this.analysisComplete = true;
- } finally {
- this.isAnalyzing = false;
- setTimeout(() => {
- this.analysisProgress = '';
- this.cdr.markForCheck();
- }, 2000);
- this.cdr.markForCheck();
- }
- }
- /**
- * 🔥 基于文件名的快速分析(返回简化JSON)
- * 返回格式: { "space": "客厅", "stage": "软装", "confidence": 95 }
- */
- private async quickAnalyzeByFileName(file: File): Promise<{ space: string; stage: string; confidence: number }> {
- const fileName = file.name.toLowerCase();
-
- // 🔥 阶段判断(优先级:白膜 > 软装 > 渲染 > 后期)
- let stage = this.targetStageType || 'rendering'; // 🔥 优先使用目标阶段,否则默认渲染
- let confidence = 50; // 🔥 默认低置信度,提示用户需要确认
- let hasKeyword = false; // 🔥 标记是否匹配到关键词
-
- // 白膜关键词(最高优先级)- 解决白膜被误判问题
- if (fileName.includes('白模') || fileName.includes('bm') || fileName.includes('whitemodel') ||
- fileName.includes('模型') || fileName.includes('建模') || fileName.includes('白膜')) {
- stage = 'white_model';
- confidence = 95;
- hasKeyword = true;
- }
- // 软装关键词
- else if (fileName.includes('软装') || fileName.includes('rz') || fileName.includes('softdecor') ||
- fileName.includes('家具') || fileName.includes('配饰') || fileName.includes('陈设')) {
- stage = 'soft_decor';
- confidence = 92;
- hasKeyword = true;
- }
- // 后期关键词
- else if (fileName.includes('后期') || fileName.includes('hq') || fileName.includes('postprocess') ||
- fileName.includes('修图') || fileName.includes('精修') || fileName.includes('调色')) {
- stage = 'post_process';
- confidence = 90;
- hasKeyword = true;
- }
- // 渲染关键词
- else if (fileName.includes('渲染') || fileName.includes('xr') || fileName.includes('rendering') ||
- fileName.includes('效果图') || fileName.includes('render')) {
- stage = 'rendering';
- confidence = 88;
- hasKeyword = true;
- }
- // 🔥 如果没有匹配到关键词,但有目标阶段,使用目标阶段并提升置信度
- else if (this.targetStageType) {
- stage = this.targetStageType;
- confidence = 70; // 🔥 使用目标阶段时,置信度提升到70%
- console.log(`⚠️ [文件名分析] 文件名无关键词,使用拖拽目标阶段: ${this.targetStageName}`);
- }
- // 🔥 空间判断
- let space = this.targetSpaceName || '未知空间';
-
- if (fileName.includes('客厅') || fileName.includes('kt') || fileName.includes('living')) {
- space = '客厅';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('卧室') || fileName.includes('ws') || fileName.includes('bedroom') ||
- fileName.includes('主卧') || fileName.includes('次卧')) {
- space = '卧室';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('餐厅') || fileName.includes('ct') || fileName.includes('dining')) {
- space = '餐厅';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('厨房') || fileName.includes('cf') || fileName.includes('kitchen')) {
- space = '厨房';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('卫生间') || fileName.includes('wsj') || fileName.includes('bathroom') ||
- fileName.includes('浴室') || fileName.includes('厕所')) {
- space = '卫生间';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('书房') || fileName.includes('sf') || fileName.includes('study')) {
- space = '书房';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('阳台') || fileName.includes('yt') || fileName.includes('balcony')) {
- space = '阳台';
- confidence = Math.min(confidence + 5, 98);
- } else if (fileName.includes('玄关') || fileName.includes('xg') || fileName.includes('entrance')) {
- space = '玄关';
- confidence = Math.min(confidence + 5, 98);
- }
- console.log(`🔍 [文件名分析] ${fileName} → 空间: ${space}, 阶段: ${stage}, 置信度: ${confidence}%`);
- return {
- space,
- stage,
- confidence
- };
- }
- /**
- * 🔥 增强的快速分析(已废弃,仅保留作为参考)
- */
- private async startEnhancedMockAnalysis_DEPRECATED(): Promise<void> {
- 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();
- }
- }
|