| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- 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图片分析(使用豆包1.6视觉识别)
- */
- private async startImageAnalysis(): Promise<void> {
- 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<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();
- }
- }
|