|
|
@@ -1,34 +1,33 @@
|
|
|
-import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core';
|
|
|
+import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
-import { WxworkAuth, FmodeParse, NovaStorage } from 'fmode-ng/core';
|
|
|
+import { WxworkAuth, FmodeParse } from 'fmode-ng/core';
|
|
|
import { IonIcon } from '@ionic/angular/standalone';
|
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
|
import { ProductSpaceService, Project } from '../../../services/product-space.service';
|
|
|
import { ProjectFileService } from '../../../services/project-file.service';
|
|
|
import { DesignAnalysisAIService } from '../../../services/design-analysis-ai.service';
|
|
|
-import { ColorGetDialogComponent } from '../../../components/color-get/color-get-dialog.component';
|
|
|
-import { completionJSON } from 'fmode-ng/core';
|
|
|
import { addIcons } from 'ionicons';
|
|
|
import { add, chevronDown, colorPalette, send, sparkles, trash } from 'ionicons/icons';
|
|
|
-import { generatePhaseDeadlines } from '../../../../../app/utils/phase-deadline.utils';
|
|
|
-import { addDays, normalizeDateInput } from '../../../../../app/utils/date-utils';
|
|
|
-
|
|
|
-// 初始化Parse对象
|
|
|
-const Parse = FmodeParse.with('nova');
|
|
|
+import { AiDesignAnalysisComponent } from './components/ai-design-analysis/ai-design-analysis.component';
|
|
|
+import { SpaceRequirementItemComponent } from './components/space-requirement-item/space-requirement-item.component';
|
|
|
|
|
|
addIcons({
|
|
|
add,sparkles,colorPalette,trash,chevronDown,send
|
|
|
})
|
|
|
-/**
|
|
|
- * 确认需求阶段组件 - Product表统一空间管理
|
|
|
- */
|
|
|
+
|
|
|
@Component({
|
|
|
selector: 'app-stage-requirements',
|
|
|
standalone: true,
|
|
|
- imports: [CommonModule, FormsModule, ReactiveFormsModule, IonIcon],
|
|
|
- providers: [],
|
|
|
+ imports: [
|
|
|
+ CommonModule,
|
|
|
+ FormsModule,
|
|
|
+ ReactiveFormsModule,
|
|
|
+ IonIcon,
|
|
|
+ AiDesignAnalysisComponent,
|
|
|
+ SpaceRequirementItemComponent
|
|
|
+ ],
|
|
|
templateUrl: './stage-requirements.component.html',
|
|
|
styleUrls: ['./stage-requirements.component.scss'],
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
@@ -39,91 +38,31 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
@Input() currentUser: any = null;
|
|
|
@Input() canEdit: boolean = true;
|
|
|
|
|
|
- // 路由参数
|
|
|
cid: string = '';
|
|
|
projectId: string = '';
|
|
|
|
|
|
- // Product-based 空间管理
|
|
|
projectProducts: Project[] = [];
|
|
|
isMultiProductProject: boolean = false;
|
|
|
activeProductId: string = '';
|
|
|
- selectedProductIds: string[] = [];
|
|
|
-
|
|
|
- // 需求分段管理
|
|
|
- requirementsSegment: string = 'global'; // global | spaces | cross-space
|
|
|
-
|
|
|
- // 产品更新事件监听器
|
|
|
+
|
|
|
private productUpdateListener: any = null;
|
|
|
-
|
|
|
- // 空间折叠状态管理
|
|
|
expandedSpaces: Set<string> = new Set();
|
|
|
-
|
|
|
- // 空间特殊需求数据
|
|
|
spaceSpecialRequirements: { [spaceId: string]: string } = {};
|
|
|
-
|
|
|
- // 图片类型定义
|
|
|
- imageTypes = [
|
|
|
- { id: 'all', name: '全部' },
|
|
|
- { id: 'soft_decor', name: '软装' },
|
|
|
- { id: 'hard_decor', name: '硬装' },
|
|
|
- { id: 'cad', name: 'CAD' },
|
|
|
- { id: 'other', name: '其他' }
|
|
|
- ];
|
|
|
-
|
|
|
- // 当前选中的图片类型标签(按空间ID)
|
|
|
- activeImageTab: { [spaceId: string]: string } = {};
|
|
|
-
|
|
|
- // 拖拽相关
|
|
|
- isDragOver: boolean = false;
|
|
|
- dragOverSpaceId: string = '';
|
|
|
-
|
|
|
- // AI分析结果存储(按空间ID)
|
|
|
analysisResultsBySpace: { [spaceId: string]: any[] } = {};
|
|
|
|
|
|
- // 全局需求
|
|
|
- globalRequirements = {
|
|
|
- stylePreference: '',
|
|
|
- colorScheme: {
|
|
|
- primary: '',
|
|
|
- secondary: '',
|
|
|
- accent: '',
|
|
|
- atmosphere: ''
|
|
|
- },
|
|
|
- overallBudget: {
|
|
|
- min: 0,
|
|
|
- max: 0
|
|
|
- },
|
|
|
- timeline: '',
|
|
|
- qualityLevel: 'standard', // standard | premium | luxury
|
|
|
- specialRequirements: '',
|
|
|
- environmentRequirements: {
|
|
|
- lighting: '',
|
|
|
- ventilation: '',
|
|
|
- noise: '',
|
|
|
- temperature: ''
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 空间需求数据
|
|
|
- spaceRequirements: any[] = [];
|
|
|
-
|
|
|
- // 跨空间需求
|
|
|
- crossSpaceRequirements: any[] = [];
|
|
|
-
|
|
|
- // 参考图片(支持按空间分类)
|
|
|
+ // File Data
|
|
|
referenceImages: Array<{
|
|
|
id: string;
|
|
|
url: string;
|
|
|
name: string;
|
|
|
- type: string; // style | space | material
|
|
|
+ type: string;
|
|
|
uploadTime: Date;
|
|
|
description?: string;
|
|
|
spaceId?: string;
|
|
|
tags: string[];
|
|
|
- projectFile?: any; // ProjectFile对象引用
|
|
|
+ projectFile?: any;
|
|
|
}> = [];
|
|
|
|
|
|
- // CAD文件
|
|
|
cadFiles: Array<{
|
|
|
id: string;
|
|
|
url: string;
|
|
|
@@ -131,123 +70,16 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
uploadTime: Date;
|
|
|
size: number;
|
|
|
spaceId?: string;
|
|
|
- projectFile?: any; // ProjectFile对象引用
|
|
|
- }> = [];
|
|
|
-
|
|
|
- // AI生成的方案
|
|
|
- aiSolution: {
|
|
|
- generated: boolean;
|
|
|
- content: string;
|
|
|
- spaces: Array<{
|
|
|
- id: string;
|
|
|
- name: string;
|
|
|
- type: string;
|
|
|
- styleDescription: string;
|
|
|
- colorPalette: string[];
|
|
|
- materials: string[];
|
|
|
- furnitureRecommendations: string[];
|
|
|
- estimatedCost?: number;
|
|
|
- timeline?: string;
|
|
|
- }>;
|
|
|
- estimatedCost: number;
|
|
|
- timeline: string;
|
|
|
- crossSpaceCoordination?: {
|
|
|
- styleConsistency: {
|
|
|
- description: string;
|
|
|
- keyElements: string[];
|
|
|
- };
|
|
|
- functionalFlow: {
|
|
|
- description: string;
|
|
|
- considerations: string[];
|
|
|
- };
|
|
|
- timelineCoordination: {
|
|
|
- description: string;
|
|
|
- strategy: string;
|
|
|
- };
|
|
|
- };
|
|
|
- } | null = null;
|
|
|
-
|
|
|
- // AI分析相关数据
|
|
|
- aiAnalysisResults: {
|
|
|
- imageAnalysis?: Array<{
|
|
|
- imageId: string;
|
|
|
- styleElements: string[];
|
|
|
- colorPalette: string[];
|
|
|
- materialAnalysis: string[];
|
|
|
- layoutFeatures: string[];
|
|
|
- mood: string;
|
|
|
- confidence: number;
|
|
|
- }>;
|
|
|
- cadAnalysis?: Array<{
|
|
|
- fileId: string;
|
|
|
- spaceStructure: any;
|
|
|
- dimensions: any;
|
|
|
- constraints: string[];
|
|
|
- opportunities: string[];
|
|
|
- }>;
|
|
|
- comprehensiveAnalysis?: {
|
|
|
- overallStyle: string;
|
|
|
- colorScheme: any;
|
|
|
- materialRecommendations: string[];
|
|
|
- layoutOptimization: string[];
|
|
|
- budgetAssessment: any;
|
|
|
- timeline: string;
|
|
|
- riskFactors: string[];
|
|
|
- };
|
|
|
- } = {};
|
|
|
-
|
|
|
- // AI设计分析对话相关
|
|
|
- showAIDesignDialog: boolean = false;
|
|
|
- aiDesignDialogVisible = false;
|
|
|
- aiDesignUploading: boolean = false;
|
|
|
- aiDesignCurrentSpace: any = null;
|
|
|
- aiDesignUploadedImages: string[] = [];
|
|
|
- aiDesignUploadedFiles: any[] = []; // 存储文件信息(类型、名称等)
|
|
|
- aiDesignTextDescription = '';
|
|
|
- aiDesignAnalyzing = false;
|
|
|
- aiDesignAnalysisResult: any = null;
|
|
|
- aiDesignGeneratingReport = false;
|
|
|
- aiDesignReport = '';
|
|
|
- aiDesignReportConfirmed = false;
|
|
|
- aiDesignDragOver = false; // 拖拽状态
|
|
|
- exportingWord = false; // 导出Word状态
|
|
|
-
|
|
|
- // AI对话系统
|
|
|
- aiChatMessages: Array<{
|
|
|
- id: string;
|
|
|
- role: 'user' | 'assistant';
|
|
|
- content: string;
|
|
|
- timestamp: Date;
|
|
|
- images?: string[];
|
|
|
- isLoading?: boolean;
|
|
|
- isStreaming?: boolean; // 流式输出中
|
|
|
- liked?: boolean;
|
|
|
- disliked?: boolean;
|
|
|
+ projectFile?: any;
|
|
|
}> = [];
|
|
|
- aiChatInput: string = '';
|
|
|
- deepThinkingEnabled: boolean = false;
|
|
|
- expandedDimensions: Set<string> = new Set(); // 管理维度折叠状态
|
|
|
- @ViewChild('chatMessagesWrapper') chatMessagesWrapper!: ElementRef;
|
|
|
- @ViewChild('chatInput') chatInputElement!: ElementRef;
|
|
|
-
|
|
|
- // AI分析状态
|
|
|
- aiAnalyzing: boolean = false;
|
|
|
- aiAnalyzingImages: boolean = false;
|
|
|
- aiAnalyzingCAD: boolean = false;
|
|
|
- aiGeneratingComprehensive: boolean = false;
|
|
|
- showAIChat: boolean = false;
|
|
|
|
|
|
- // AI分析配置
|
|
|
- private readonly AI_MODEL = 'fmode-1.6-cn';
|
|
|
+ // AI Analysis Data
|
|
|
+ aiAnalysisResults: any = {};
|
|
|
|
|
|
- // 加载状态
|
|
|
loading: boolean = true;
|
|
|
uploading: boolean = false;
|
|
|
- generating: boolean = false;
|
|
|
- saving: boolean = false;
|
|
|
|
|
|
- // 模板引用变量
|
|
|
- @ViewChild('chatMessages') chatMessagesContainer!: ElementRef;
|
|
|
+ @ViewChild(AiDesignAnalysisComponent) aiDesignComponent!: AiDesignAnalysisComponent;
|
|
|
|
|
|
constructor(
|
|
|
private route: ActivatedRoute,
|
|
|
@@ -259,145 +91,67 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
) {}
|
|
|
|
|
|
async ngOnInit() {
|
|
|
- console.log('🚀 [确认需求] ngOnInit 开始');
|
|
|
-
|
|
|
- // 从父路由获取参数
|
|
|
this.cid = this.route.parent?.snapshot.paramMap.get('cid') || this.cid;
|
|
|
this.projectId = this.route.parent?.snapshot.paramMap.get('projectId') || this.projectId;
|
|
|
|
|
|
- console.log('📋 [确认需求] 路由参数获取结果:', {
|
|
|
- cid: this.cid,
|
|
|
- projectId: this.projectId,
|
|
|
- '有cid': !!this.cid,
|
|
|
- '有projectId': !!this.projectId,
|
|
|
- '完整路由': window.location.pathname
|
|
|
- });
|
|
|
-
|
|
|
- // 若无当前用户,从企业微信获取并计算权限
|
|
|
try {
|
|
|
if (!this.currentUser && this.cid) {
|
|
|
- console.log('🔑 [确认需求] 开始获取当前用户...');
|
|
|
const wx = new WxworkAuth({ cid: this.cid, appId: 'crm' });
|
|
|
this.currentUser = await wx.currentProfile();
|
|
|
- console.log('✅ [确认需求] 当前用户获取成功:', this.currentUser?.get?.('name'));
|
|
|
}
|
|
|
|
|
|
const role = this.currentUser?.get?.('roleName') || '';
|
|
|
-
|
|
|
- console.log('🔍 确认需求阶段权限检查:', {
|
|
|
- '当前用户': this.currentUser?.get?.('name') || 'Unknown',
|
|
|
- '用户角色': role,
|
|
|
- '有currentUser': !!this.currentUser,
|
|
|
- '原始canEdit': this.canEdit,
|
|
|
- 'cid': this.cid,
|
|
|
- 'projectId': this.projectId
|
|
|
- });
|
|
|
-
|
|
|
- // 🔥 关键修复:只有当成功获取到用户且角色有效时,才覆盖canEdit
|
|
|
if (this.currentUser && role) {
|
|
|
const calculatedCanEdit = ['客服', '组员', '组长', '管理员', '设计师', '客服主管'].includes(role);
|
|
|
this.canEdit = calculatedCanEdit;
|
|
|
- console.log('✅ 根据角色计算canEdit:', calculatedCanEdit, '角色:', role);
|
|
|
- } else {
|
|
|
- // 如果没有用户信息或角色为空,保留默认值true
|
|
|
- console.log('⚠️ 未获取到用户角色,保留默认canEdit:', this.canEdit);
|
|
|
}
|
|
|
-
|
|
|
- console.log('✅ 最终canEdit值:', this.canEdit);
|
|
|
} catch (e) {
|
|
|
- console.error('❌ 权限检查失败,保留默认canEdit:', this.canEdit, e);
|
|
|
+ console.error('权限检查失败', e);
|
|
|
}
|
|
|
|
|
|
await this.loadData();
|
|
|
-
|
|
|
- // 监听产品更新事件
|
|
|
this.setupProductUpdateListener();
|
|
|
-
|
|
|
- console.log('🏁 [确认需求] ngOnInit 完成,最终状态:', {
|
|
|
- 'this.project': !!this.project,
|
|
|
- 'this.currentUser': !!this.currentUser,
|
|
|
- 'this.canEdit': this.canEdit,
|
|
|
- 'projectId': this.project?.id
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 🔥 设置产品更新监听器
|
|
|
- */
|
|
|
private setupProductUpdateListener(): void {
|
|
|
this.productUpdateListener = (event: any) => {
|
|
|
const detail = event.detail || {};
|
|
|
if (detail.projectId === this.projectId) {
|
|
|
- console.log(`🔄 [确认需求] 检测到产品${detail.action === 'add' ? '添加' : detail.action === 'edit' ? '编辑' : '删除'}事件,重新加载空间...`);
|
|
|
this.loadData();
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
document.addEventListener('product-spaces-updated', this.productUpdateListener);
|
|
|
- console.log('✅ [确认需求] 已设置产品更新监听器');
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 组件销毁时清理监听器
|
|
|
- */
|
|
|
ngOnDestroy(): void {
|
|
|
if (this.productUpdateListener) {
|
|
|
document.removeEventListener('product-spaces-updated', this.productUpdateListener);
|
|
|
- console.log('🧹 [确认需求] 已清理产品更新监听器');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 加载数据
|
|
|
- */
|
|
|
async loadData() {
|
|
|
- console.log('📦 [确认需求] loadData 开始');
|
|
|
try {
|
|
|
this.loading = true;
|
|
|
|
|
|
- // 🔥 关键修复:如果没有project对象,从projectId加载(参考售后归档组件)
|
|
|
if (!this.project && this.projectId) {
|
|
|
- console.log('📥 [确认需求] 从projectId加载项目信息...');
|
|
|
const Parse = FmodeParse.with('nova');
|
|
|
const query = new Parse.Query('Project');
|
|
|
query.include('contact', 'assignee', 'department');
|
|
|
try {
|
|
|
this.project = await query.get(this.projectId);
|
|
|
- console.log('✅ [确认需求] 项目信息加载成功:', {
|
|
|
- projectId: this.project.id,
|
|
|
- name: this.project.get('name'),
|
|
|
- currentStage: this.project.get('currentStage')
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- console.error('❌ [确认需求] 加载项目失败:', error);
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('加载项目失败:', error);
|
|
|
window?.fmode?.alert('加载项目失败: ' + (error.message || '未知错误'));
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!this.project) {
|
|
|
- console.warn('⚠️ [确认需求] 项目对象为空,无法加载数据');
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!this.project) return;
|
|
|
|
|
|
- console.log('✅ [确认需求] 项目对象已准备好:', {
|
|
|
- projectId: this.project.id,
|
|
|
- name: this.project.get?.('name'),
|
|
|
- currentStage: this.project.get?.('currentStage')
|
|
|
- });
|
|
|
-
|
|
|
- // ✅ 使用统一空间管理加载Product数据
|
|
|
const projectId = this.projectId || this.project?.id || '';
|
|
|
if (projectId) {
|
|
|
- console.log('🔄 [需求确认] 开始加载项目空间...');
|
|
|
-
|
|
|
- // 从统一存储获取空间数据
|
|
|
const unifiedSpaces = await this.productSpaceService.getUnifiedSpaceData(projectId);
|
|
|
-
|
|
|
if (unifiedSpaces.length > 0) {
|
|
|
- console.log(`✅ [需求确认] 从统一存储加载到 ${unifiedSpaces.length} 个空间`);
|
|
|
-
|
|
|
- // 转换为组件使用的格式
|
|
|
this.projectProducts = unifiedSpaces.map(space => ({
|
|
|
id: space.id,
|
|
|
name: space.name,
|
|
|
@@ -415,77 +169,31 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
designerId: space.designerId
|
|
|
}));
|
|
|
} else {
|
|
|
- console.log('⚠️ [需求确认] 没有找到统一空间数据,尝试从Product表加载...');
|
|
|
this.projectProducts = await this.productSpaceService.getProjectProductSpaces(projectId);
|
|
|
}
|
|
|
-
|
|
|
- console.log(`✅ [需求确认] 空间加载完成,共 ${this.projectProducts.length} 个空间`);
|
|
|
}
|
|
|
|
|
|
- // 防御性去重:避免同名空间重复展示
|
|
|
this.projectProducts = this.projectProducts.filter((p, idx, arr) => {
|
|
|
const key = (p.name || '').trim().toLowerCase();
|
|
|
return arr.findIndex(x => (x.name || '').trim().toLowerCase() === key) === idx;
|
|
|
});
|
|
|
this.isMultiProductProject = this.projectProducts.length > 1;
|
|
|
|
|
|
- // 如果有产品,默认选中第一个
|
|
|
if (this.projectProducts.length > 0 && !this.activeProductId) {
|
|
|
this.activeProductId = this.projectProducts[0].id;
|
|
|
}
|
|
|
|
|
|
- // 初始化每个空间的图片标签为"全部"
|
|
|
for (const product of this.projectProducts) {
|
|
|
- if (!this.activeImageTab[product.id]) {
|
|
|
- this.activeImageTab[product.id] = 'all';
|
|
|
- }
|
|
|
- // 初始化空间特殊需求(从项目数据中加载或使用默认值)
|
|
|
if (!this.spaceSpecialRequirements[product.id]) {
|
|
|
const projectData = this.project?.get('data') || {};
|
|
|
const spaceRequirements = projectData.spaceRequirements || {};
|
|
|
this.spaceSpecialRequirements[product.id] = spaceRequirements[product.id] || '';
|
|
|
-
|
|
|
- // 如果没有数据,添加示例数据用于测试
|
|
|
- if (!this.spaceSpecialRequirements[product.id]) {
|
|
|
- const spaceName = product.name || '空间';
|
|
|
- this.spaceSpecialRequirements[product.id] = `${spaceName}需要充足的储物空间,采用现代简约风格`;
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 模拟加载需求数据
|
|
|
- this.globalRequirements = {
|
|
|
- stylePreference: '现代简约',
|
|
|
- colorScheme: {
|
|
|
- primary: '#ffffff',
|
|
|
- secondary: '#f5f5f5',
|
|
|
- accent: '#3880ff',
|
|
|
- atmosphere: '温馨'
|
|
|
- },
|
|
|
- overallBudget: { min: 15, max: 25 },
|
|
|
- timeline: '45天',
|
|
|
- qualityLevel: 'premium',
|
|
|
- specialRequirements: '需要充足的储物空间',
|
|
|
- environmentRequirements: {
|
|
|
- lighting: '明亮自然',
|
|
|
- ventilation: '良好通风',
|
|
|
- noise: '隔音处理',
|
|
|
- temperature: '恒温控制'
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 加载已上传的文件(参考图片和CAD文件)
|
|
|
await this.loadProjectFiles();
|
|
|
-
|
|
|
- // 加载已保存的AI分析结果
|
|
|
await this.loadAnalysisResults();
|
|
|
-
|
|
|
- // 自动选择第一个空间用于AI分析
|
|
|
- if (this.projectProducts.length > 0 && !this.aiDesignCurrentSpace) {
|
|
|
- this.aiDesignCurrentSpace = this.projectProducts[0];
|
|
|
- console.log('✨ 自动选择第一个空间用于AI分析:', this.aiDesignCurrentSpace.name);
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
this.cdr.markForCheck();
|
|
|
|
|
|
} catch (err) {
|
|
|
@@ -495,289 +203,164 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // ===== 多空间需求管理方法 =====
|
|
|
-
|
|
|
- /**
|
|
|
- * 切换需求分段
|
|
|
- */
|
|
|
- onRequirementsSegmentChange(event: any): void {
|
|
|
- this.requirementsSegment = event.detail.value;
|
|
|
- this.cdr.markForCheck();
|
|
|
+ async loadProjectFiles() {
|
|
|
+ try {
|
|
|
+ const projectId = this.projectId || this.project?.id;
|
|
|
+ if (!projectId) return;
|
|
|
+
|
|
|
+ const files = await this.projectFileService.getProjectFiles(projectId);
|
|
|
+
|
|
|
+ this.referenceImages = [];
|
|
|
+ this.cadFiles = [];
|
|
|
+
|
|
|
+ files.forEach((file: any) => {
|
|
|
+ const type = file.get('type');
|
|
|
+ const spaceId = file.get('spaceId');
|
|
|
+ const metadata = file.get('metadata') || {};
|
|
|
+
|
|
|
+ if (type === 'reference_image' || metadata.uploadedFor === 'requirements_analysis') {
|
|
|
+ this.referenceImages.push({
|
|
|
+ id: file.id,
|
|
|
+ url: file.get('fileUrl'),
|
|
|
+ name: file.get('fileName'),
|
|
|
+ type: metadata.imageType || 'other',
|
|
|
+ uploadTime: file.createdAt,
|
|
|
+ spaceId: spaceId,
|
|
|
+ tags: metadata.tags || [],
|
|
|
+ projectFile: file
|
|
|
+ });
|
|
|
+ } else if (type === 'cad_file' || metadata.uploadedFor === 'requirements_cad') {
|
|
|
+ this.cadFiles.push({
|
|
|
+ id: file.id,
|
|
|
+ url: file.get('fileUrl'),
|
|
|
+ name: file.get('fileName'),
|
|
|
+ uploadTime: file.createdAt,
|
|
|
+ size: 0,
|
|
|
+ spaceId: spaceId,
|
|
|
+ projectFile: file
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to load files', e);
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 选择需求分段
|
|
|
- */
|
|
|
- selectRequirementsSegment(segment: string): void {
|
|
|
- this.requirementsSegment = segment;
|
|
|
- this.cdr.markForCheck();
|
|
|
+
|
|
|
+ async loadAnalysisResults() {
|
|
|
+ const projectData = this.project?.get('data') || {};
|
|
|
+ if (projectData.requirementsAnalysis) {
|
|
|
+ Object.keys(projectData.requirementsAnalysis).forEach(spaceId => {
|
|
|
+ this.analysisResultsBySpace[spaceId] = projectData.requirementsAnalysis[spaceId].analysisResults || [];
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 选择产品空间
|
|
|
- */
|
|
|
- selectProduct(productId: string): void {
|
|
|
- this.activeProductId = productId;
|
|
|
- this.cdr.markForCheck();
|
|
|
+ // Space List Helpers
|
|
|
+ getSpaceReferenceImages(spaceId: string): any[] {
|
|
|
+ return this.referenceImages.filter(img => img.spaceId === spaceId);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 切换产品选择状态
|
|
|
- */
|
|
|
- toggleProductSelection(productId: string): void {
|
|
|
- const index = this.selectedProductIds.indexOf(productId);
|
|
|
- if (index > -1) {
|
|
|
- this.selectedProductIds.splice(index, 1);
|
|
|
- } else {
|
|
|
- this.selectedProductIds.push(productId);
|
|
|
- }
|
|
|
- this.cdr.markForCheck();
|
|
|
+ getSpaceCADFiles(spaceId: string): any[] {
|
|
|
+ return this.cadFiles.filter(file => file.spaceId === spaceId);
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 选择图片类型标签
|
|
|
- */
|
|
|
- selectImageTab(spaceId: string, tabId: string): void {
|
|
|
- this.activeImageTab[spaceId] = tabId;
|
|
|
- this.cdr.markForCheck();
|
|
|
+
|
|
|
+ getSpaceAnalysisResults(spaceId: string): any[] {
|
|
|
+ return this.analysisResultsBySpace[spaceId] || [];
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 获取空间的总文件数(图片+CAD)
|
|
|
- */
|
|
|
- getTotalSpaceFileCount(spaceId: string): number {
|
|
|
- const imageCount = this.getSpaceReferenceImages(spaceId).length;
|
|
|
- const cadCount = this.getSpaceCADFiles(spaceId).length;
|
|
|
- return imageCount + cadCount;
|
|
|
+ isSpaceExpanded(spaceId: string): boolean {
|
|
|
+ return this.expandedSpaces.has(spaceId);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 获取指定空间和类型的图片数量
|
|
|
- */
|
|
|
- getImageCountByType(spaceId: string, typeId: string): number {
|
|
|
- if (typeId === 'all') {
|
|
|
- return this.getSpaceReferenceImages(spaceId).length;
|
|
|
- } else if (typeId === 'cad') {
|
|
|
- return this.getSpaceCADFiles(spaceId).length;
|
|
|
+ toggleSpaceExpansion(spaceId: string) {
|
|
|
+ if (this.expandedSpaces.has(spaceId)) {
|
|
|
+ this.expandedSpaces.delete(spaceId);
|
|
|
} else {
|
|
|
- return this.getImagesByType(spaceId, typeId).length;
|
|
|
+ this.expandedSpaces.add(spaceId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 按类型获取图片
|
|
|
- */
|
|
|
- getImagesByType(spaceId: string, type: string): any[] {
|
|
|
- return this.getSpaceReferenceImages(spaceId).filter(img => img.type === type);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取图片类型标签文本
|
|
|
- */
|
|
|
- getImageTypeLabel(type: string): string {
|
|
|
- const typeMap: { [key: string]: string } = {
|
|
|
- 'all': '全部',
|
|
|
- 'soft_decor': '软装',
|
|
|
- 'hard_decor': '硬装',
|
|
|
- 'cad': 'CAD',
|
|
|
- 'other': '其他',
|
|
|
- 'style': '风格',
|
|
|
- 'space': '空间',
|
|
|
- 'material': '材质'
|
|
|
- };
|
|
|
- return typeMap[type] || type;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取图片类型徽章样式类
|
|
|
- */
|
|
|
- getImageTypeBadgeClass(type: string): string {
|
|
|
- const classMap: { [key: string]: string } = {
|
|
|
- 'soft_decor': 'badge-soft-decor',
|
|
|
- 'hard_decor': 'badge-hard-decor',
|
|
|
- 'other': 'badge-other',
|
|
|
- 'style': 'badge-style',
|
|
|
- 'space': 'badge-space',
|
|
|
- 'material': 'badge-material'
|
|
|
- };
|
|
|
- return classMap[type] || 'badge-default';
|
|
|
+ // Event Handlers from Children
|
|
|
+ onAnalysisComplete(result: any) {
|
|
|
+ console.log('AI Analysis Complete', result);
|
|
|
+ this.loadData();
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 拖拽悬停处理
|
|
|
- */
|
|
|
- onDragOver(event: DragEvent, spaceId: string): void {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.isDragOver = true;
|
|
|
- this.dragOverSpaceId = spaceId;
|
|
|
+ async handleUploadImages(event: {spaceId: string, files: File[], type?: string}) {
|
|
|
+ console.log('Upload Images', event);
|
|
|
+ await this.uploadAndAnalyzeImages(event.files, event.spaceId, event.type);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 拖拽离开处理
|
|
|
- */
|
|
|
- onDragLeave(event: DragEvent): void {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.isDragOver = false;
|
|
|
- this.dragOverSpaceId = '';
|
|
|
+ async handleUploadCAD(event: {spaceId: string, files: File[]}) {
|
|
|
+ console.log('Upload CAD', event);
|
|
|
+ await this.uploadCADFiles(event.files, event.spaceId);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 拖拽放下处理
|
|
|
- */
|
|
|
- async onDrop(event: DragEvent, spaceId: string): Promise<void> {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.isDragOver = false;
|
|
|
- this.dragOverSpaceId = '';
|
|
|
-
|
|
|
- const files = event.dataTransfer?.files;
|
|
|
- if (!files || files.length === 0) return;
|
|
|
-
|
|
|
- // 分类处理拖拽的文件
|
|
|
- const imageFiles: File[] = [];
|
|
|
- const cadFiles: File[] = [];
|
|
|
-
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const file = files[i];
|
|
|
- const fileName = file.name.toLowerCase();
|
|
|
-
|
|
|
- // 识别图片文件
|
|
|
- if (file.type.startsWith('image/')) {
|
|
|
- imageFiles.push(file);
|
|
|
- }
|
|
|
- // 识别CAD文件
|
|
|
- else if (this.isCADFile(file, fileName)) {
|
|
|
- cadFiles.push(file);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 上传图片并进行AI分析
|
|
|
- if (imageFiles.length > 0) {
|
|
|
- await this.uploadAndAnalyzeImages(imageFiles, spaceId);
|
|
|
+ async handleDeleteImage(imageId: string) {
|
|
|
+ if (!confirm('确定要删除这张参考图片吗?')) return;
|
|
|
+ try {
|
|
|
+ await this.projectFileService.deleteProjectFile(imageId);
|
|
|
+ this.referenceImages = this.referenceImages.filter(img => img.id !== imageId);
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('删除失败', e);
|
|
|
+ window?.fmode?.alert('删除失败');
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 上传CAD文件
|
|
|
- if (cadFiles.length > 0) {
|
|
|
- await this.uploadCADFiles(cadFiles, spaceId);
|
|
|
+ async handleDeleteCAD(fileId: string) {
|
|
|
+ if (!confirm('确定要删除这个CAD文件吗?')) return;
|
|
|
+ try {
|
|
|
+ await this.projectFileService.deleteProjectFile(fileId);
|
|
|
+ this.cadFiles = this.cadFiles.filter(f => f.id !== fileId);
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('删除失败', e);
|
|
|
+ window?.fmode?.alert('删除失败');
|
|
|
}
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 判断是否为CAD文件
|
|
|
- */
|
|
|
- private isCADFile(file: File, fileName: string): boolean {
|
|
|
- const cadExtensions = ['.dwg', '.dxf', '.rvt', '.ifc', '.step', '.stp', '.iges', '.igs', '.pdf'];
|
|
|
- const cadMimeTypes = [
|
|
|
- 'application/vnd.autodesk.autocad.drawing',
|
|
|
- 'application/vnd.autodesk.autocad.drawing.macroenabled',
|
|
|
- 'application/pdf',
|
|
|
- 'application/x-pdf'
|
|
|
- ];
|
|
|
-
|
|
|
- // 检查文件扩展名
|
|
|
- const hasCADExtension = cadExtensions.some(ext => fileName.endsWith(ext));
|
|
|
-
|
|
|
- // 检查MIME类型
|
|
|
- const hasCADMimeType = cadMimeTypes.includes(file.type);
|
|
|
-
|
|
|
- return hasCADExtension || hasCADMimeType;
|
|
|
+ handleSpecialRequirementsChange(event: {spaceId: string, content: string}) {
|
|
|
+ this.spaceSpecialRequirements[event.spaceId] = event.content;
|
|
|
+ this.saveSpaceRequirements(event.spaceId, event.content);
|
|
|
+ }
|
|
|
+
|
|
|
+ async saveSpaceRequirements(spaceId: string, content: string) {
|
|
|
+ try {
|
|
|
+ const projectData = this.project.get('data') || {};
|
|
|
+ if (!projectData.spaceRequirements) projectData.spaceRequirements = {};
|
|
|
+ projectData.spaceRequirements[spaceId] = content;
|
|
|
+ this.project.set('data', projectData);
|
|
|
+ await this.project.save();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('保存需求备注失败', e);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 上传CAD文件
|
|
|
- */
|
|
|
- async uploadCADFiles(files: File[], spaceId: string): Promise<void> {
|
|
|
- try {
|
|
|
- this.uploading = true;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
-
|
|
|
- if (!targetProjectId) {
|
|
|
- console.error('未找到项目ID,无法上传文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (const file of files) {
|
|
|
- // 文件大小验证 (50MB for CAD)
|
|
|
- if (file.size > 50 * 1024 * 1024) {
|
|
|
- console.warn(`CAD文件 ${file.name} 超过50MB限制,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 上传CAD文件
|
|
|
- const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
- file,
|
|
|
- targetProjectId,
|
|
|
- 'cad_file',
|
|
|
- spaceId,
|
|
|
- 'requirements',
|
|
|
- {
|
|
|
- uploadedFor: 'requirements_cad',
|
|
|
- spaceId: spaceId,
|
|
|
- uploadStage: 'requirements'
|
|
|
- },
|
|
|
- (progress) => {
|
|
|
- console.log(`CAD文件上传进度: ${progress}%`);
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 创建CAD文件记录
|
|
|
- const uploadedCAD = {
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || file.name,
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- spaceId: spaceId,
|
|
|
- size: file.size,
|
|
|
- projectFile: projectFile
|
|
|
- };
|
|
|
+ handleViewAnalysis(imageId: string) {
|
|
|
+ console.log('View Analysis for', imageId);
|
|
|
+ window?.fmode?.toast('查看分析详情功能开发中');
|
|
|
+ }
|
|
|
|
|
|
- if (uploadedCAD.id) {
|
|
|
- this.cadFiles.push(uploadedCAD);
|
|
|
- console.log(`✅ CAD文件上传成功: ${uploadedCAD.name}`);
|
|
|
-
|
|
|
- // 对CAD文件进行AI分析
|
|
|
- await this.analyzeCADFileWithAI(uploadedCAD, spaceId);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error(`CAD文件上传失败: ${file.name}`, error);
|
|
|
- }
|
|
|
+ openAIDialog(space: any) {
|
|
|
+ if (this.aiDesignComponent) {
|
|
|
+ // Scroll to component
|
|
|
+ const element = (this.aiDesignComponent as any).elementRef?.nativeElement || document.querySelector('app-ai-design-analysis');
|
|
|
+ if (element) {
|
|
|
+ element.scrollIntoView({ behavior: 'smooth' });
|
|
|
}
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('CAD文件上传失败:', error);
|
|
|
- window?.fmode?.alert('CAD文件上传失败,请重试');
|
|
|
- } finally {
|
|
|
- this.uploading = false;
|
|
|
+ // Select the space in AI component
|
|
|
+ this.aiDesignComponent.selectAISpace(space);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ async uploadAndAnalyzeImages(files: File[], spaceId: string, type: string = 'other'): Promise<void> {
|
|
|
+ this.uploading = true;
|
|
|
+ const targetProjectId = this.projectId || this.project?.id;
|
|
|
|
|
|
- /**
|
|
|
- * 上传并分析图片
|
|
|
- */
|
|
|
- async uploadAndAnalyzeImages(files: File[], spaceId: string): Promise<void> {
|
|
|
try {
|
|
|
- this.uploading = true;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
-
|
|
|
- if (!targetProjectId) {
|
|
|
- console.error('未找到项目ID,无法上传文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
for (const file of files) {
|
|
|
- // 文件大小验证 (10MB)
|
|
|
- if (file.size > 10 * 1024 * 1024) {
|
|
|
- console.warn(`文件 ${file.name} 超过10MB限制,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 上传文件
|
|
|
const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
file,
|
|
|
targetProjectId,
|
|
|
@@ -787,4074 +370,64 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
{
|
|
|
uploadedFor: 'requirements_analysis',
|
|
|
spaceId: spaceId,
|
|
|
- uploadStage: 'requirements'
|
|
|
- },
|
|
|
- (progress) => {
|
|
|
- console.log(`上传进度: ${progress}%`);
|
|
|
+ imageType: type
|
|
|
}
|
|
|
);
|
|
|
|
|
|
- // 创建参考图片记录
|
|
|
- const uploadedFile = {
|
|
|
+ this.referenceImages.push({
|
|
|
id: projectFile.id || '',
|
|
|
url: projectFile.get('fileUrl') || '',
|
|
|
name: projectFile.get('fileName') || file.name,
|
|
|
- type: 'other', // 默认类型,AI分析后会更新
|
|
|
+ type: type,
|
|
|
uploadTime: projectFile.createdAt || new Date(),
|
|
|
spaceId: spaceId,
|
|
|
tags: [],
|
|
|
projectFile: projectFile
|
|
|
- };
|
|
|
-
|
|
|
- if (uploadedFile.id) {
|
|
|
- this.analysisImageMap[uploadedFile.id] = uploadedFile;
|
|
|
- this.referenceImages.push(uploadedFile);
|
|
|
-
|
|
|
- // 触发AI分析
|
|
|
- await this.analyzeImageWithAI(uploadedFile, spaceId);
|
|
|
- }
|
|
|
+ });
|
|
|
}
|
|
|
-
|
|
|
this.cdr.markForCheck();
|
|
|
-
|
|
|
} catch (error) {
|
|
|
console.error('上传失败:', error);
|
|
|
- window?.fmode?.alert('文件上传失败,请重试');
|
|
|
+ window?.fmode?.alert('文件上传失败');
|
|
|
} finally {
|
|
|
this.uploading = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 对CAD文件进行AI分析
|
|
|
- */
|
|
|
- async analyzeCADFileWithAI(cadFile: any, spaceId: string): Promise<void> {
|
|
|
- try {
|
|
|
- // CAD文件的AI分析结果
|
|
|
- const analysisResult = {
|
|
|
- suggestedImageType: 'cad',
|
|
|
- originalAnalysis: {
|
|
|
- quality: {
|
|
|
- score: 85,
|
|
|
- level: 'high',
|
|
|
- sharpness: 90,
|
|
|
- brightness: 80,
|
|
|
- contrast: 85
|
|
|
- },
|
|
|
- content: {
|
|
|
- category: 'cad',
|
|
|
- confidence: 95,
|
|
|
- description: 'CAD设计文件 - 建筑平面图/立面图',
|
|
|
- tags: ['CAD', '设计图纸', '建筑']
|
|
|
- },
|
|
|
- technical: {
|
|
|
- megapixels: 0,
|
|
|
- dpi: 0,
|
|
|
- format: cadFile.name.split('.').pop()?.toUpperCase() || 'CAD'
|
|
|
- }
|
|
|
- },
|
|
|
- enhancedAnalysis: {
|
|
|
- style: '专业建筑设计',
|
|
|
- atmosphere: '精确、规范、专业',
|
|
|
- material: '数字设计',
|
|
|
- texture: '线条清晰',
|
|
|
- quality: '高精度',
|
|
|
- form: '几何精确',
|
|
|
- structure: '标准建筑结构',
|
|
|
- colorComposition: '黑白线条'
|
|
|
- },
|
|
|
- colorAnalysis: {
|
|
|
- brightness: '高对比度,黑白分明',
|
|
|
- hue: '黑色线条,白色背景',
|
|
|
- saturation: '无饱和度(黑白图)',
|
|
|
- openness: '低开放度,线条单一',
|
|
|
- extracted: ['#000000', '#FFFFFF'],
|
|
|
- organized: [
|
|
|
- { color: '#FFFFFF', role: '背景', percentage: 70 },
|
|
|
- { color: '#000000', role: '线条', percentage: 30 }
|
|
|
- ],
|
|
|
- expanded: [],
|
|
|
- harmonized: []
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 存储分析结果
|
|
|
- if (!this.analysisResultsBySpace[spaceId]) {
|
|
|
- this.analysisResultsBySpace[spaceId] = [];
|
|
|
- }
|
|
|
-
|
|
|
- const analysisRecord = {
|
|
|
- imageId: cadFile.id,
|
|
|
- imageType: 'cad',
|
|
|
- originalAnalysis: analysisResult.originalAnalysis,
|
|
|
- enhancedAnalysis: analysisResult.enhancedAnalysis,
|
|
|
- colorAnalysis: analysisResult.colorAnalysis,
|
|
|
- analysisTime: new Date().toISOString(),
|
|
|
- isCADFile: true,
|
|
|
- fileName: cadFile.name
|
|
|
- };
|
|
|
-
|
|
|
- this.analysisResultsBySpace[spaceId].push(analysisRecord);
|
|
|
-
|
|
|
- // 保存分析结果到数据库
|
|
|
- await this.saveAnalysisResultToDatabase(spaceId, cadFile.id, analysisRecord);
|
|
|
-
|
|
|
- console.log(`✅ CAD文件分析完成: ${cadFile.name}`);
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('CAD文件分析失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 使用AI分析图片
|
|
|
- */
|
|
|
- async analyzeImageWithAI(imageFile: any, spaceId: string): Promise<void> {
|
|
|
- try {
|
|
|
- // 调用AI分析服务
|
|
|
- const analysisResult = await this.performImageAnalysis(imageFile.url, imageFile);
|
|
|
-
|
|
|
- // 存储分析结果
|
|
|
- if (!this.analysisResultsBySpace[spaceId]) {
|
|
|
- this.analysisResultsBySpace[spaceId] = [];
|
|
|
- }
|
|
|
-
|
|
|
- const analysisRecord = {
|
|
|
- imageId: imageFile.id,
|
|
|
- imageType: analysisResult.suggestedImageType || 'other',
|
|
|
- // 原有分析维度
|
|
|
- originalAnalysis: analysisResult.originalAnalysis,
|
|
|
- // 新增设计分析维度
|
|
|
- enhancedAnalysis: analysisResult.enhancedAnalysis,
|
|
|
- // 色彩解析报告
|
|
|
- colorAnalysis: analysisResult.colorAnalysis,
|
|
|
- // 分析时间戳
|
|
|
- analysisTime: new Date().toISOString()
|
|
|
- };
|
|
|
-
|
|
|
- this.analysisResultsBySpace[spaceId].push(analysisRecord);
|
|
|
-
|
|
|
- // 更新图片类型
|
|
|
- const image = this.referenceImages.find(img => img.id === imageFile.id);
|
|
|
- if (image) {
|
|
|
- image.type = analysisResult.suggestedImageType || 'other';
|
|
|
- }
|
|
|
-
|
|
|
- // 保存分析结果到数据库
|
|
|
- await this.saveAnalysisResultToDatabase(spaceId, imageFile.id, analysisRecord);
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('AI分析失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存分析结果到数据库
|
|
|
- */
|
|
|
- private async saveAnalysisResultToDatabase(spaceId: string, imageId: string, analysisRecord: any): Promise<void> {
|
|
|
- try {
|
|
|
- const projectData = this.project?.get('data') || {};
|
|
|
-
|
|
|
- // 初始化分析结果存储结构
|
|
|
- if (!projectData.requirementsAnalysis) {
|
|
|
- projectData.requirementsAnalysis = {};
|
|
|
- }
|
|
|
- if (!projectData.requirementsAnalysis[spaceId]) {
|
|
|
- projectData.requirementsAnalysis[spaceId] = {
|
|
|
- analysisResults: [],
|
|
|
- lastUpdated: new Date().toISOString()
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否已存在该图片的分析结果
|
|
|
- const existingIndex = projectData.requirementsAnalysis[spaceId].analysisResults.findIndex(
|
|
|
- (r: any) => r.imageId === imageId
|
|
|
- );
|
|
|
-
|
|
|
- if (existingIndex >= 0) {
|
|
|
- // 更新现有分析结果
|
|
|
- projectData.requirementsAnalysis[spaceId].analysisResults[existingIndex] = analysisRecord;
|
|
|
- } else {
|
|
|
- // 添加新的分析结果
|
|
|
- projectData.requirementsAnalysis[spaceId].analysisResults.push(analysisRecord);
|
|
|
- }
|
|
|
-
|
|
|
- projectData.requirementsAnalysis[spaceId].lastUpdated = new Date().toISOString();
|
|
|
-
|
|
|
- // 保存到项目
|
|
|
- this.project?.set('data', projectData);
|
|
|
- await this.project?.save();
|
|
|
-
|
|
|
- console.log(`✅ 分析结果已保存: 空间${spaceId}, 图片${imageId}`);
|
|
|
- } catch (error) {
|
|
|
- console.error('保存分析结果失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 执行图片分析(调用豆包1.6)
|
|
|
- */
|
|
|
- async performImageAnalysis(imageUrl: string, imageFile: any): Promise<any> {
|
|
|
- try {
|
|
|
- // 获取图片基础信息用于分类
|
|
|
- const imageInfo = await this.getImageInfo(imageFile);
|
|
|
-
|
|
|
- // 根据图片特征进行智能分类
|
|
|
- const imageType = this.classifyImageByFeatures(imageInfo);
|
|
|
-
|
|
|
- // 调用AI服务进行深度分析
|
|
|
- const aiAnalysis = await this.callAIAnalysis(imageUrl, imageInfo);
|
|
|
-
|
|
|
- return {
|
|
|
- suggestedImageType: imageType,
|
|
|
- // 原有分析维度(保留)
|
|
|
- originalAnalysis: {
|
|
|
- quality: aiAnalysis.quality || {
|
|
|
- score: 75,
|
|
|
- level: 'high',
|
|
|
- sharpness: 80,
|
|
|
- brightness: 70,
|
|
|
- contrast: 75
|
|
|
- },
|
|
|
- content: aiAnalysis.content || {
|
|
|
- category: imageType === 'soft_decor' ? 'soft_decor' : 'hard_decor',
|
|
|
- confidence: 85,
|
|
|
- description: '室内设计参考图',
|
|
|
- tags: []
|
|
|
- },
|
|
|
- technical: {
|
|
|
- megapixels: imageInfo.megapixels || 2.1,
|
|
|
- dpi: imageInfo.dpi || 72,
|
|
|
- format: imageInfo.format || 'image/jpeg'
|
|
|
- }
|
|
|
- },
|
|
|
- // 新增设计分析维度
|
|
|
- enhancedAnalysis: {
|
|
|
- style: aiAnalysis.style || '现代简约',
|
|
|
- atmosphere: aiAnalysis.atmosphere || '温馨舒适',
|
|
|
- material: aiAnalysis.material || '木质、金属、布艺混合',
|
|
|
- texture: aiAnalysis.texture || '光滑与粗糙结合',
|
|
|
- quality: aiAnalysis.qualityDesc || '高质感',
|
|
|
- form: aiAnalysis.form || '几何线条为主',
|
|
|
- structure: aiAnalysis.structure || '开放式布局',
|
|
|
- colorComposition: aiAnalysis.colorComposition || '中性色系为主'
|
|
|
- },
|
|
|
- // 色彩解析报告
|
|
|
- colorAnalysis: {
|
|
|
- brightness: aiAnalysis.brightness || '低长调,以低明度为主,明暗对比大',
|
|
|
- hue: aiAnalysis.hue || '红、橙、黄、绿等4个色相',
|
|
|
- saturation: aiAnalysis.saturation || '中等饱和度,整体素雅',
|
|
|
- openness: aiAnalysis.openness || '色彩开放度中等,3-4种主要色系',
|
|
|
- extracted: aiAnalysis.extractedColors || ['#FFFFFF', '#333333', '#999999'],
|
|
|
- organized: aiAnalysis.organizedColors || [
|
|
|
- { color: '#FFFFFF', role: '主色', percentage: 60 },
|
|
|
- { color: '#333333', role: '次色', percentage: 30 },
|
|
|
- { color: '#999999', role: '辅色', percentage: 10 }
|
|
|
- ],
|
|
|
- expanded: aiAnalysis.expandedColors || ['#F5F5F5', '#E0E0E0'],
|
|
|
- harmonized: aiAnalysis.harmonizedColors || ['#FAFAFA', '#D9D9D9']
|
|
|
- },
|
|
|
- // 风格元素分析(新增)
|
|
|
- styleElements: {
|
|
|
- styleKeywords: aiAnalysis.styleKeywords || ['新古典主义', '现代轻奢', '艺术装饰']
|
|
|
- },
|
|
|
- // 色彩搭配分析(新增)
|
|
|
- colorScheme: {
|
|
|
- primaryColors: aiAnalysis.primaryColors || ['#333333', '#FFFFFF'],
|
|
|
- secondaryColors: aiAnalysis.secondaryColors || ['#999999', '#F5F5F5'],
|
|
|
- accentColors: aiAnalysis.accentColors || ['#D4AF37', '#C0C0C0']
|
|
|
- },
|
|
|
- // 材质分析(新增)
|
|
|
- materialAnalysis: {
|
|
|
- materials: aiAnalysis.materials || ['大理石', '玻璃', '布', '金属', '木材']
|
|
|
- },
|
|
|
- // 布局特征分析(新增)
|
|
|
- layoutFeatures: {
|
|
|
- features: aiAnalysis.layoutFeatures || [
|
|
|
- '对称式布局',
|
|
|
- '多区域采光设计',
|
|
|
- '以壁炉为视觉中心',
|
|
|
- '开放式活动空间',
|
|
|
- '层次化软装陈设'
|
|
|
- ]
|
|
|
- },
|
|
|
- // 空间氛围分析(新增)
|
|
|
- atmosphereAnalysis: {
|
|
|
- atmosphere: aiAnalysis.atmosphereDesc || '高级、温馨、舒适的居住氛围'
|
|
|
- }
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- console.error('AI分析失败:', error);
|
|
|
- // 返回默认分析结果
|
|
|
- return this.getDefaultAnalysisResult();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取图片信息
|
|
|
- */
|
|
|
- private async getImageInfo(imageFile: any): Promise<any> {
|
|
|
- return new Promise((resolve) => {
|
|
|
- const img = new Image();
|
|
|
- img.onload = () => {
|
|
|
- const megapixels = (img.width * img.height) / 1000000;
|
|
|
- resolve({
|
|
|
- width: img.width,
|
|
|
- height: img.height,
|
|
|
- megapixels: Math.round(megapixels * 100) / 100,
|
|
|
- dpi: 72,
|
|
|
- format: imageFile.type || 'image/jpeg',
|
|
|
- aspectRatio: img.width / img.height
|
|
|
- });
|
|
|
- };
|
|
|
- img.onerror = () => {
|
|
|
- resolve({
|
|
|
- width: 1920,
|
|
|
- height: 1080,
|
|
|
- megapixels: 2.1,
|
|
|
- dpi: 72,
|
|
|
- format: imageFile.type || 'image/jpeg',
|
|
|
- aspectRatio: 16 / 9
|
|
|
- });
|
|
|
- };
|
|
|
- img.src = imageFile.url || '';
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 根据图片特征分类
|
|
|
- */
|
|
|
- private classifyImageByFeatures(imageInfo: any): string {
|
|
|
- // 根据像素、宽高比等特征进行分类
|
|
|
- const megapixels = imageInfo.megapixels || 0;
|
|
|
- const aspectRatio = imageInfo.aspectRatio || 1;
|
|
|
-
|
|
|
- // 高像素 + 宽屏 = 渲染图
|
|
|
- if (megapixels > 3 && aspectRatio > 1.5) {
|
|
|
- return 'rendering';
|
|
|
- }
|
|
|
-
|
|
|
- // 中等像素 + 接近正方形 = 软装
|
|
|
- if (megapixels > 1 && aspectRatio > 0.8 && aspectRatio < 1.3) {
|
|
|
- return 'soft_decor';
|
|
|
- }
|
|
|
-
|
|
|
- // 低像素或特殊比例 = 硬装
|
|
|
- if (megapixels < 1.5) {
|
|
|
- return 'hard_decor';
|
|
|
- }
|
|
|
-
|
|
|
- return 'other';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 调用AI分析服务
|
|
|
- */
|
|
|
- private async callAIAnalysis(imageUrl: string, imageInfo: any): Promise<any> {
|
|
|
- // 这里可以集成豆包1.6 API
|
|
|
- // 目前返回模拟数据
|
|
|
- return {
|
|
|
- quality: {
|
|
|
- score: 85,
|
|
|
- level: 'high',
|
|
|
- sharpness: 88,
|
|
|
- brightness: 82,
|
|
|
- contrast: 85
|
|
|
- },
|
|
|
- content: {
|
|
|
- category: 'soft_decor',
|
|
|
- confidence: 92,
|
|
|
- description: '现代简约风格室内设计',
|
|
|
- tags: ['现代', '简约', '温馨']
|
|
|
- },
|
|
|
- style: '现代简约',
|
|
|
- atmosphere: '温馨舒适',
|
|
|
- material: '木质、金属、布艺混合',
|
|
|
- texture: '光滑与粗糙结合',
|
|
|
- qualityDesc: '高质感',
|
|
|
- form: '几何线条为主',
|
|
|
- structure: '开放式布局',
|
|
|
- colorComposition: '中性色系为主',
|
|
|
- brightness: '低长调,以低明度为主,明暗对比大',
|
|
|
- hue: '红、橙、黄、绿等4个色相',
|
|
|
- saturation: '中等饱和度,整体素雅',
|
|
|
- openness: '色彩开放度中等,3-4种主要色系',
|
|
|
- extractedColors: ['#FFFFFF', '#333333', '#999999', '#CCCCCC'],
|
|
|
- organizedColors: [
|
|
|
- { color: '#FFFFFF', role: '主色', percentage: 55 },
|
|
|
- { color: '#333333', role: '次色', percentage: 25 },
|
|
|
- { color: '#999999', role: '辅色', percentage: 15 },
|
|
|
- { color: '#CCCCCC', role: '点缀', percentage: 5 }
|
|
|
- ],
|
|
|
- expandedColors: ['#F5F5F5', '#E0E0E0', '#D9D9D9'],
|
|
|
- harmonizedColors: ['#FAFAFA', '#D9D9D9', '#C0C0C0']
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取默认分析结果
|
|
|
- */
|
|
|
- private getDefaultAnalysisResult(): any {
|
|
|
- return {
|
|
|
- suggestedImageType: 'other',
|
|
|
- originalAnalysis: {
|
|
|
- quality: { score: 70, level: 'medium', sharpness: 70, brightness: 70, contrast: 70 },
|
|
|
- content: { category: 'unknown', confidence: 60, description: '参考图', tags: [] },
|
|
|
- technical: { megapixels: 2.0, dpi: 72, format: 'image/jpeg' }
|
|
|
- },
|
|
|
- enhancedAnalysis: {
|
|
|
- style: '待分析',
|
|
|
- atmosphere: '待分析',
|
|
|
- material: '待分析',
|
|
|
- texture: '待分析',
|
|
|
- quality: '待分析',
|
|
|
- form: '待分析',
|
|
|
- structure: '待分析',
|
|
|
- colorComposition: '待分析'
|
|
|
- },
|
|
|
- colorAnalysis: {
|
|
|
- brightness: '待分析',
|
|
|
- hue: '待分析',
|
|
|
- saturation: '待分析',
|
|
|
- openness: '待分析',
|
|
|
- extracted: [],
|
|
|
- organized: [],
|
|
|
- expanded: [],
|
|
|
- harmonized: []
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取图片分析结果
|
|
|
- */
|
|
|
- getImageAnalysisResults(spaceId: string): any[] {
|
|
|
- return this.analysisResultsBySpace[spaceId] || [];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取图片URL
|
|
|
- */
|
|
|
- getImageUrl(imageId: string): string {
|
|
|
- // 先查找参考图片
|
|
|
- const image = this.referenceImages.find(img => img.id === imageId);
|
|
|
- if (image) {
|
|
|
- return image.url || '';
|
|
|
- }
|
|
|
-
|
|
|
- // 再查找CAD文件
|
|
|
- const cadFile = this.cadFiles.find(file => file.id === imageId);
|
|
|
- if (cadFile) {
|
|
|
- return cadFile.url || '';
|
|
|
- }
|
|
|
-
|
|
|
- // 最后查找分析图片映射
|
|
|
- const analysisImage = this.analysisImageMap[imageId];
|
|
|
- return analysisImage?.url || '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 加载已保存的AI分析结果
|
|
|
- */
|
|
|
- private async loadAnalysisResults(): Promise<void> {
|
|
|
- try {
|
|
|
- const projectData = this.project?.get('data') || {};
|
|
|
- const requirementsAnalysis = projectData.requirementsAnalysis || {};
|
|
|
-
|
|
|
- // 加载每个空间的分析结果
|
|
|
- for (const spaceId in requirementsAnalysis) {
|
|
|
- if (requirementsAnalysis.hasOwnProperty(spaceId)) {
|
|
|
- const spaceAnalysis = requirementsAnalysis[spaceId];
|
|
|
- if (spaceAnalysis.analysisResults && Array.isArray(spaceAnalysis.analysisResults)) {
|
|
|
- this.analysisResultsBySpace[spaceId] = spaceAnalysis.analysisResults;
|
|
|
- console.log(`✅ 已加载空间${spaceId}的分析结果: ${spaceAnalysis.analysisResults.length}条`);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('加载分析结果失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间特殊需求
|
|
|
- */
|
|
|
- getSpaceSpecialRequirements(spaceId: string): string {
|
|
|
- return this.spaceSpecialRequirements[spaceId] || '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 设置空间特殊需求
|
|
|
- */
|
|
|
- setSpaceSpecialRequirements(spaceId: string, value: string): void {
|
|
|
- this.spaceSpecialRequirements[spaceId] = value;
|
|
|
- // 保存到数据库或本地存储
|
|
|
- this.saveSpaceRequirements(spaceId, value);
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存空间需求到数据库
|
|
|
- */
|
|
|
- private async saveSpaceRequirements(spaceId: string, requirements: string): Promise<void> {
|
|
|
- try {
|
|
|
- if (!this.project) return;
|
|
|
-
|
|
|
- // 获取或初始化project.data
|
|
|
- let projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- // 初始化特殊需求对象
|
|
|
- if (!projectData.spaceSpecialRequirements) {
|
|
|
- projectData.spaceSpecialRequirements = {};
|
|
|
- }
|
|
|
-
|
|
|
- // 保存该空间的特殊需求
|
|
|
- projectData.spaceSpecialRequirements[spaceId] = {
|
|
|
- content: requirements,
|
|
|
- updatedAt: new Date().toISOString(),
|
|
|
- updatedBy: this.currentUser?.id || 'unknown'
|
|
|
- };
|
|
|
-
|
|
|
- // 更新project对象
|
|
|
- this.project.set('data', projectData);
|
|
|
-
|
|
|
- // 保存到服务器
|
|
|
- await this.project.save();
|
|
|
-
|
|
|
- console.log(`✅ 空间 ${spaceId} 的特殊需求已保存:`, requirements);
|
|
|
- } catch (error) {
|
|
|
- console.error('保存特殊需求失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 上传参考图片并指定类型
|
|
|
- */
|
|
|
- async uploadReferenceImageWithType(event: any, productId?: string, imageType?: string): Promise<void> {
|
|
|
- const files = event.target.files;
|
|
|
- if (!files || files.length === 0) return;
|
|
|
+ async uploadCADFiles(files: File[], spaceId: string): Promise<void> {
|
|
|
+ this.uploading = true;
|
|
|
+ const targetProjectId = this.projectId || this.project?.id;
|
|
|
|
|
|
try {
|
|
|
- this.uploading = true;
|
|
|
- const targetProductId = productId || this.activeProductId;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
- const finalImageType = imageType || 'other';
|
|
|
-
|
|
|
- if (!targetProjectId) {
|
|
|
- console.error('未找到项目ID,无法上传文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const file = files[i];
|
|
|
-
|
|
|
- // 文件类型验证
|
|
|
- if (!file.type.startsWith('image/')) {
|
|
|
- console.warn(`文件 ${file.name} 不是图片格式,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 文件大小验证 (10MB)
|
|
|
- if (file.size > 10 * 1024 * 1024) {
|
|
|
- console.warn(`文件 ${file.name} 超过10MB限制,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用ProjectFileService上传到服务器
|
|
|
+ for (const file of files) {
|
|
|
const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
file,
|
|
|
targetProjectId,
|
|
|
- 'reference_image',
|
|
|
- targetProductId,
|
|
|
+ 'cad_file',
|
|
|
+ spaceId,
|
|
|
'requirements',
|
|
|
{
|
|
|
- imageType: finalImageType,
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_reference',
|
|
|
- uploadStage: 'requirements'
|
|
|
- },
|
|
|
- (progress) => {
|
|
|
- console.log(`上传进度: ${progress}%`);
|
|
|
+ uploadedFor: 'requirements_cad',
|
|
|
+ spaceId: spaceId
|
|
|
}
|
|
|
);
|
|
|
-
|
|
|
- // 为ProjectFile添加扩展数据字段
|
|
|
- const existingData = projectFile.get('data') || {};
|
|
|
- projectFile.set('data', {
|
|
|
- ...existingData,
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_reference',
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- imageType: finalImageType,
|
|
|
- analysis: {
|
|
|
- ai: null,
|
|
|
- manual: null,
|
|
|
- lastAnalyzedAt: null
|
|
|
- }
|
|
|
- });
|
|
|
- await projectFile.save();
|
|
|
-
|
|
|
- // 创建参考图片记录
|
|
|
- const uploadedFile = {
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || file.name,
|
|
|
- type: finalImageType,
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- spaceId: targetProductId,
|
|
|
- tags: [],
|
|
|
- projectFile: projectFile
|
|
|
- };
|
|
|
-
|
|
|
- // 添加到参考图片列表
|
|
|
- if (uploadedFile.id) {
|
|
|
- this.analysisImageMap[uploadedFile.id] = uploadedFile;
|
|
|
- this.referenceImages.push(uploadedFile);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('上传失败:', error);
|
|
|
- window?.fmode?.alert('文件上传失败,请重试');
|
|
|
- } finally {
|
|
|
- this.uploading = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 上传参考图片 - 使用ProjectFileService实际存储
|
|
|
- */
|
|
|
- async uploadReferenceImage(event: any, productId?: string): Promise<void> {
|
|
|
- const files = event.target.files;
|
|
|
- if (!files || files.length === 0) return;
|
|
|
-
|
|
|
- try {
|
|
|
- this.uploading = true;
|
|
|
- const targetProductId = productId || this.activeProductId;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
-
|
|
|
- if (!targetProjectId) {
|
|
|
- console.error('未找到项目ID,无法上传文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const file = files[i];
|
|
|
-
|
|
|
- // 文件类型验证
|
|
|
- if (!file.type.startsWith('image/')) {
|
|
|
- console.warn(`文件 ${file.name} 不是图片格式,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 文件大小验证 (10MB)
|
|
|
- if (file.size > 10 * 1024 * 1024) {
|
|
|
- console.warn(`文件 ${file.name} 超过10MB限制,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用ProjectFileService上传到服务器
|
|
|
- const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
- file,
|
|
|
- targetProjectId,
|
|
|
- 'reference_image',
|
|
|
- targetProductId,
|
|
|
- 'requirements', // stage参数
|
|
|
- {
|
|
|
- imageType: 'style',
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- // 补充:添加关联空间ID和交付类型标识
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_reference', // 需求阶段参考图片
|
|
|
- uploadStage: 'requirements'
|
|
|
- },
|
|
|
- (progress) => {
|
|
|
- console.log(`上传进度: ${progress}%`);
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 补充:为ProjectFile添加扩展数据字段
|
|
|
- const existingData = projectFile.get('data') || {};
|
|
|
- projectFile.set('data', {
|
|
|
- ...existingData,
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_reference',
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- analysis: {
|
|
|
- // 预留AI分析结果字段
|
|
|
- ai: null,
|
|
|
- manual: null,
|
|
|
- lastAnalyzedAt: null
|
|
|
- }
|
|
|
- });
|
|
|
- await projectFile.save();
|
|
|
-
|
|
|
- // 创建参考图片记录
|
|
|
- const uploadedFile = {
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || file.name,
|
|
|
- type: 'style',
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- spaceId: targetProductId,
|
|
|
- tags: [],
|
|
|
- projectFile: projectFile // 保存ProjectFile对象引用
|
|
|
- };
|
|
|
-
|
|
|
- // 添加到参考图片列表
|
|
|
- if (uploadedFile.id) {
|
|
|
- this.analysisImageMap[uploadedFile.id] = uploadedFile;
|
|
|
- this.referenceImages.push(uploadedFile);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('上传失败:', error);
|
|
|
- window?.fmode?.alert('文件上传失败,请重试');
|
|
|
- } finally {
|
|
|
- this.uploading = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- analysisImageMap:any = {}
|
|
|
- /**
|
|
|
- * 删除参考图片 - 同时删除服务器文件
|
|
|
- */
|
|
|
- async deleteReferenceImage(imageId: string): Promise<void> {
|
|
|
- try {
|
|
|
- // 查找图片记录
|
|
|
- const image = this.referenceImages.find(img => img.id === imageId);
|
|
|
-
|
|
|
- if (image && image.projectFile) {
|
|
|
- // 使用ProjectFileService删除服务器上的文件
|
|
|
- await this.projectFileService.deleteProjectFile(imageId);
|
|
|
- }
|
|
|
-
|
|
|
- // 从列表中移除
|
|
|
- this.referenceImages = this.referenceImages.filter(img => img.id !== imageId);
|
|
|
- delete this.analysisImageMap[imageId];
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('删除参考图片失败:', error);
|
|
|
- window?.fmode?.alert('删除文件失败,请重试');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 查看图片色彩分析 - 打开ColorGetDialog
|
|
|
- */
|
|
|
- async viewImageColorAnalysis(imageId: string): Promise<void> {
|
|
|
- const image = this.referenceImages.find(img => img.id === imageId);
|
|
|
- if (!image) {
|
|
|
- console.error('未找到图片记录');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 打开色彩分析对话框
|
|
|
- const dialogRef = this.dialog.open(ColorGetDialogComponent, {
|
|
|
- width: '90vw',
|
|
|
- maxWidth: '800px',
|
|
|
- height: 'auto',
|
|
|
- maxHeight: '90vh',
|
|
|
- data: {
|
|
|
- fileId: image.id,
|
|
|
- fileObject: image.projectFile,
|
|
|
- url: image.url,
|
|
|
- name: image.name
|
|
|
- },
|
|
|
- panelClass: 'color-analysis-dialog'
|
|
|
- });
|
|
|
-
|
|
|
- // 对话框关闭后,刷新分析结果
|
|
|
- dialogRef.afterClosed().subscribe(result => {
|
|
|
- console.log('色彩分析对话框已关闭', result);
|
|
|
- this.cdr.markForCheck();
|
|
|
- });
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('打开色彩分析对话框失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 上传CAD文件 - 使用ProjectFileService实际存储
|
|
|
- */
|
|
|
- async uploadCAD(event: any, productId?: string): Promise<void> {
|
|
|
- const files = event.target.files;
|
|
|
- if (!files || files.length === 0) return;
|
|
|
-
|
|
|
- try {
|
|
|
- this.uploading = true;
|
|
|
- const targetProductId = productId || this.activeProductId;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
-
|
|
|
- if (!targetProjectId) {
|
|
|
- console.error('未找到项目ID,无法上传文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const file = files[i];
|
|
|
-
|
|
|
- // 验证文件类型
|
|
|
- const allowedExtensions = ['.dwg', '.dxf', '.pdf'];
|
|
|
- const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
|
- if (!allowedExtensions.includes(fileExtension)) {
|
|
|
- console.warn(`文件 ${file.name} 不是支持的CAD格式,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 验证文件大小 (50MB)
|
|
|
- if (file.size > 50 * 1024 * 1024) {
|
|
|
- console.warn(`文件 ${file.name} 超过50MB限制,跳过`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用ProjectFileService上传到服务器
|
|
|
- const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
- file,
|
|
|
- targetProjectId,
|
|
|
- 'cad_drawing',
|
|
|
- targetProductId,
|
|
|
- 'requirements', // stage参数
|
|
|
- {
|
|
|
- cadFormat: fileExtension.replace('.', ''),
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- // 补充:添加关联空间ID和交付类型标识
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_cad',
|
|
|
- uploadStage: 'requirements'
|
|
|
- },
|
|
|
- (progress) => {
|
|
|
- console.log(`上传进度: ${progress}%`);
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 补充:为CAD文件ProjectFile添加扩展数据字段
|
|
|
- const existingData = projectFile.get('data') || {};
|
|
|
- projectFile.set('data', {
|
|
|
- ...existingData,
|
|
|
- spaceId: targetProductId,
|
|
|
- deliveryType: 'requirements_cad',
|
|
|
- uploadedFor: 'requirements_analysis',
|
|
|
- cadFormat: fileExtension.replace('.', ''),
|
|
|
- analysis: {
|
|
|
- // 预留CAD分析结果字段
|
|
|
- ai: null,
|
|
|
- manual: null,
|
|
|
- lastAnalyzedAt: null,
|
|
|
- spaceStructure: null,
|
|
|
- dimensions: null,
|
|
|
- constraints: [],
|
|
|
- opportunities: []
|
|
|
- }
|
|
|
+
|
|
|
+ this.cadFiles.push({
|
|
|
+ id: projectFile.id || '',
|
|
|
+ url: projectFile.get('fileUrl') || '',
|
|
|
+ name: projectFile.get('fileName') || file.name,
|
|
|
+ uploadTime: projectFile.createdAt || new Date(),
|
|
|
+ spaceId: spaceId,
|
|
|
+ size: file.size,
|
|
|
+ projectFile: projectFile
|
|
|
});
|
|
|
- await projectFile.save();
|
|
|
-
|
|
|
- // 创建CAD文件记录
|
|
|
- const uploadedFile = {
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || file.name,
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- size: projectFile.get('fileSize') || file.size,
|
|
|
- spaceId: targetProductId,
|
|
|
- projectFile: projectFile // 保存ProjectFile对象引用
|
|
|
- };
|
|
|
-
|
|
|
- // 添加到CAD文件列表
|
|
|
- if (uploadedFile.id) {
|
|
|
- this.analysisFileMap[uploadedFile.id] = uploadedFile;
|
|
|
- this.cadFiles.push(uploadedFile);
|
|
|
- }
|
|
|
}
|
|
|
-
|
|
|
this.cdr.markForCheck();
|
|
|
-
|
|
|
} catch (error) {
|
|
|
- console.error('上传失败:', error);
|
|
|
- window?.fmode?.alert('文件上传失败,请重试');
|
|
|
+ console.error('CAD上传失败:', error);
|
|
|
+ window?.fmode?.alert('CAD上传失败');
|
|
|
} finally {
|
|
|
this.uploading = false;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 删除CAD文件 - 同时删除服务器文件
|
|
|
- */
|
|
|
- async deleteCAD(fileId: string): Promise<void> {
|
|
|
- try {
|
|
|
- // 查找文件记录
|
|
|
- const file = this.cadFiles.find(f => f.id === fileId);
|
|
|
-
|
|
|
- if (file && file.projectFile) {
|
|
|
- // 使用ProjectFileService删除服务器上的文件
|
|
|
- await this.projectFileService.deleteProjectFile(fileId);
|
|
|
- }
|
|
|
-
|
|
|
- // 从列表中移除
|
|
|
- this.cadFiles = this.cadFiles.filter(f => f.id !== fileId);
|
|
|
- delete this.analysisFileMap[fileId];
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('删除CAD文件失败:', error);
|
|
|
- window?.fmode?.alert('删除文件失败,请重试');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成AI方案
|
|
|
- */
|
|
|
- async generateAISolution(): Promise<void> {
|
|
|
- try {
|
|
|
- this.generating = true;
|
|
|
-
|
|
|
- // 先进行AI分析,再生成方案
|
|
|
- console.log("performComprehensiveAIAnalysis")
|
|
|
- await this.performComprehensiveAIAnalysis();
|
|
|
-
|
|
|
- // 基于AI分析结果生成方案
|
|
|
- this.aiSolution = {
|
|
|
- generated: true,
|
|
|
- content: this.generateSolutionContent(),
|
|
|
- spaces: this.generateSpaceSolutions(),
|
|
|
- estimatedCost: this.calculateAIEnhancedEstimatedCost(),
|
|
|
- timeline: this.calculateAIEnhancedTimeline(),
|
|
|
- crossSpaceCoordination: this.generateAICrossSpaceCoordination()
|
|
|
- };
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('生成失败:', error);
|
|
|
- } finally {
|
|
|
- this.generating = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * AI分析参考图片
|
|
|
- */
|
|
|
- async analyzeReferenceImages(): Promise<void> {
|
|
|
- if (this.referenceImages.length === 0) return;
|
|
|
-
|
|
|
- try {
|
|
|
- this.aiAnalyzingImages = true;
|
|
|
- this.aiAnalysisResults.imageAnalysis = [];
|
|
|
-
|
|
|
- for (const image of this.referenceImages) {
|
|
|
- const analysisResult = await this.analyzeImage(image);
|
|
|
- this.aiAnalysisResults.imageAnalysis.push(analysisResult);
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('图片分析失败:', error);
|
|
|
- } finally {
|
|
|
- this.aiAnalyzingImages = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 分析单张图片 - 使用URL而非Base64
|
|
|
- */
|
|
|
- private async analyzeImage(image: any): Promise<any> {
|
|
|
- try {
|
|
|
- const prompt = `作为专业的室内设计师,请分析这张家装参考图片,提取以下设计信息:
|
|
|
-
|
|
|
-要求:
|
|
|
-1. 准确识别图片中的设计风格 styleElements(如现代简约、北欧、轻奢等)
|
|
|
-2. 提取主要色彩搭配 colorPalette (使用HEX格式,如#FFFFFF)
|
|
|
-3. 识别使用的材质 materialAnalysis(如实木、大理石、布艺等)
|
|
|
-4. 分析空间布局特点 layoutFeatures
|
|
|
-5. 描述整体氛围感受 mood
|
|
|
-6. 给出分析置信度 confidence(0-1之间的小数)
|
|
|
-
|
|
|
-请严格按照以下JSON格式输出:`;
|
|
|
-
|
|
|
- const outputSchema = `{
|
|
|
- "styleElements": [""],
|
|
|
- "colorPalette": [""],
|
|
|
- "materialAnalysis": [""],
|
|
|
- "layoutFeatures": [""],
|
|
|
- "mood": "",
|
|
|
- "confidence": 0.00
|
|
|
-}`;
|
|
|
-
|
|
|
- // 使用图片URL进行分析
|
|
|
- const result = await completionJSON(
|
|
|
- prompt,
|
|
|
- outputSchema,
|
|
|
- undefined,
|
|
|
- 2,
|
|
|
- {
|
|
|
- model: this.AI_MODEL,
|
|
|
- vision: true,
|
|
|
- images:[image.url], // 直接传入图片URL
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 保存分析结果到ProjectFile.analysis.ai
|
|
|
- if (image.projectFile) {
|
|
|
- await this.saveImageAnalysisToProjectFile(image.projectFile, result);
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- imageId: image.id,
|
|
|
- ...result
|
|
|
- };
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('图片分析失败:', error);
|
|
|
- return {
|
|
|
- imageId: image.id,
|
|
|
- styleElements: [],
|
|
|
- colorPalette: [],
|
|
|
- materialAnalysis: [],
|
|
|
- layoutFeatures: [],
|
|
|
- mood: '分析失败',
|
|
|
- confidence: 0
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存图片分析结果到ProjectFile.analysis.ai
|
|
|
- */
|
|
|
- private async saveImageAnalysisToProjectFile(projectFile: any, analysisResult: any): Promise<void> {
|
|
|
- try {
|
|
|
- // 补充:更新ProjectFile.data.analysis.ai字段(与现有analysis字段并存)
|
|
|
- const existingData = projectFile.get('data') || {};
|
|
|
- const existingAnalysis = existingData.analysis || {};
|
|
|
-
|
|
|
- // 保存AI分析结果到data.analysis.ai字段
|
|
|
- existingAnalysis.ai = {
|
|
|
- ...analysisResult,
|
|
|
- analyzedAt: new Date().toISOString(),
|
|
|
- version: '1.0',
|
|
|
- source: 'image_analysis'
|
|
|
- };
|
|
|
- existingAnalysis.lastAnalyzedAt = new Date().toISOString();
|
|
|
-
|
|
|
- existingData.analysis = existingAnalysis;
|
|
|
- projectFile.set('data', existingData);
|
|
|
-
|
|
|
- // 兼容:同时保存到原有的analysis字段
|
|
|
- const currentAnalysis = projectFile.get('analysis') || {};
|
|
|
- currentAnalysis.ai = {
|
|
|
- ...analysisResult,
|
|
|
- analyzedAt: new Date().toISOString(),
|
|
|
- version: '1.0',
|
|
|
- source: 'image_analysis'
|
|
|
- };
|
|
|
- projectFile.set('analysis', currentAnalysis);
|
|
|
-
|
|
|
- // 确保关联Product
|
|
|
- if(!projectFile?.get("product")?.id && projectFile?.get("data")?.spaceId){
|
|
|
- projectFile.set("product",{__type:"Pointer",className:"Product",objectId:projectFile?.get("data")?.spaceId})
|
|
|
- }
|
|
|
-
|
|
|
- await projectFile.save();
|
|
|
-
|
|
|
- console.log('图片分析结果已保存到ProjectFile.data.analysis.ai和ProjectFile.analysis.ai');
|
|
|
- } catch (error) {
|
|
|
- console.error('保存分析结果失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * AI分析CAD文件
|
|
|
- */
|
|
|
- async analyzeCADFiles(): Promise<void> {
|
|
|
- if (this.cadFiles.length === 0) return;
|
|
|
-
|
|
|
- try {
|
|
|
- this.aiAnalyzingCAD = true;
|
|
|
- this.aiAnalysisResults.cadAnalysis = [];
|
|
|
-
|
|
|
- for (const cadFile of this.cadFiles) {
|
|
|
- const analysisResult = await this.analyzeCADFile(cadFile);
|
|
|
- this.analysisFileMap[analysisResult?.fileId] = analysisResult;
|
|
|
- this.aiAnalysisResults.cadAnalysis.push(analysisResult);
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('CAD分析失败:', error);
|
|
|
- } finally {
|
|
|
- this.aiAnalyzingCAD = false;
|
|
|
- }
|
|
|
- }
|
|
|
- analysisFileMap:any = {}
|
|
|
-
|
|
|
- /**
|
|
|
- * 分析单个CAD文件
|
|
|
- */
|
|
|
- private async analyzeCADFile(cadFile: any): Promise<any> {
|
|
|
- try {
|
|
|
- const prompt = `分析这个CAD户型文件,提取空间结构信息:
|
|
|
-{
|
|
|
- "spaceStructure": {
|
|
|
- "totalArea": "总面积",
|
|
|
- "roomCount": "房间数量",
|
|
|
- "layoutType": "布局类型"
|
|
|
- },
|
|
|
- "dimensions": {
|
|
|
- "length": "长度",
|
|
|
- "width": "宽度",
|
|
|
- "height": "层高"
|
|
|
- },
|
|
|
- "constraints": ["限制因素1", "限制因素2"],
|
|
|
- "opportunities": ["优化机会1", "优化机会2"]
|
|
|
}
|
|
|
-
|
|
|
-要求:
|
|
|
-1. 识别空间结构和尺寸
|
|
|
-2. 分析承重墙、管道等限制因素
|
|
|
-3. 提供布局优化建议`;
|
|
|
-
|
|
|
- const output = `{
|
|
|
- "spaceStructure": {
|
|
|
- "totalArea": "120㎡",
|
|
|
- "roomCount": "3室2厅",
|
|
|
- "layoutType": "南北通透"
|
|
|
- },
|
|
|
- "dimensions": {
|
|
|
- "length": "12m",
|
|
|
- "width": "10m",
|
|
|
- "height": "2.8m"
|
|
|
- },
|
|
|
- "constraints": ["承重墙不可拆改", "主管道位置固定"],
|
|
|
- "opportunities": ["客厅阳台可打通", "厨房可做开放式设计"]
|
|
|
-}`;
|
|
|
-
|
|
|
- // 模拟CAD分析(实际需要CAD解析库)
|
|
|
- const result = await completionJSON(
|
|
|
- prompt,
|
|
|
- output,
|
|
|
- (_content) => {
|
|
|
- // 进度回调
|
|
|
- },
|
|
|
- 2,
|
|
|
- {
|
|
|
- model: this.AI_MODEL
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- return {
|
|
|
- fileId: cadFile.id,
|
|
|
- ...result
|
|
|
- };
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('CAD文件分析失败:', error);
|
|
|
- return {
|
|
|
- fileId: cadFile.id,
|
|
|
- spaceStructure: {},
|
|
|
- dimensions: {},
|
|
|
- constraints: [],
|
|
|
- opportunities: []
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 执行综合AI分析
|
|
|
- */
|
|
|
- async performComprehensiveAIAnalysis(): Promise<void> {
|
|
|
- try {
|
|
|
- this.aiGeneratingComprehensive = true;
|
|
|
-
|
|
|
- // 并行执行图片和CAD分析
|
|
|
- await Promise.all([
|
|
|
- this.analyzeReferenceImages(),
|
|
|
- this.analyzeCADFiles()
|
|
|
- ]);
|
|
|
-
|
|
|
- // 生成综合分析
|
|
|
- await this.generateComprehensiveAnalysis();
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error) {
|
|
|
- console.error('综合分析失败:', error);
|
|
|
- } finally {
|
|
|
- this.aiGeneratingComprehensive = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成综合分析
|
|
|
- */
|
|
|
- private async generateComprehensiveAnalysis(): Promise<void> {
|
|
|
- try {
|
|
|
- const prompt = `基于以下信息,生成综合设计方案分析:
|
|
|
-
|
|
|
-参考图片分析:${JSON.stringify(this.aiAnalysisResults.imageAnalysis, null, 2)}
|
|
|
-CAD文件分析:${JSON.stringify(this.aiAnalysisResults.cadAnalysis, null, 2)}
|
|
|
-用户需求:${JSON.stringify(this.globalRequirements, null, 2)}
|
|
|
-产品信息:${JSON.stringify(this.projectProducts.map(p => ({
|
|
|
- name: p.name,
|
|
|
- type: p.type,
|
|
|
- area: p.area
|
|
|
- })), null, 2)}
|
|
|
-
|
|
|
-请生成以下格式的综合分析:
|
|
|
-{
|
|
|
- "overallStyle": "整体风格定位",
|
|
|
- "colorScheme": {
|
|
|
- "primary": "主色调",
|
|
|
- "secondary": "副色调",
|
|
|
- "accent": "点缀色"
|
|
|
- },
|
|
|
- "materialRecommendations": ["材质1", "材质2"],
|
|
|
- "layoutOptimization": ["布局优化建议1", "布局优化建议2"],
|
|
|
- "budgetAssessment": {
|
|
|
- "estimatedMin": 最低预算,
|
|
|
- "estimatedMax": 最高预算,
|
|
|
- "riskLevel": "风险等级"
|
|
|
- },
|
|
|
- "timeline": "预计工期",
|
|
|
- "riskFactors": ["风险因素1", "风险因素2"]
|
|
|
-}`;
|
|
|
-
|
|
|
- const output = `{
|
|
|
- "overallStyle": "现代简约风格,注重功能性和舒适性",
|
|
|
- "colorScheme": {
|
|
|
- "primary": "#FFFFFF",
|
|
|
- "secondary": "#F5F5F5",
|
|
|
- "accent": "#3880FF"
|
|
|
- },
|
|
|
- "materialRecommendations": ["实木复合地板", "环保乳胶漆", "布艺沙发"],
|
|
|
- "layoutOptimization": ["打通客厅阳台,增加空间感", "厨房做开放式设计,提升互动性"],
|
|
|
- "budgetAssessment": {
|
|
|
- "estimatedMin": 150000,
|
|
|
- "estimatedMax": 250000,
|
|
|
- "riskLevel": "中等"
|
|
|
- },
|
|
|
- "timeline": "60-75个工作日",
|
|
|
- "riskFactors": ["工期可能受天气影响", "材料价格波动风险"]
|
|
|
-}`;
|
|
|
-
|
|
|
- const result = await completionJSON(
|
|
|
- prompt,
|
|
|
- output,
|
|
|
- (_content) => {
|
|
|
- // 进度回调
|
|
|
- },
|
|
|
- 2,
|
|
|
- {
|
|
|
- model: this.AI_MODEL
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- this.aiAnalysisResults.comprehensiveAnalysis = result;
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('综合分析生成失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * AI聊天助手
|
|
|
- */
|
|
|
- async sendAIChatMessage(): Promise<void> {
|
|
|
- if (!this.aiChatInput.trim()) return;
|
|
|
-
|
|
|
- try {
|
|
|
- const userMessage = {
|
|
|
- id: `msg_${Date.now()}`,
|
|
|
- role: 'user' as const,
|
|
|
- content: this.aiChatInput,
|
|
|
- timestamp: new Date()
|
|
|
- };
|
|
|
-
|
|
|
- this.aiChatMessages.push(userMessage);
|
|
|
- this.aiChatInput = '';
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- // 生成AI回复
|
|
|
- const aiResponse = await this.generateAIChatResponse(userMessage.content);
|
|
|
-
|
|
|
- this.aiChatMessages.push({
|
|
|
- id: `msg_${Date.now() + 1}`,
|
|
|
- role: 'assistant',
|
|
|
- content: aiResponse,
|
|
|
- timestamp: new Date()
|
|
|
- });
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('AI聊天失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成AI聊天回复
|
|
|
- */
|
|
|
- private async generateAIChatResponse(userMessage: string): Promise<string> {
|
|
|
- try {
|
|
|
- const context = `
|
|
|
-项目背景:${JSON.stringify(this.globalRequirements, null, 2)}
|
|
|
-AI分析结果:${JSON.stringify(this.aiAnalysisResults, null, 2)}
|
|
|
-产品信息:${JSON.stringify(this.projectProducts.map(p => ({
|
|
|
- name: p.name,
|
|
|
- type: p.type,
|
|
|
- area: p.area
|
|
|
- })), null, 2)}`;
|
|
|
-
|
|
|
- const prompt = `作为专业的家装设计AI助手,基于以下项目信息回答用户问题:
|
|
|
-
|
|
|
-${context}
|
|
|
-
|
|
|
-用户问题:${userMessage}
|
|
|
-
|
|
|
-请提供专业、实用的建议,回答要简洁明了,字数控制在200字以内。`;
|
|
|
-
|
|
|
- const result = await completionJSON(
|
|
|
- prompt,
|
|
|
- '{"response": "专业的家装设计建议"}',
|
|
|
- (content) => {
|
|
|
- // 流式输出回调
|
|
|
- },
|
|
|
- 2,
|
|
|
- {
|
|
|
- model: this.AI_MODEL
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- return result.response || '抱歉,我暂时无法回答这个问题。';
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('AI回复生成失败:', error);
|
|
|
- return '抱歉,服务暂时不可用,请稍后再试。';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 从ProjectFile加载已上传的图片和CAD文件
|
|
|
- */
|
|
|
- async loadProjectFiles(): Promise<void> {
|
|
|
- try {
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
- if (!targetProjectId) {
|
|
|
- console.warn('未找到项目ID,无法加载文件');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 加载参考图片
|
|
|
- const referenceFiles = await this.projectFileService.getProjectFiles(
|
|
|
- targetProjectId,
|
|
|
- {
|
|
|
- fileType: 'reference_image',
|
|
|
- stage: 'requirements'
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 转换为referenceImages格式
|
|
|
- this.referenceImages = referenceFiles.map(projectFile => ({
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || '',
|
|
|
- type: 'style',
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- spaceId: projectFile.get('data')?.spaceId,
|
|
|
- tags: [],
|
|
|
- projectFile: projectFile
|
|
|
- }));
|
|
|
-
|
|
|
- // 构建analysisImageMap
|
|
|
- this.referenceImages.forEach(img => {
|
|
|
- this.analysisImageMap[img.id] = img;
|
|
|
- });
|
|
|
-
|
|
|
- // 加载CAD文件
|
|
|
- const cadFiles = await this.projectFileService.getProjectFiles(
|
|
|
- targetProjectId,
|
|
|
- {
|
|
|
- fileType: 'cad_drawing',
|
|
|
- stage: 'requirements'
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 转换为cadFiles格式
|
|
|
- this.cadFiles = cadFiles.map(projectFile => ({
|
|
|
- id: projectFile.id || '',
|
|
|
- url: projectFile.get('fileUrl') || '',
|
|
|
- name: projectFile.get('fileName') || '',
|
|
|
- uploadTime: projectFile.createdAt || new Date(),
|
|
|
- size: projectFile.get('fileSize') || 0,
|
|
|
- spaceId: projectFile.get('data')?.spaceId,
|
|
|
- projectFile: projectFile
|
|
|
- }));
|
|
|
-
|
|
|
- // 构建analysisFileMap
|
|
|
- this.cadFiles.forEach(file => {
|
|
|
- this.analysisFileMap[file.id] = file;
|
|
|
- });
|
|
|
-
|
|
|
- // 加载已有的AI分析结果
|
|
|
- await this.loadExistingAnalysisResults();
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- console.log(`已加载 ${this.referenceImages.length} 张参考图片和 ${this.cadFiles.length} 个CAD文件`);
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('加载项目文件失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 加载已有的AI分析结果
|
|
|
- */
|
|
|
- private async loadExistingAnalysisResults(): Promise<void> {
|
|
|
- try {
|
|
|
- // 加载图片分析结果
|
|
|
- this.aiAnalysisResults.imageAnalysis = [];
|
|
|
- for (const image of this.referenceImages) {
|
|
|
- if (image.projectFile) {
|
|
|
- const analysis = image.projectFile.get('analysis');
|
|
|
- if (analysis && analysis.ai) {
|
|
|
- this.aiAnalysisResults.imageAnalysis.push({
|
|
|
- imageId: image.id,
|
|
|
- ...analysis.ai
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`已加载 ${this.aiAnalysisResults.imageAnalysis.length} 个图片分析结果`);
|
|
|
- } catch (error) {
|
|
|
- console.error('加载分析结果失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成方案内容
|
|
|
- */
|
|
|
- private generateSolutionContent(): string {
|
|
|
- const analysis = this.aiAnalysisResults.comprehensiveAnalysis;
|
|
|
- if (analysis) {
|
|
|
- return `基于AI分析结果,我们为您推荐${analysis.overallStyle},通过${analysis.colorScheme.primary}、${analysis.colorScheme.secondary}、${analysis.colorScheme.accent}的色彩搭配,营造${this.globalRequirements.colorScheme.atmosphere}的居住氛围。`;
|
|
|
- }
|
|
|
- return `基于您的${this.globalRequirements.stylePreference}风格需求,我们为您设计了以下方案...`;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成空间方案
|
|
|
- */
|
|
|
- private generateSpaceSolutions(): any[] {
|
|
|
- return this.projectProducts.map(product => {
|
|
|
- const analysis = this.aiAnalysisResults.comprehensiveAnalysis;
|
|
|
- return {
|
|
|
- id: product.id,
|
|
|
- name: product.name,
|
|
|
- type: product.type,
|
|
|
- styleDescription: `${analysis?.overallStyle || this.globalRequirements.stylePreference}风格${product.name}设计`,
|
|
|
- colorPalette: analysis ? [analysis.colorScheme.primary, analysis.colorScheme.secondary, analysis.colorScheme.accent] : [this.globalRequirements.colorScheme.primary, this.globalRequirements.colorScheme.secondary, this.globalRequirements.colorScheme.accent],
|
|
|
- materials: analysis?.materialRecommendations || ['实木', '大理石', '布艺'],
|
|
|
- furnitureRecommendations: this.getFurnitureRecommendations(product.type),
|
|
|
- estimatedCost: this.calculateProductEstimatedCost(product),
|
|
|
- timeline: this.calculateProductTimeline(product)
|
|
|
- };
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算AI增强的估算成本
|
|
|
- */
|
|
|
- private calculateAIEnhancedEstimatedCost(): number {
|
|
|
- const analysis = this.aiAnalysisResults.comprehensiveAnalysis;
|
|
|
- if (analysis?.budgetAssessment) {
|
|
|
- return analysis.budgetAssessment.estimatedMax || this.calculateTotalEstimatedCost();
|
|
|
- }
|
|
|
- return this.globalRequirements.overallBudget.max || this.calculateTotalEstimatedCost();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算AI增强的工期
|
|
|
- */
|
|
|
- private calculateAIEnhancedTimeline(): string {
|
|
|
- const analysis = this.aiAnalysisResults.comprehensiveAnalysis;
|
|
|
- return analysis?.timeline || this.calculateTotalTimeline();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成AI跨空间协调方案
|
|
|
- */
|
|
|
- private generateAICrossSpaceCoordination(): any {
|
|
|
- const analysis = this.aiAnalysisResults.comprehensiveAnalysis;
|
|
|
- return {
|
|
|
- styleConsistency: {
|
|
|
- description: analysis ? `确保${analysis.overallStyle}风格在各空间的统一体现` : '确保各空间风格统一协调',
|
|
|
- keyElements: ['色彩搭配', '材质选择', '设计元素']
|
|
|
- },
|
|
|
- functionalFlow: {
|
|
|
- description: analysis ? `基于${analysis.layoutOptimization?.join('、') || '空间规划'}优化功能流线` : '优化空间之间的功能流线',
|
|
|
- considerations: ['动线规划', '采光通风', '噪音控制']
|
|
|
- },
|
|
|
- timelineCoordination: {
|
|
|
- description: '协调各空间施工时间',
|
|
|
- strategy: '并行施工,关键节点协调'
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 切换AI聊天显示
|
|
|
- */
|
|
|
- toggleAIChat(): void {
|
|
|
- this.showAIChat = !this.showAIChat;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取家具推荐
|
|
|
- */
|
|
|
- private getFurnitureRecommendations(spaceType: string): string[] {
|
|
|
- const recommendations: Record<string, string[]> = {
|
|
|
- 'living_room': ['沙发', '茶几', '电视柜', '书架', '装饰柜'],
|
|
|
- 'bedroom': ['床', '衣柜', '床头柜', '梳妆台', '椅子'],
|
|
|
- 'kitchen': ['橱柜', '冰箱', '灶具', '抽油烟机', '洗碗机'],
|
|
|
- 'bathroom': ['浴室柜', '马桶', '淋浴房', '花洒', '镜子'],
|
|
|
- 'dining_room': ['餐桌', '餐椅', '餐边柜', '酒柜', '装饰品'],
|
|
|
- 'study': ['书桌', '书椅', '书架', '台灯', '电脑桌']
|
|
|
- };
|
|
|
- return recommendations[spaceType] || ['基础家具'];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算产品估算成本
|
|
|
- */
|
|
|
- private calculateProductEstimatedCost(product: Project): number {
|
|
|
- const baseCostPerSqm: Record<string, number> = {
|
|
|
- 'living_room': 1500,
|
|
|
- 'bedroom': 1200,
|
|
|
- 'kitchen': 2000,
|
|
|
- 'bathroom': 1800,
|
|
|
- 'dining_room': 1300,
|
|
|
- 'study': 1100
|
|
|
- };
|
|
|
- const baseCost = (baseCostPerSqm[product.type] || 1000) * (product.area || 10);
|
|
|
- const qualityMultiplier: Record<string, number> = {
|
|
|
- 'standard': 1.0,
|
|
|
- 'premium': 1.5,
|
|
|
- 'luxury': 2.0
|
|
|
- };
|
|
|
- return baseCost * (qualityMultiplier[this.globalRequirements.qualityLevel] || 1.0);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算总估算成本
|
|
|
- */
|
|
|
- private calculateTotalEstimatedCost(): number {
|
|
|
- return this.projectProducts.reduce((total, product) => {
|
|
|
- return total + this.calculateProductEstimatedCost(product);
|
|
|
- }, 0);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算产品工期
|
|
|
- */
|
|
|
- private calculateProductTimeline(product: Project): string {
|
|
|
- const baseDays: Record<string, number> = {
|
|
|
- 'living_room': 15,
|
|
|
- 'bedroom': 12,
|
|
|
- 'kitchen': 20,
|
|
|
- 'bathroom': 18,
|
|
|
- 'dining_room': 10,
|
|
|
- 'study': 8
|
|
|
- };
|
|
|
- return `${baseDays[product.type] || 10}-15个工作日`;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算总工期
|
|
|
- */
|
|
|
- private calculateTotalTimeline(): string {
|
|
|
- const totalDays = this.projectProducts.reduce((total, product) => {
|
|
|
- const baseDays: Record<string, number> = {
|
|
|
- 'living_room': 15,
|
|
|
- 'bedroom': 12,
|
|
|
- 'kitchen': 20,
|
|
|
- 'bathroom': 18,
|
|
|
- 'dining_room': 10,
|
|
|
- 'study': 8
|
|
|
- };
|
|
|
- return total + (baseDays[product.type] || 10);
|
|
|
- }, 0);
|
|
|
- return `预计${Math.ceil(totalDays * 0.7)}-${totalDays}个工作日(考虑并行施工)`;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存草稿
|
|
|
- */
|
|
|
- async saveDraft(): Promise<void> {
|
|
|
- if (!this.project || !this.canEdit) return;
|
|
|
-
|
|
|
- try {
|
|
|
- this.saving = true;
|
|
|
-
|
|
|
- // 模拟保存逻辑
|
|
|
- console.log('保存草稿');
|
|
|
-
|
|
|
- } catch (err) {
|
|
|
- console.error('保存失败:', err);
|
|
|
- } finally {
|
|
|
- this.saving = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- /**
|
|
|
- * 提交确认(完全参考订单阶段实现)
|
|
|
- */
|
|
|
- async submitRequirements(): Promise<void> {
|
|
|
- console.log('🔘 [确认需求] 按钮被点击', {
|
|
|
- hasProject: !!this.project,
|
|
|
- hasCurrentUser: !!this.currentUser,
|
|
|
- canEdit: this.canEdit,
|
|
|
- saving: this.saving,
|
|
|
- projectId: this.project?.id
|
|
|
- });
|
|
|
-
|
|
|
- if (!this.project || !this.currentUser) {
|
|
|
- console.error('❌ [确认需求] 缺少必要数据', {
|
|
|
- project: !!this.project,
|
|
|
- currentUser: !!this.currentUser
|
|
|
- });
|
|
|
- window?.fmode?.alert('项目数据未加载,请刷新页面重试');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!this.canEdit) {
|
|
|
- console.error('❌ [确认需求] 无编辑权限');
|
|
|
- window?.fmode?.alert('当前账号无编辑权限,请联系组长或管理员');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- this.saving = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
- console.log('📝 [确认需求] 开始保存数据...');
|
|
|
-
|
|
|
- const data = this.project.get('data') || {};
|
|
|
-
|
|
|
- // 保存需求确认数据
|
|
|
- const confirmedAt = new Date();
|
|
|
- data.requirementsConfirmed = true;
|
|
|
- data.requirementsConfirmedBy = this.currentUser.id;
|
|
|
- data.requirementsConfirmedByName = this.currentUser.get('name');
|
|
|
- data.requirementsConfirmedAt = confirmedAt.toISOString();
|
|
|
-
|
|
|
- // 补充:需求确认详细信息
|
|
|
- data.requirementsDetail = {
|
|
|
- globalRequirements: this.globalRequirements,
|
|
|
- spaceRequirements: this.spaceRequirements,
|
|
|
- crossSpaceRequirements: this.crossSpaceRequirements,
|
|
|
- referenceImages: this.referenceImages.map(img => ({
|
|
|
- id: img.id,
|
|
|
- url: img.url,
|
|
|
- name: img.name,
|
|
|
- type: img.type,
|
|
|
- spaceId: img.spaceId,
|
|
|
- tags: img.tags
|
|
|
- })),
|
|
|
- cadFiles: this.cadFiles.map(file => ({
|
|
|
- id: file.id,
|
|
|
- url: file.url,
|
|
|
- name: file.name,
|
|
|
- size: file.size,
|
|
|
- spaceId: file.spaceId
|
|
|
- })),
|
|
|
- aiAnalysisResults: this.aiAnalysisResults,
|
|
|
- confirmedAt: new Date().toISOString()
|
|
|
- };
|
|
|
-
|
|
|
- // 重新生成阶段截止时间 (基于当前确认时间与项目交付日期)
|
|
|
- this.rebuildPhaseDeadlines(data, confirmedAt);
|
|
|
-
|
|
|
- // 补充:初始化空间交付物汇总
|
|
|
- if (!data.spaceDeliverableSummary && this.projectProducts.length > 0) {
|
|
|
- data.spaceDeliverableSummary = {};
|
|
|
- let totalDeliverables = 0;
|
|
|
- let completedDeliverables = 0;
|
|
|
-
|
|
|
- this.projectProducts.forEach(product => {
|
|
|
- const productSummary = {
|
|
|
- spaceName: product.name,
|
|
|
- totalDeliverables: 4, // 白模、软装、渲染、后期各1个
|
|
|
- completedDeliverables: 0,
|
|
|
- completionRate: 0,
|
|
|
- lastUpdateTime: new Date().toISOString(),
|
|
|
- phaseProgress: {
|
|
|
- white_model: 0,
|
|
|
- soft_decor: 0,
|
|
|
- rendering: 0,
|
|
|
- post_process: 0
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- data.spaceDeliverableSummary[product.id] = productSummary;
|
|
|
- totalDeliverables += productSummary.totalDeliverables;
|
|
|
- completedDeliverables += productSummary.completedDeliverables;
|
|
|
- });
|
|
|
-
|
|
|
- data.spaceDeliverableSummary.overallCompletionRate = totalDeliverables > 0
|
|
|
- ? Math.round((completedDeliverables / totalDeliverables) * 100)
|
|
|
- : 0;
|
|
|
- }
|
|
|
-
|
|
|
- // 派发阶段完成事件,通知父组件前进
|
|
|
- console.log('📡 [确认需求] 派发 stage:completed 事件');
|
|
|
- try {
|
|
|
- const ev = new CustomEvent('stage:completed', {
|
|
|
- detail: { stage: 'requirements', nextStage: 'delivery' },
|
|
|
- bubbles: true,
|
|
|
- cancelable: true
|
|
|
- });
|
|
|
- document.dispatchEvent(ev);
|
|
|
- console.log('✅ [确认需求] 事件派发成功');
|
|
|
- } catch (eventErr) {
|
|
|
- console.warn('⚠️ [确认需求] 事件派发失败:', eventErr);
|
|
|
- }
|
|
|
-
|
|
|
- window?.fmode?.toast?.success?.('需求确认完成,项目已进入"交付执行"阶段');
|
|
|
- console.log('✅ [确认需求] 流程完成');
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (e) {
|
|
|
- console.error('❌ [确认需求] 保存失败:', e);
|
|
|
- window?.fmode?.alert('提交失败,请稍后重试');
|
|
|
- } finally {
|
|
|
- this.saving = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private rebuildPhaseDeadlines(data: any, confirmedAt?: Date): void {
|
|
|
- const start = confirmedAt ? new Date(confirmedAt) : new Date();
|
|
|
- const projectDeadline = this.project?.get('deadline');
|
|
|
- const fallbackDelivery = addDays(start, 30);
|
|
|
- const deliveryDate = projectDeadline
|
|
|
- ? normalizeDateInput(projectDeadline, fallbackDelivery)
|
|
|
- : fallbackDelivery;
|
|
|
-
|
|
|
- data.phaseDeadlines = generatePhaseDeadlines(start, deliveryDate);
|
|
|
- }
|
|
|
-
|
|
|
- // ===== 工具方法 =====
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间图标
|
|
|
- */
|
|
|
- getSpaceIcon(spaceType: string): string {
|
|
|
- const iconMap: Record<string, string> = {
|
|
|
- 'living_room': 'home',
|
|
|
- 'bedroom': 'bed',
|
|
|
- 'kitchen': 'restaurant',
|
|
|
- 'bathroom': 'water',
|
|
|
- 'dining_room': 'restaurant',
|
|
|
- 'study': 'book',
|
|
|
- 'balcony': 'sunny',
|
|
|
- 'corridor': 'walk',
|
|
|
- 'storage': 'archive',
|
|
|
- 'entrance': 'log-in',
|
|
|
- 'other': 'grid'
|
|
|
- };
|
|
|
- return iconMap[spaceType] || 'grid';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间类型名称
|
|
|
- */
|
|
|
- getSpaceTypeName(spaceType: string): string {
|
|
|
- const nameMap: Record<string, string> = {
|
|
|
- 'living_room': '客厅',
|
|
|
- 'bedroom': '卧室',
|
|
|
- 'kitchen': '厨房',
|
|
|
- 'bathroom': '卫生间',
|
|
|
- 'dining_room': '餐厅',
|
|
|
- 'study': '书房',
|
|
|
- 'balcony': '阳台',
|
|
|
- 'corridor': '走廊',
|
|
|
- 'storage': '储物间',
|
|
|
- 'entrance': '玄关',
|
|
|
- 'other': '其他'
|
|
|
- };
|
|
|
- return nameMap[spaceType] || '其他';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间显示名称
|
|
|
- */
|
|
|
- getSpaceDisplayName(space: any): string {
|
|
|
- return space.name || this.getSpaceTypeName(space.type);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取当前产品的需求
|
|
|
- */
|
|
|
- getCurrentProductRequirement(): any {
|
|
|
- if (!this.activeProductId) return null;
|
|
|
- return this.spaceRequirements.find(req => req.productId === this.activeProductId) || {
|
|
|
- productId: this.activeProductId,
|
|
|
- colorRequirement: {},
|
|
|
- spaceStructureRequirement: {},
|
|
|
- materialRequirement: {},
|
|
|
- lightingRequirement: {},
|
|
|
- specificRequirements: ''
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取当前产品特殊需求的 getter/setter
|
|
|
- */
|
|
|
- get currentProductSpecificRequirements(): string {
|
|
|
- const requirement = this.getCurrentProductRequirement();
|
|
|
- return requirement?.specificRequirements || '';
|
|
|
- }
|
|
|
-
|
|
|
- set currentProductSpecificRequirements(value: string) {
|
|
|
- const requirement = this.getCurrentProductRequirement();
|
|
|
- if (requirement) {
|
|
|
- requirement.specificRequirements = value;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取当前产品的评价
|
|
|
- */
|
|
|
- getCurrentProductFeedback(): any {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 过滤参考图片
|
|
|
- */
|
|
|
- getFilteredReferenceImages(): typeof this.referenceImages {
|
|
|
- if (!this.isMultiProductProject || !this.activeProductId) {
|
|
|
- return this.referenceImages;
|
|
|
- }
|
|
|
- return this.referenceImages.filter(img => !img.spaceId || img.spaceId === this.activeProductId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 过滤CAD文件
|
|
|
- */
|
|
|
- getFilteredCADFiles(): typeof this.cadFiles {
|
|
|
- if (!this.isMultiProductProject || !this.activeProductId) {
|
|
|
- return this.cadFiles;
|
|
|
- }
|
|
|
- return this.cadFiles.filter(file => !file.spaceId || file.spaceId === this.activeProductId);
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化文件大小
|
|
|
- */
|
|
|
- formatFileSize(bytes: number): string {
|
|
|
- if (bytes === 0) return '0 B';
|
|
|
- const k = 1024;
|
|
|
- const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
- const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取质量等级名称
|
|
|
- */
|
|
|
- getQualityLevelName(level: string): string {
|
|
|
- const levelMap: Record<string, string> = {
|
|
|
- 'standard': '标准',
|
|
|
- 'premium': '高级',
|
|
|
- 'luxury': '豪华'
|
|
|
- };
|
|
|
- return levelMap[level] || '标准';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取氛围名称
|
|
|
- */
|
|
|
- getAtmosphereName(atmosphere: string): string {
|
|
|
- const atmosphereMap: Record<string, string> = {
|
|
|
- 'warm': '温馨',
|
|
|
- 'luxury': '高级',
|
|
|
- 'minimal': '简约',
|
|
|
- 'fashion': '时尚',
|
|
|
- 'nordic': '北欧',
|
|
|
- 'chinese': '中式',
|
|
|
- 'european': '欧式'
|
|
|
- };
|
|
|
- return atmosphereMap[atmosphere] || atmosphere;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取跨空间需求类型名称
|
|
|
- */
|
|
|
- getCrossSpaceRequirementTypeName(type: string): string {
|
|
|
- const typeMap: Record<string, string> = {
|
|
|
- 'style': '风格统一',
|
|
|
- 'color': '色彩协调',
|
|
|
- 'material': '材质搭配',
|
|
|
- 'lighting': '照明衔接',
|
|
|
- 'functional': '功能关联',
|
|
|
- 'structural': '结构连接'
|
|
|
- };
|
|
|
- return typeMap[type] || type;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算需求完成度
|
|
|
- */
|
|
|
- calculateRequirementsCompleteness(): number {
|
|
|
- let completedItems = 0;
|
|
|
- let totalItems = 0;
|
|
|
-
|
|
|
- // 全局需求检查
|
|
|
- if (this.globalRequirements.stylePreference) completedItems++;
|
|
|
- totalItems++;
|
|
|
- if (this.globalRequirements.colorScheme.primary) completedItems++;
|
|
|
- totalItems++;
|
|
|
- if (this.globalRequirements.overallBudget.max > 0) completedItems++;
|
|
|
- totalItems++;
|
|
|
- if (this.globalRequirements.timeline) completedItems++;
|
|
|
- totalItems++;
|
|
|
-
|
|
|
- // 产品需求检查
|
|
|
- for (const product of this.projectProducts) {
|
|
|
- const productFeedback = this.spaceRequirements.find(req => req.productId === product.id);
|
|
|
- if (productFeedback) {
|
|
|
- completedItems++;
|
|
|
- }
|
|
|
- totalItems++;
|
|
|
- }
|
|
|
-
|
|
|
- return totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算产品完成度
|
|
|
- */
|
|
|
- calculateProductCompletion(_productId: string): number {
|
|
|
- // 简化计算,实际应该基于产品的具体需求完成情况
|
|
|
- return Math.floor(Math.random() * 100);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 创建跨产品需求
|
|
|
- */
|
|
|
- async createCrossProductRequirement(requirement?: any): Promise<void> {
|
|
|
- console.log('创建跨产品需求:', requirement || {});
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 删除跨产品需求
|
|
|
- */
|
|
|
- async deleteCrossProductRequirement(requirementId: string): Promise<void> {
|
|
|
- console.log('删除跨产品需求:', requirementId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取工序颜色
|
|
|
- */
|
|
|
- getProcessColor(spaceType: string): string {
|
|
|
- const colorMap: Record<string, string> = {
|
|
|
- 'living_room': 'primary',
|
|
|
- 'bedroom': 'secondary',
|
|
|
- 'kitchen': 'tertiary',
|
|
|
- 'bathroom': 'success',
|
|
|
- 'dining_room': 'warning',
|
|
|
- 'study': 'medium'
|
|
|
- };
|
|
|
- return colorMap[spaceType] || 'primary';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取工序徽章类名
|
|
|
- */
|
|
|
- getProcessBadgeClass(spaceType: string): string {
|
|
|
- const colorClass = this.getProcessColor(spaceType);
|
|
|
- return `badge-${colorClass}`;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取关联空间ID列表
|
|
|
- */
|
|
|
- getRelatedSpaceIds(requirement: any): string[] {
|
|
|
- if (!requirement) return [];
|
|
|
- const ids = [requirement.primarySpaceId];
|
|
|
- if (requirement.relatedSpaceIds && Array.isArray(requirement.relatedSpaceIds)) {
|
|
|
- ids.push(...requirement.relatedSpaceIds);
|
|
|
- }
|
|
|
- return ids.filter(id => id); // 过滤掉空值
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 根据产品ID获取产品显示名称(用于模板)
|
|
|
- */
|
|
|
- getProductDisplayNameById(productId: string): string {
|
|
|
- const product = this.projectProducts.find(p => p.id === productId);
|
|
|
- return this.getProductDisplayName(product);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取产品显示名称
|
|
|
- */
|
|
|
- getProductDisplayName(product: Project | undefined): string {
|
|
|
- return product?.name || '未知产品';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 触发文件选择器点击
|
|
|
- */
|
|
|
- triggerFileClick(inputId: string): void {
|
|
|
- const element = document.getElementById(inputId) as HTMLInputElement;
|
|
|
- if (element) {
|
|
|
- element.click();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ===== AI分析辅助方法 =====
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取分析对应的图片
|
|
|
- */
|
|
|
- getAnalysisImage(imageId: string): any {
|
|
|
- return this.referenceImages.find(img => img.id === imageId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取分析对应的CAD文件
|
|
|
- */
|
|
|
- getAnalysisFile(fileId: string): any {
|
|
|
- return this.cadFiles.find(file => file.id === fileId);
|
|
|
- }
|
|
|
-
|
|
|
- // === 模板辅助方法:避免在模板中使用箭头函数/复杂表达式 ===
|
|
|
- onAiChatInputChange(value: string): void {
|
|
|
- this.aiChatInput = value;
|
|
|
- }
|
|
|
-
|
|
|
- isAiChatSendDisabled(): boolean {
|
|
|
- return this.aiAnalyzing || !this.aiChatInput?.trim();
|
|
|
- }
|
|
|
-
|
|
|
- isSaving(): boolean {
|
|
|
- return this.saving;
|
|
|
- }
|
|
|
-
|
|
|
- isSubmitDisabled(): boolean {
|
|
|
- return this.saving || this.generating || this.aiAnalyzing;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取风险等级样式类名
|
|
|
- */
|
|
|
- getRiskLevelClass(riskLevel: string): string {
|
|
|
- const classMap: Record<string, string> = {
|
|
|
- 'low': 'badge-success',
|
|
|
- 'medium': 'badge-warning',
|
|
|
- 'high': 'badge-danger'
|
|
|
- };
|
|
|
- return classMap[riskLevel] || 'badge-secondary';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取风险等级名称
|
|
|
- */
|
|
|
- getRiskLevelName(riskLevel: string): string {
|
|
|
- const nameMap: Record<string, string> = {
|
|
|
- 'low': '低风险',
|
|
|
- 'medium': '中等风险',
|
|
|
- 'high': '高风险'
|
|
|
- };
|
|
|
- return nameMap[riskLevel] || '未知风险';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 切换空间折叠状态
|
|
|
- */
|
|
|
- toggleSpaceExpansion(spaceId: string): void {
|
|
|
- if (this.expandedSpaces.has(spaceId)) {
|
|
|
- this.expandedSpaces.delete(spaceId);
|
|
|
- } else {
|
|
|
- this.expandedSpaces.add(spaceId);
|
|
|
- }
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查空间是否展开
|
|
|
- */
|
|
|
- isSpaceExpanded(spaceId: string): boolean {
|
|
|
- return this.expandedSpaces.has(spaceId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间的参考图片
|
|
|
- */
|
|
|
- getSpaceReferenceImages(spaceId: string): any[] {
|
|
|
- return this.referenceImages.filter(img => img.spaceId === spaceId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间的CAD文件
|
|
|
- */
|
|
|
- getSpaceCADFiles(spaceId: string): any[] {
|
|
|
- return this.cadFiles.filter(file => file.spaceId === spaceId);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查图片是否有分析结果
|
|
|
- */
|
|
|
- hasImageAnalysis(imageId: string): boolean {
|
|
|
- for (const spaceId in this.analysisResultsBySpace) {
|
|
|
- const results = this.analysisResultsBySpace[spaceId];
|
|
|
- if (results.some(r => r.imageId === imageId)) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 删除CAD文件
|
|
|
- */
|
|
|
- async deleteCADFile(cadFileId: string): Promise<void> {
|
|
|
- if (!confirm('确定要删除此CAD文件吗?')) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const index = this.cadFiles.findIndex(f => f.id === cadFileId);
|
|
|
- if (index >= 0) {
|
|
|
- this.cadFiles.splice(index, 1);
|
|
|
-
|
|
|
- // 删除相关的分析结果
|
|
|
- for (const spaceId in this.analysisResultsBySpace) {
|
|
|
- const results = this.analysisResultsBySpace[spaceId];
|
|
|
- const analysisIndex = results.findIndex(r => r.imageId === cadFileId);
|
|
|
- if (analysisIndex >= 0) {
|
|
|
- results.splice(analysisIndex, 1);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`✅ CAD文件已删除: ${cadFileId}`);
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('删除CAD文件失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间文件数量统计
|
|
|
- */
|
|
|
- getSpaceFileCount(spaceId: string): { images: number; cad: number; total: number } {
|
|
|
- const images = this.getSpaceReferenceImages(spaceId).length;
|
|
|
- const cad = this.getSpaceCADFiles(spaceId).length;
|
|
|
- return { images, cad, total: images + cad };
|
|
|
- }
|
|
|
-
|
|
|
- // ===== AI设计分析对话功能 =====
|
|
|
-
|
|
|
- /**
|
|
|
- * 选择AI分析空间
|
|
|
- */
|
|
|
- selectAISpace(space: any): void {
|
|
|
- this.aiDesignCurrentSpace = space;
|
|
|
-
|
|
|
- // 从保存的数据中加载(如果存在)
|
|
|
- const projectData = this.project?.get('data') || {};
|
|
|
-
|
|
|
- // 加载对话历史
|
|
|
- const aiChatHistory = projectData.aiChatHistory || {};
|
|
|
- if (aiChatHistory[space.id]) {
|
|
|
- this.aiChatMessages = aiChatHistory[space.id].messages.map((m: any) => ({
|
|
|
- id: `${m.role}-${Date.now()}-${Math.random()}`,
|
|
|
- role: m.role,
|
|
|
- content: m.content,
|
|
|
- timestamp: new Date(m.timestamp),
|
|
|
- images: m.images
|
|
|
- }));
|
|
|
- console.log('💬 加载对话历史:', this.aiChatMessages.length, '条消息');
|
|
|
- } else {
|
|
|
- this.aiChatMessages = [];
|
|
|
- }
|
|
|
-
|
|
|
- // 优先从确认的报告中加载
|
|
|
- const designReports = projectData.designReports || {};
|
|
|
- if (designReports[space.id]) {
|
|
|
- const savedData = designReports[space.id];
|
|
|
- console.log('✅ 加载已保存的AI分析数据:', space.name);
|
|
|
- this.aiDesignUploadedImages = savedData.images || [];
|
|
|
- this.aiDesignUploadedFiles = savedData.files || [];
|
|
|
- this.aiDesignTextDescription = savedData.textDescription || '';
|
|
|
- this.aiDesignAnalysisResult = savedData.analysisData || null;
|
|
|
- this.aiDesignReport = savedData.report || '';
|
|
|
- this.aiDesignReportConfirmed = !!savedData.confirmedAt;
|
|
|
-
|
|
|
- // 如果有对话历史但消息列表为空,从保存的对话历史加载
|
|
|
- if (this.aiChatMessages.length === 0 && savedData.chatHistory) {
|
|
|
- this.aiChatMessages = savedData.chatHistory.map((m: any) => ({
|
|
|
- id: `${m.role}-${Date.now()}-${Math.random()}`,
|
|
|
- role: m.role,
|
|
|
- content: m.content,
|
|
|
- timestamp: new Date(m.timestamp)
|
|
|
- }));
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 如果没有确认的报告,尝试从分析结果中加载
|
|
|
- const aiDesignAnalysis = projectData.aiDesignAnalysis || {};
|
|
|
- if (aiDesignAnalysis[space.id]) {
|
|
|
- const savedData = aiDesignAnalysis[space.id];
|
|
|
- console.log('✅ 加载临时AI分析数据:', space.name);
|
|
|
- this.aiDesignUploadedImages = savedData.images || [];
|
|
|
- this.aiDesignUploadedFiles = savedData.files || [];
|
|
|
- this.aiDesignTextDescription = savedData.textDescription || '';
|
|
|
- this.aiDesignAnalysisResult = savedData.analysisData || null;
|
|
|
- this.aiDesignReport = '';
|
|
|
- this.aiDesignReportConfirmed = false;
|
|
|
- } else {
|
|
|
- console.log('ℹ️ 该空间暂无AI分析数据:', space.name);
|
|
|
- this.aiDesignUploadedImages = [];
|
|
|
- this.aiDesignUploadedFiles = [];
|
|
|
- this.aiDesignTextDescription = '';
|
|
|
- this.aiDesignAnalysisResult = null;
|
|
|
- this.aiDesignReport = '';
|
|
|
- this.aiDesignReportConfirmed = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- console.log('✨ 选择AI分析空间:', space.name);
|
|
|
-
|
|
|
- // 滚动到底部(如果有对话历史)
|
|
|
- if (this.aiChatMessages.length > 0) {
|
|
|
- this.scrollToBottom();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 打开AI设计分析对话框(兼容原按钮)
|
|
|
- */
|
|
|
- openAIDesignDialog(space: any): void {
|
|
|
- // 兼容空间需求管理中的AI按钮,直接选择空间
|
|
|
- this.selectAISpace(space);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 重置AI分析
|
|
|
- */
|
|
|
- resetAIAnalysis(): void {
|
|
|
- this.aiDesignUploadedImages = [];
|
|
|
- this.aiDesignUploadedFiles = [];
|
|
|
- this.aiDesignTextDescription = '';
|
|
|
- this.aiDesignAnalysisResult = null;
|
|
|
- this.aiDesignReport = '';
|
|
|
- this.aiDesignReportConfirmed = false;
|
|
|
- this.aiDesignAnalyzing = false;
|
|
|
- this.aiDesignGeneratingReport = false;
|
|
|
- this.expandedDimensions.clear(); // 清空折叠状态
|
|
|
- this.cdr.markForCheck();
|
|
|
- console.log('🔄 重置AI分析');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取AI简洁摘要
|
|
|
- */
|
|
|
- getAISummary(): string {
|
|
|
- if (!this.aiDesignAnalysisResult) {
|
|
|
- return '';
|
|
|
- }
|
|
|
-
|
|
|
- // 使用AI服务生成简洁摘要
|
|
|
- return this.designAnalysisAIService.generateBriefSummary(this.aiDesignAnalysisResult);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 切换维度折叠状态
|
|
|
- */
|
|
|
- toggleDimension(dimensionKey: string): void {
|
|
|
- if (this.expandedDimensions.has(dimensionKey)) {
|
|
|
- this.expandedDimensions.delete(dimensionKey);
|
|
|
- } else {
|
|
|
- this.expandedDimensions.add(dimensionKey);
|
|
|
- }
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成客服标注
|
|
|
- */
|
|
|
- async generateServiceNotes(): Promise<void> {
|
|
|
- if (!this.aiDesignAnalysisResult) {
|
|
|
- window?.fmode?.alert('请先完成AI分析');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 使用AI服务生成客服标注格式
|
|
|
- const serviceNotes = this.designAnalysisAIService.generateCustomerServiceNotes(
|
|
|
- this.aiDesignAnalysisResult,
|
|
|
- this.aiDesignTextDescription
|
|
|
- );
|
|
|
-
|
|
|
- console.log('📋 生成的客服标注:', serviceNotes);
|
|
|
-
|
|
|
- // 保存到项目data
|
|
|
- const projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- if (!projectData.designReports) {
|
|
|
- projectData.designReports = {};
|
|
|
- }
|
|
|
-
|
|
|
- if (!projectData.designReports[this.aiDesignCurrentSpace.id]) {
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id] = {};
|
|
|
- }
|
|
|
-
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id].serviceNotes = serviceNotes;
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id].serviceNotesGeneratedAt = new Date();
|
|
|
-
|
|
|
- this.project.set('data', projectData);
|
|
|
- await this.project.save();
|
|
|
-
|
|
|
- // 自动复制到剪贴板
|
|
|
- const copied = await this.copyToClipboard(serviceNotes);
|
|
|
-
|
|
|
- // 显示客服标注
|
|
|
- const message = `客服标注已生成${copied ? '并复制到剪贴板' : ''}!\n\n${serviceNotes}`;
|
|
|
- window?.fmode?.alert(message);
|
|
|
-
|
|
|
- console.log('✅ 客服标注已生成并保存');
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ 生成客服标注失败:', error);
|
|
|
- window?.fmode?.alert('生成失败: ' + (error.message || '未知错误'));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 复制到剪贴板
|
|
|
- */
|
|
|
- private async copyToClipboard(text: string): Promise<boolean> {
|
|
|
- try {
|
|
|
- await navigator.clipboard.writeText(text);
|
|
|
- console.log('✅ 已复制到剪贴板');
|
|
|
- return true;
|
|
|
- } catch (error) {
|
|
|
- console.warn('⚠️ 复制失败,使用备用方案');
|
|
|
- try {
|
|
|
- // 备用方案
|
|
|
- const textarea = document.createElement('textarea');
|
|
|
- textarea.value = text;
|
|
|
- textarea.style.position = 'fixed';
|
|
|
- textarea.style.opacity = '0';
|
|
|
- document.body.appendChild(textarea);
|
|
|
- textarea.select();
|
|
|
- const success = document.execCommand('copy');
|
|
|
- document.body.removeChild(textarea);
|
|
|
- if (success) {
|
|
|
- console.log('✅ 备用方案复制成功');
|
|
|
- }
|
|
|
- return success;
|
|
|
- } catch (fallbackError) {
|
|
|
- console.error('❌ 复制失败:', fallbackError);
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 触发AI对话框文件选择
|
|
|
- */
|
|
|
- triggerAIDialogFileInput(): void {
|
|
|
- const element = document.getElementById('aiDesignFileInput') as HTMLInputElement;
|
|
|
- if (element) {
|
|
|
- element.click();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理AI对话框文件选择
|
|
|
- */
|
|
|
- async handleAIFileSelect(event: Event): Promise<void> {
|
|
|
- const input = event.target as HTMLInputElement;
|
|
|
-
|
|
|
- if (!input.files || input.files.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- await this.handleAIFileUpload(Array.from(input.files));
|
|
|
-
|
|
|
- // 清空input,允许重复选择同一文件
|
|
|
- input.value = '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 移除AI对话框中的图片
|
|
|
- */
|
|
|
- removeAIDialogImage(index: number): void {
|
|
|
- this.aiDesignUploadedImages.splice(index, 1);
|
|
|
- this.aiDesignUploadedFiles.splice(index, 1);
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理文件拖拽进入
|
|
|
- */
|
|
|
- onAIFileDragOver(event: DragEvent): void {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.aiDesignDragOver = true;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理文件拖拽离开
|
|
|
- */
|
|
|
- onAIFileDragLeave(event: DragEvent): void {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.aiDesignDragOver = false;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理文件拖放
|
|
|
- */
|
|
|
- async onAIFileDrop(event: DragEvent): Promise<void> {
|
|
|
- event.preventDefault();
|
|
|
- event.stopPropagation();
|
|
|
- this.aiDesignDragOver = false;
|
|
|
-
|
|
|
- const files = event.dataTransfer?.files;
|
|
|
- if (!files || files.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- await this.handleAIFileUpload(Array.from(files));
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理AI文件上传(统一处理点击和拖拽)
|
|
|
- * 🔥 修复:直接转base64,不上传到云存储,避免631错误
|
|
|
- */
|
|
|
- private async handleAIFileUpload(files: File[]): Promise<void> {
|
|
|
- const maxFiles = 20; // 扩展至20个文件
|
|
|
- const remainingSlots = maxFiles - this.aiDesignUploadedImages.length;
|
|
|
-
|
|
|
- if (remainingSlots <= 0) {
|
|
|
- window?.fmode?.alert(`最多只能上传${maxFiles}个文件`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 🔥 只支持图片格式进行AI分析
|
|
|
- const supportedImageTypes = [
|
|
|
- 'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
|
|
|
- 'image/webp', 'image/bmp', 'image/tiff'
|
|
|
- ];
|
|
|
-
|
|
|
- const filesToProcess = files.slice(0, remainingSlots);
|
|
|
- this.aiDesignUploading = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- try {
|
|
|
- for (const file of filesToProcess) {
|
|
|
- // 检查文件类型
|
|
|
- const fileExt = file.name.split('.').pop()?.toLowerCase();
|
|
|
- const supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff'];
|
|
|
- const isSupported = supportedImageTypes.includes(file.type) ||
|
|
|
- supportedExtensions.includes(fileExt || '');
|
|
|
-
|
|
|
- if (!isSupported) {
|
|
|
- console.warn(`文件 ${file.name} 格式不支持,跳过`);
|
|
|
- window?.fmode?.alert(`文件格式不支持: ${file.name}\n只支持图片格式: JPG、PNG、GIF、WebP等`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 🔥 智能处理大文件:自动压缩
|
|
|
- let processedFile = file;
|
|
|
- const maxSize = 50 * 1024 * 1024; // 50MB硬限制
|
|
|
- const compressThreshold = 5 * 1024 * 1024; // 5MB开始压缩
|
|
|
-
|
|
|
- if (file.size > maxSize) {
|
|
|
- console.warn(`文件 ${file.name} 超过50MB硬限制,跳过`);
|
|
|
- window?.fmode?.alert(`文件超过50MB限制: ${file.name}\n请使用专业工具压缩后再上传`);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 🔥 关键修复:直接转base64,不上传到云存储
|
|
|
- console.log(`📤 准备处理文件: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
-
|
|
|
- // 如果文件大于5MB,自动压缩
|
|
|
- if (file.size > compressThreshold) {
|
|
|
- console.log(`🔄 文件较大,开始压缩...`);
|
|
|
- try {
|
|
|
- processedFile = await this.compressImage(file);
|
|
|
- console.log(`✅ 压缩完成,压缩后大小: ${(processedFile.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
- } catch (compressError) {
|
|
|
- console.warn('⚠️ 压缩失败,使用原文件:', compressError);
|
|
|
- // 压缩失败,继续使用原文件
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`🔄 将图片转换为base64格式...`);
|
|
|
-
|
|
|
- try {
|
|
|
- // 使用FileReader转换为base64
|
|
|
- const base64 = await new Promise<string>((resolve, reject) => {
|
|
|
- const reader = new FileReader();
|
|
|
- reader.onloadend = () => {
|
|
|
- const result = reader.result as string;
|
|
|
- resolve(result);
|
|
|
- };
|
|
|
- reader.onerror = () => {
|
|
|
- reject(new Error('文件读取失败'));
|
|
|
- };
|
|
|
- reader.readAsDataURL(processedFile);
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`✅ 图片已转换为base64,大小: ${(base64.length / 1024).toFixed(2)}KB`);
|
|
|
-
|
|
|
- // 🔥 保存base64数据(AI分析时使用)
|
|
|
- this.aiDesignUploadedImages.push(base64);
|
|
|
- this.aiDesignUploadedFiles.push({
|
|
|
- url: base64, // base64字符串
|
|
|
- name: file.name,
|
|
|
- type: file.type,
|
|
|
- size: file.size,
|
|
|
- extension: fileExt,
|
|
|
- isBase64: true // 标记为base64数据
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`💾 已保存图片: ${file.name}`);
|
|
|
-
|
|
|
- } catch (convertError) {
|
|
|
- console.error(`❌ 转换文件失败: ${file.name}`, convertError);
|
|
|
- window?.fmode?.alert(`处理文件失败: ${file.name}`);
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- console.log(`✅ 已处理${this.aiDesignUploadedImages.length}个文件`);
|
|
|
- console.log(`🎯 所有图片已转为base64,可直接进行AI分析`);
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ 处理文件失败:', error);
|
|
|
- window?.fmode?.alert(`处理文件失败: ${error?.message || '未知错误'}`);
|
|
|
- } finally {
|
|
|
- this.aiDesignUploading = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 开始AI分析(直接调用AI进行真实分析)
|
|
|
- */
|
|
|
- async startAIDesignAnalysis(): Promise<void> {
|
|
|
- // 🔥 防止重复分析:如果正在分析中,直接返回
|
|
|
- if (this.aiDesignAnalyzing) {
|
|
|
- console.log('⚠️ 正在分析中,忽略重复调用');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (this.aiDesignUploadedImages.length === 0) {
|
|
|
- window?.fmode?.alert('请先上传参考图片');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 🔥 关键检查:验证图片是否有效(支持HTTP/HTTPS URL 和 base64格式)
|
|
|
- const validImages = this.aiDesignUploadedImages.filter(data => {
|
|
|
- // 支持两种格式:1) HTTP/HTTPS URL 2) base64 (data:image/...)
|
|
|
- const isValidUrl = data && (data.startsWith('http://') || data.startsWith('https://'));
|
|
|
- const isValidBase64 = data && data.startsWith('data:image/');
|
|
|
- const isValid = isValidUrl || isValidBase64;
|
|
|
-
|
|
|
- if (!isValid) {
|
|
|
- console.warn('⚠️ 无效的图片数据:', data?.substring(0, 50) + '...');
|
|
|
- }
|
|
|
- return isValid;
|
|
|
- });
|
|
|
-
|
|
|
- if (validImages.length === 0) {
|
|
|
- window?.fmode?.alert('图片处理失败,请重新上传。\n提示:\n• 支持JPG/PNG/GIF/WebP等格式\n• 单张图片最大50MB\n• 超过5MB会自动压缩\n• 支持同时上传多张图片');
|
|
|
- // 清空无效的图片列表
|
|
|
- this.aiDesignUploadedImages = [];
|
|
|
- this.aiDesignUploadedFiles = [];
|
|
|
- this.cdr.markForCheck();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (validImages.length < this.aiDesignUploadedImages.length) {
|
|
|
- console.warn(`⚠️ 发现${this.aiDesignUploadedImages.length - validImages.length}个无效图片,已自动过滤`);
|
|
|
- this.aiDesignUploadedImages = validImages;
|
|
|
- }
|
|
|
-
|
|
|
- console.log('✅ 验证通过,有效图片数量:', validImages.length);
|
|
|
- console.log('📸 图片格式:', validImages.map(img => img.startsWith('data:') ? 'base64' : 'URL'));
|
|
|
-
|
|
|
- try {
|
|
|
- // 🔥 支持多轮对话:如果已有对话记录,将新上传的图片作为补充分析
|
|
|
- const isFollowUp = this.aiChatMessages.length > 0;
|
|
|
-
|
|
|
- if (isFollowUp) {
|
|
|
- console.log('📌 检测到已有对话记录,将作为补充分析');
|
|
|
- }
|
|
|
- // 添加用户消息(简短描述,不显示完整提示词)
|
|
|
- const userMessage = {
|
|
|
- id: `user-${Date.now()}`,
|
|
|
- role: 'user' as const,
|
|
|
- content: this.aiDesignTextDescription || `请对这${this.aiDesignUploadedImages.length}张参考图片进行专业的室内设计分析`,
|
|
|
- timestamp: new Date(),
|
|
|
- images: [...this.aiDesignUploadedImages]
|
|
|
- };
|
|
|
-
|
|
|
- this.aiChatMessages.push(userMessage);
|
|
|
-
|
|
|
- // 添加AI流式输出消息(初始为空,会实时更新)
|
|
|
- const aiStreamMessage = {
|
|
|
- id: `ai-${Date.now()}`,
|
|
|
- role: 'assistant' as const,
|
|
|
- content: '', // 初始为空,会实时更新
|
|
|
- timestamp: new Date(),
|
|
|
- isStreaming: true // 标记为流式输出中
|
|
|
- };
|
|
|
-
|
|
|
- this.aiChatMessages.push(aiStreamMessage);
|
|
|
- this.aiDesignAnalyzing = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- // 滚动到底部
|
|
|
- this.scrollToBottom();
|
|
|
-
|
|
|
- // 🔥 构建对话历史(排除当前正在添加的消息和流式输出中的消息)
|
|
|
- const conversationHistory = this.aiChatMessages
|
|
|
- .filter(m => m.id !== userMessage.id && m.id !== aiStreamMessage.id && !m.isStreaming)
|
|
|
- .map(m => ({
|
|
|
- role: m.role,
|
|
|
- content: m.content || ''
|
|
|
- }));
|
|
|
-
|
|
|
- // 直接调用AI分析服务
|
|
|
- console.log('🤖 开始AI图片分析...');
|
|
|
- console.log('📸 图片数量:', this.aiDesignUploadedImages.length);
|
|
|
- console.log('🏠 空间类型:', this.aiDesignCurrentSpace?.name);
|
|
|
- console.log('💬 对话历史数量:', conversationHistory.length, '条');
|
|
|
-
|
|
|
- const analysisResult = await this.designAnalysisAIService.analyzeReferenceImages({
|
|
|
- images: this.aiDesignUploadedImages,
|
|
|
- textDescription: this.aiDesignTextDescription,
|
|
|
- // 🔥 不传递spaceType,让AI基于图片内容自动识别空间类型
|
|
|
- spaceType: undefined,
|
|
|
- conversationHistory: conversationHistory, // 🔥 传递对话历史,支持多轮对话
|
|
|
- deepThinking: this.deepThinkingEnabled,
|
|
|
- onProgressChange: (progress) => {
|
|
|
- console.log('📊 AI分析进度:', progress);
|
|
|
- },
|
|
|
- // 🔥 流式输出回调:实时更新消息内容
|
|
|
- onContentStream: (content) => {
|
|
|
- const streamMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
- if (streamMsg) {
|
|
|
- streamMsg.content = content;
|
|
|
- this.cdr.markForCheck();
|
|
|
- // 滚动到底部以显示新内容
|
|
|
- setTimeout(() => this.scrollToBottom(), 50);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 标记流式输出完成(保留流式输出的原始内容,不再重新格式化)
|
|
|
- const finalMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
- if (finalMsg) {
|
|
|
- finalMsg.isStreaming = false;
|
|
|
- // 保持流式输出的内容不变,已经是完整的AI返回内容
|
|
|
- }
|
|
|
-
|
|
|
- // 保存最新的分析结果
|
|
|
- this.aiDesignAnalysisResult = analysisResult;
|
|
|
-
|
|
|
- // 保存对话记录到项目
|
|
|
- await this.saveChatHistory();
|
|
|
-
|
|
|
- console.log('✅ AI分析完成');
|
|
|
-
|
|
|
- // 滚动到底部
|
|
|
- this.scrollToBottom();
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ AI分析失败:', error);
|
|
|
-
|
|
|
- // 移除流式输出消息或加载消息
|
|
|
- this.aiChatMessages = this.aiChatMessages.filter(m => !m.isLoading && !m.isStreaming);
|
|
|
-
|
|
|
- // 添加错误消息
|
|
|
- this.aiChatMessages.push({
|
|
|
- id: `ai-error-${Date.now()}`,
|
|
|
- role: 'assistant' as const,
|
|
|
- content: `抱歉,分析过程出现错误:${error.message || '未知错误'}。请重试。`,
|
|
|
- timestamp: new Date()
|
|
|
- });
|
|
|
-
|
|
|
- window?.fmode?.alert('AI分析失败: ' + (error.message || '未知错误'));
|
|
|
- } finally {
|
|
|
- this.aiDesignAnalyzing = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成客户报告
|
|
|
- */
|
|
|
- async generateClientReport(): Promise<void> {
|
|
|
- if (!this.aiDesignAnalysisResult) {
|
|
|
- window?.fmode?.alert('请先完成AI分析');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- this.aiDesignGeneratingReport = true;
|
|
|
- this.aiDesignReport = '';
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- console.log('📝 开始生成报告...');
|
|
|
-
|
|
|
- this.aiDesignReport = await this.designAnalysisAIService.generateClientReport({
|
|
|
- analysisData: this.aiDesignAnalysisResult,
|
|
|
- spaceName: this.aiDesignCurrentSpace?.name || '空间',
|
|
|
- onContentChange: (content) => {
|
|
|
- this.aiDesignReport = content;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- console.log('✅ 报告生成完成');
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ 生成报告失败:', error);
|
|
|
- window?.fmode?.alert('生成报告失败: ' + (error.message || '未知错误'));
|
|
|
- } finally {
|
|
|
- this.aiDesignGeneratingReport = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 确认报告
|
|
|
- */
|
|
|
- async confirmDesignReport(): Promise<void> {
|
|
|
- if (!this.aiDesignReport) {
|
|
|
- window?.fmode?.alert('报告尚未生成');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- this.aiDesignReportConfirmed = true;
|
|
|
-
|
|
|
- if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
- const projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- if (!projectData.designReports) {
|
|
|
- projectData.designReports = {};
|
|
|
- }
|
|
|
-
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id] = {
|
|
|
- report: this.aiDesignReport,
|
|
|
- analysisData: this.aiDesignAnalysisResult,
|
|
|
- images: this.aiDesignUploadedImages,
|
|
|
- files: this.aiDesignUploadedFiles,
|
|
|
- textDescription: this.aiDesignTextDescription,
|
|
|
- confirmedAt: new Date().toISOString(),
|
|
|
- confirmedBy: this.currentUser?.id || 'unknown'
|
|
|
- };
|
|
|
-
|
|
|
- console.log('💾 保存数据:', {
|
|
|
- spaceId: this.aiDesignCurrentSpace.id,
|
|
|
- spaceName: this.aiDesignCurrentSpace.name,
|
|
|
- imagesCount: this.aiDesignUploadedImages.length,
|
|
|
- filesCount: this.aiDesignUploadedFiles.length,
|
|
|
- hasAnalysisResult: !!this.aiDesignAnalysisResult,
|
|
|
- hasReport: !!this.aiDesignReport
|
|
|
- });
|
|
|
-
|
|
|
- this.project.set('data', projectData);
|
|
|
- await this.project.save();
|
|
|
-
|
|
|
- console.log('✅ 报告已保存');
|
|
|
- }
|
|
|
-
|
|
|
- window?.fmode?.alert('设计分析报告已确认并保存');
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('❌ 保存报告失败:', error);
|
|
|
- window?.fmode?.alert('保存报告失败,请重试');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取场景类型中文名
|
|
|
- */
|
|
|
- getSceneTypeName(type: string): string {
|
|
|
- const typeMap: { [key: string]: string } = {
|
|
|
- '客餐厅': '客餐厅',
|
|
|
- '厨房': '厨房',
|
|
|
- '卧室': '卧室',
|
|
|
- '阳台': '阳台',
|
|
|
- '卫生间': '卫生间',
|
|
|
- '其他': '其他'
|
|
|
- };
|
|
|
- return typeMap[type] || type;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取空间基调图标
|
|
|
- */
|
|
|
- getSpaceToneIcon(tone: string): string {
|
|
|
- const toneIcons: Record<string, string> = {
|
|
|
- '温馨': '🏡',
|
|
|
- '现代': '🏢',
|
|
|
- '轻奢': '💎',
|
|
|
- '简约': '⬜',
|
|
|
- '古典': '🏛️',
|
|
|
- '自然': '🌿'
|
|
|
- };
|
|
|
- return toneIcons[tone] || '🏠';
|
|
|
- }
|
|
|
-
|
|
|
- // ===== AI对话系统功能方法 =====
|
|
|
-
|
|
|
- /**
|
|
|
- * 输入框变化事件 - 自动调整高度
|
|
|
- */
|
|
|
- onChatInputChange(event: any): void {
|
|
|
- const textarea = event.target;
|
|
|
- if (textarea) {
|
|
|
- textarea.style.height = 'auto';
|
|
|
- textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 输入框回车事件 - Shift+Enter换行,Enter发送
|
|
|
- */
|
|
|
- onChatInputEnter(event: Event): void {
|
|
|
- const keyEvent = event as KeyboardEvent;
|
|
|
- if (keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
|
|
|
- keyEvent.preventDefault();
|
|
|
- this.sendChatMessage();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 使用快捷提示
|
|
|
- */
|
|
|
- useQuickPrompt(prompt: string): void {
|
|
|
- this.aiChatInput = prompt;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- // 聚焦到输入框
|
|
|
- setTimeout(() => {
|
|
|
- this.chatInputElement?.nativeElement?.focus();
|
|
|
- }, 100);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 发送聊天消息
|
|
|
- */
|
|
|
- async sendChatMessage(): Promise<void> {
|
|
|
- const message = this.aiChatInput?.trim();
|
|
|
-
|
|
|
- if (!message || this.aiDesignAnalyzing) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 🔥 优化:如果已有对话历史,允许继续对话;否则需要先上传图片
|
|
|
- if (this.aiDesignUploadedImages.length === 0 && this.aiChatMessages.length === 0) {
|
|
|
- window?.fmode?.alert('请先上传参考图片开始分析');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果没有图片但有对话历史,使用之前的图片继续对话
|
|
|
- const imagesToUse = this.aiDesignUploadedImages.length > 0
|
|
|
- ? this.aiDesignUploadedImages
|
|
|
- : this.getPreviousImages();
|
|
|
-
|
|
|
- if (imagesToUse.length === 0) {
|
|
|
- window?.fmode?.alert('请先上传参考图片');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 添加用户消息
|
|
|
- const userMessage = {
|
|
|
- id: `user-${Date.now()}`,
|
|
|
- role: 'user' as const,
|
|
|
- content: message,
|
|
|
- timestamp: new Date(),
|
|
|
- images: [...imagesToUse]
|
|
|
- };
|
|
|
-
|
|
|
- this.aiChatMessages.push(userMessage);
|
|
|
- this.aiChatInput = '';
|
|
|
-
|
|
|
- // 重置输入框高度
|
|
|
- if (this.chatInputElement) {
|
|
|
- this.chatInputElement.nativeElement.style.height = 'auto';
|
|
|
- }
|
|
|
-
|
|
|
- // 添加AI流式输出消息(初始为空,会实时更新)
|
|
|
- const aiStreamMessage = {
|
|
|
- id: `ai-${Date.now()}`,
|
|
|
- role: 'assistant' as const,
|
|
|
- content: '', // 初始为空,会实时更新
|
|
|
- timestamp: new Date(),
|
|
|
- isStreaming: true // 标记为流式输出中
|
|
|
- };
|
|
|
-
|
|
|
- this.aiChatMessages.push(aiStreamMessage);
|
|
|
- this.aiDesignAnalyzing = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- // 滚动到底部
|
|
|
- this.scrollToBottom();
|
|
|
-
|
|
|
- // 🔥 构建对话历史(排除当前消息和流式输出中的消息)
|
|
|
- const conversationHistory = this.aiChatMessages
|
|
|
- .filter(m =>
|
|
|
- m.id !== userMessage.id &&
|
|
|
- m.id !== aiStreamMessage.id &&
|
|
|
- !m.isStreaming &&
|
|
|
- m.content &&
|
|
|
- m.content.trim().length > 0
|
|
|
- )
|
|
|
- .map(m => ({
|
|
|
- role: m.role,
|
|
|
- content: m.content || ''
|
|
|
- }));
|
|
|
-
|
|
|
- // 🔥 使用真正的AI对话功能(而不是重复分析)
|
|
|
- console.log('💬 开始AI对话...');
|
|
|
- console.log('📜 对话历史数量:', conversationHistory.length, '条');
|
|
|
- console.log('📸 使用图片数量:', imagesToUse.length, '张');
|
|
|
-
|
|
|
- const aiResponse = await this.designAnalysisAIService.chatWithAI({
|
|
|
- userMessage: message,
|
|
|
- conversationHistory: conversationHistory,
|
|
|
- images: imagesToUse,
|
|
|
- // 🔥 流式输出回调:实时更新消息内容
|
|
|
- onContentStream: (content) => {
|
|
|
- const streamMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
- if (streamMsg) {
|
|
|
- streamMsg.content = content;
|
|
|
- this.cdr.markForCheck();
|
|
|
- // 滚动到底部以显示新内容
|
|
|
- setTimeout(() => this.scrollToBottom(), 50);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 标记流式输出完成
|
|
|
- const finalMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
- if (finalMsg) {
|
|
|
- finalMsg.isStreaming = false;
|
|
|
- finalMsg.content = aiResponse; // 确保内容是最终的响应
|
|
|
- }
|
|
|
-
|
|
|
- // 保存对话记录到项目
|
|
|
- await this.saveChatHistory();
|
|
|
-
|
|
|
- console.log('✅ AI对话完成');
|
|
|
-
|
|
|
- // 滚动到底部
|
|
|
- this.scrollToBottom();
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ AI对话失败:', error);
|
|
|
-
|
|
|
- // 移除流式输出消息或加载消息
|
|
|
- this.aiChatMessages = this.aiChatMessages.filter(m => !m.isLoading && !m.isStreaming);
|
|
|
-
|
|
|
- // 添加错误消息
|
|
|
- this.aiChatMessages.push({
|
|
|
- id: `ai-error-${Date.now()}`,
|
|
|
- role: 'assistant' as const,
|
|
|
- content: `抱歉,分析过程出现错误:${error.message || '未知错误'}。请重试或修改您的问题。`,
|
|
|
- timestamp: new Date()
|
|
|
- });
|
|
|
-
|
|
|
- window?.fmode?.alert('AI分析失败: ' + (error.message || '未知错误'));
|
|
|
- } finally {
|
|
|
- this.aiDesignAnalyzing = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取之前对话中使用的图片
|
|
|
- */
|
|
|
- private getPreviousImages(): string[] {
|
|
|
- // 从对话历史中获取最近的用户消息的图片
|
|
|
- for (let i = this.aiChatMessages.length - 1; i >= 0; i--) {
|
|
|
- const message = this.aiChatMessages[i];
|
|
|
- if (message.role === 'user' && message.images && message.images.length > 0) {
|
|
|
- console.log('📸 使用之前对话中的图片继续分析,图片数量:', message.images.length);
|
|
|
- return message.images;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 如果对话历史中没有图片,尝试从分析结果中获取
|
|
|
- if (this.aiDesignAnalysisResult && this.aiDesignUploadedImages.length > 0) {
|
|
|
- console.log('📸 使用初始分析的图片继续对话');
|
|
|
- return this.aiDesignUploadedImages;
|
|
|
- }
|
|
|
-
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化AI响应(优化显示效果)
|
|
|
- */
|
|
|
- private formatAIResponse(analysisResult: any): string {
|
|
|
- if (!analysisResult) {
|
|
|
- return '❌ 分析结果为空';
|
|
|
- }
|
|
|
-
|
|
|
- // 如果有原始内容,直接使用(AI返回的是纯文字格式)
|
|
|
- if (analysisResult.rawContent) {
|
|
|
- return analysisResult.rawContent;
|
|
|
- }
|
|
|
-
|
|
|
- // 否则格式化结构化数据(兼容模式)
|
|
|
- let response = '';
|
|
|
-
|
|
|
- // 添加分析标题
|
|
|
- response += `# ✨ AI设计分析报告\n\n`;
|
|
|
- response += `> 基于豆包1.6模型的"灯光+材质"细节落地分析\n\n`;
|
|
|
- response += `---\n\n`;
|
|
|
-
|
|
|
- // 场景识别
|
|
|
- if (analysisResult.sceneRecognition && Object.keys(analysisResult.sceneRecognition).length > 0) {
|
|
|
- response += `## 🏠 场景识别\n\n`;
|
|
|
- if (analysisResult.sceneRecognition.spaceType) {
|
|
|
- response += `- **空间类型**:${analysisResult.sceneRecognition.spaceType}\n`;
|
|
|
- }
|
|
|
- if (analysisResult.sceneRecognition.confidence) {
|
|
|
- response += `- **识别置信度**:${analysisResult.sceneRecognition.confidence}\n`;
|
|
|
- }
|
|
|
- if (analysisResult.sceneRecognition.evidence) {
|
|
|
- response += `- **识别依据**:${analysisResult.sceneRecognition.evidence}\n`;
|
|
|
- }
|
|
|
- response += '\n';
|
|
|
- }
|
|
|
-
|
|
|
- // 整体基调
|
|
|
- if (analysisResult.overallTone && Object.keys(analysisResult.overallTone).length > 0) {
|
|
|
- response += `## 🎭 整体基调\n\n`;
|
|
|
- if (analysisResult.overallTone.primary) {
|
|
|
- response += `- **主基调**:${analysisResult.overallTone.primary}\n`;
|
|
|
- }
|
|
|
- if (analysisResult.overallTone.secondary) {
|
|
|
- response += `- **次基调**:${analysisResult.overallTone.secondary}\n`;
|
|
|
- }
|
|
|
- if (analysisResult.overallTone.description) {
|
|
|
- response += `- **基调特征**:${analysisResult.overallTone.description}\n`;
|
|
|
- }
|
|
|
- response += '\n';
|
|
|
- }
|
|
|
-
|
|
|
- // 设计维度分析
|
|
|
- if (analysisResult.designDimensions) {
|
|
|
- response += `## 🎨 设计维度分析\n\n`;
|
|
|
-
|
|
|
- const dims = analysisResult.designDimensions;
|
|
|
-
|
|
|
- if (dims.colorSystem) {
|
|
|
- response += `### 🌈 色彩系统\n\n`;
|
|
|
- if (dims.colorSystem.primaryColors?.length > 0) {
|
|
|
- response += `- **主色调**:${dims.colorSystem.primaryColors.join(', ')}\n`;
|
|
|
- }
|
|
|
- if (dims.colorSystem.secondaryColors?.length > 0) {
|
|
|
- response += `- **辅助色**:${dims.colorSystem.secondaryColors.join(', ')}\n`;
|
|
|
- }
|
|
|
- response += '\n';
|
|
|
- }
|
|
|
-
|
|
|
- if (dims.lightingDesign) {
|
|
|
- response += `### 💡 灯光设计\n\n`;
|
|
|
- response += `${dims.lightingDesign.description || '暂无描述'}\n\n`;
|
|
|
- }
|
|
|
-
|
|
|
- if (dims.materialAnalysis) {
|
|
|
- response += `### 🏗️ 材质分析\n\n`;
|
|
|
- response += `${dims.materialAnalysis.description || '暂无描述'}\n\n`;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 优化建议
|
|
|
- if (analysisResult.suggestions && analysisResult.suggestions.length > 0) {
|
|
|
- response += `## ✨ 专业优化建议\n\n`;
|
|
|
- analysisResult.suggestions.forEach((suggestion: string, index: number) => {
|
|
|
- response += `${index + 1}. ${suggestion}\n`;
|
|
|
- });
|
|
|
- response += '\n';
|
|
|
- }
|
|
|
-
|
|
|
- return this.enhanceMarkdownFormat(response);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 增强Markdown格式(添加更多视觉元素)
|
|
|
- */
|
|
|
- private enhanceMarkdownFormat(content: string): string {
|
|
|
- // 为表格添加样式标记
|
|
|
- let enhanced = content;
|
|
|
-
|
|
|
- // 确保表格格式正确
|
|
|
- enhanced = enhanced.replace(/\n\|/g, '\n|');
|
|
|
-
|
|
|
- // 为重要信息添加强调
|
|
|
- enhanced = enhanced.replace(/RGB\((\d+),\s*(\d+),\s*(\d+)\)/g, '**RGB($1, $2, $3)**');
|
|
|
- enhanced = enhanced.replace(/(\d+)lux/g, '**$1lux**');
|
|
|
- enhanced = enhanced.replace(/(\d+)K/g, '**$1K**');
|
|
|
- enhanced = enhanced.replace(/Ra(\d+)/g, '**Ra$1**');
|
|
|
- enhanced = enhanced.replace(/(\d+)mm/g, '**$1mm**');
|
|
|
-
|
|
|
- return enhanced;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化消息内容 - 纯文本美观排版
|
|
|
- */
|
|
|
- formatMessageContent(content: string): string {
|
|
|
- if (!content) return '';
|
|
|
-
|
|
|
- let formatted = content;
|
|
|
-
|
|
|
- // 将连续的换行符转换为段落
|
|
|
- formatted = formatted.replace(/\n\n+/g, '</p><p class="content-paragraph">');
|
|
|
-
|
|
|
- // 单个换行符转换为 <br>
|
|
|
- formatted = formatted.replace(/\n/g, '<br>');
|
|
|
-
|
|
|
- // 包裹在段落标签中
|
|
|
- formatted = `<p class="content-paragraph">${formatted}</p>`;
|
|
|
-
|
|
|
- // 识别并标记标题行(以中文冒号或数字开头的行)
|
|
|
- formatted = formatted.replace(/<p class="content-paragraph">([一二三四五六七八九十]+、.*?)<br>/g, '<h4 class="content-heading">$1</h4><p class="content-paragraph">');
|
|
|
- formatted = formatted.replace(/<p class="content-paragraph">(\d+[\.|、].*?)<br>/g, '<h4 class="content-heading">$1</h4><p class="content-paragraph">');
|
|
|
-
|
|
|
- // 识别列表项(以 - 或 • 开头)
|
|
|
- formatted = formatted.replace(/- (.*?)<br>/g, '<li class="content-list-item">$1</li>');
|
|
|
- formatted = formatted.replace(/• (.*?)<br>/g, '<li class="content-list-item">$1</li>');
|
|
|
-
|
|
|
- // 包裹连续的列表项
|
|
|
- formatted = formatted.replace(/(<li class="content-list-item">.*?<\/li>)+/g, '<ul class="content-list">$&</ul>');
|
|
|
-
|
|
|
- // 清理空段落
|
|
|
- formatted = formatted.replace(/<p class="content-paragraph"><\/p>/g, '');
|
|
|
-
|
|
|
- return formatted;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存对话历史到项目
|
|
|
- */
|
|
|
- private async saveChatHistory(): Promise<void> {
|
|
|
- if (!this.project || !this.aiDesignCurrentSpace?.id) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- if (!projectData.aiChatHistory) {
|
|
|
- projectData.aiChatHistory = {};
|
|
|
- }
|
|
|
-
|
|
|
- projectData.aiChatHistory[this.aiDesignCurrentSpace.id] = {
|
|
|
- messages: this.aiChatMessages.map(m => ({
|
|
|
- role: m.role,
|
|
|
- content: m.content,
|
|
|
- timestamp: m.timestamp.toISOString(),
|
|
|
- images: m.images
|
|
|
- })),
|
|
|
- lastUpdated: new Date().toISOString()
|
|
|
- };
|
|
|
-
|
|
|
- this.project.set('data', projectData);
|
|
|
- await this.project.save();
|
|
|
-
|
|
|
- console.log('💾 对话历史已保存');
|
|
|
- } catch (error) {
|
|
|
- console.error('保存对话历史失败:', error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 滚动到底部
|
|
|
- */
|
|
|
- private scrollToBottom(): void {
|
|
|
- setTimeout(() => {
|
|
|
- if (this.chatMessagesWrapper) {
|
|
|
- const element = this.chatMessagesWrapper.nativeElement;
|
|
|
- element.scrollTop = element.scrollHeight;
|
|
|
- }
|
|
|
- }, 100);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 打开附件对话框
|
|
|
- */
|
|
|
- openAttachmentDialog(): void {
|
|
|
- console.log('📎 打开附件对话框');
|
|
|
- // 触发文件上传
|
|
|
- const fileInput = document.getElementById('aiDesignFileInput') as HTMLInputElement;
|
|
|
- if (fileInput) {
|
|
|
- fileInput.click();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 切换深度思考模式
|
|
|
- */
|
|
|
- toggleDeepThinking(): void {
|
|
|
- this.deepThinkingEnabled = !this.deepThinkingEnabled;
|
|
|
- console.log('💡 深度思考模式:', this.deepThinkingEnabled ? '已开启' : '已关闭');
|
|
|
-
|
|
|
- const status = this.deepThinkingEnabled ? '已开启' : '已关闭';
|
|
|
- window?.fmode?.alert(`深度思考模式${status}`);
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 复制消息内容
|
|
|
- */
|
|
|
- copyMessage(content: string): void {
|
|
|
- if (!content) return;
|
|
|
-
|
|
|
- // 移除HTML标签
|
|
|
- const textContent = content.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
|
-
|
|
|
- navigator.clipboard.writeText(textContent).then(() => {
|
|
|
- console.log('📋 消息已复制');
|
|
|
- window?.fmode?.alert('消息已复制到剪贴板');
|
|
|
- }).catch(err => {
|
|
|
- console.error('复制失败:', err);
|
|
|
- window?.fmode?.alert('复制失败');
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 重新生成消息
|
|
|
- */
|
|
|
- async regenerateMessage(message: any): Promise<void> {
|
|
|
- // 找到该消息的索引
|
|
|
- const index = this.aiChatMessages.findIndex(m => m.id === message.id);
|
|
|
- if (index === -1) return;
|
|
|
-
|
|
|
- // 找到之前的用户消息
|
|
|
- let userMessageIndex = index - 1;
|
|
|
- while (userMessageIndex >= 0 && this.aiChatMessages[userMessageIndex].role !== 'user') {
|
|
|
- userMessageIndex--;
|
|
|
- }
|
|
|
-
|
|
|
- if (userMessageIndex < 0) {
|
|
|
- window?.fmode?.alert('找不到对应的用户消息');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const userMessage = this.aiChatMessages[userMessageIndex];
|
|
|
-
|
|
|
- // 移除当前AI消息
|
|
|
- this.aiChatMessages.splice(index, 1);
|
|
|
-
|
|
|
- // 重新发送
|
|
|
- this.aiChatInput = userMessage.content;
|
|
|
- await this.sendChatMessage();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 点赞消息
|
|
|
- */
|
|
|
- likeMessage(message: any): void {
|
|
|
- message.liked = !message.liked;
|
|
|
- if (message.liked) {
|
|
|
- message.disliked = false;
|
|
|
- }
|
|
|
- console.log('👍 消息反馈: 有帮助');
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 点踩消息
|
|
|
- */
|
|
|
- dislikeMessage(message: any): void {
|
|
|
- message.disliked = !message.disliked;
|
|
|
- if (message.disliked) {
|
|
|
- message.liked = false;
|
|
|
- }
|
|
|
- console.log('👎 消息反馈: 无帮助');
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清空对话
|
|
|
- */
|
|
|
- clearChat(): void {
|
|
|
- if (this.aiChatMessages.length === 0) return;
|
|
|
-
|
|
|
- if (confirm('确定要清空所有对话记录吗?')) {
|
|
|
- this.aiChatMessages = [];
|
|
|
- this.aiChatInput = '';
|
|
|
- console.log('🗑️ 对话已清空');
|
|
|
- window?.fmode?.alert('对话已清空');
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 导出对话
|
|
|
- */
|
|
|
- exportChat(): void {
|
|
|
- if (this.aiChatMessages.length === 0) {
|
|
|
- window?.fmode?.alert('没有对话记录可导出');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let exportContent = `# AI设计分析对话记录\n\n`;
|
|
|
- exportContent += `项目: ${this.project?.get('name') || '未命名项目'}\n`;
|
|
|
- exportContent += `空间: ${this.aiDesignCurrentSpace?.name || '未知空间'}\n`;
|
|
|
- exportContent += `导出时间: ${new Date().toLocaleString()}\n\n`;
|
|
|
- exportContent += `---\n\n`;
|
|
|
-
|
|
|
- this.aiChatMessages.forEach((message, index) => {
|
|
|
- const role = message.role === 'user' ? '👤 用户' : '🤖 AI助手';
|
|
|
- const time = message.timestamp.toLocaleTimeString();
|
|
|
- const content = message.content.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
|
-
|
|
|
- exportContent += `## ${role} [${time}]\n\n`;
|
|
|
- exportContent += `${content}\n\n`;
|
|
|
-
|
|
|
- if (message.images && message.images.length > 0) {
|
|
|
- exportContent += `附图: ${message.images.length} 张\n\n`;
|
|
|
- }
|
|
|
-
|
|
|
- exportContent += `---\n\n`;
|
|
|
- });
|
|
|
-
|
|
|
- // 创建下载
|
|
|
- const blob = new Blob([exportContent], { type: 'text/markdown;charset=utf-8' });
|
|
|
- const url = URL.createObjectURL(blob);
|
|
|
- const link = document.createElement('a');
|
|
|
- link.href = url;
|
|
|
- link.download = `AI对话记录_${this.aiDesignCurrentSpace?.name}_${Date.now()}.md`;
|
|
|
- document.body.appendChild(link);
|
|
|
- link.click();
|
|
|
- document.body.removeChild(link);
|
|
|
- URL.revokeObjectURL(url);
|
|
|
-
|
|
|
- console.log('💾 对话已导出');
|
|
|
- window?.fmode?.alert('对话已导出');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 确认当前分析结果
|
|
|
- */
|
|
|
- async confirmCurrentAnalysis(): Promise<void> {
|
|
|
- if (this.aiChatMessages.length === 0) {
|
|
|
- window?.fmode?.alert('没有分析结果可确认');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 整合所有AI回复作为最终报告
|
|
|
- const aiReplies = this.aiChatMessages
|
|
|
- .filter(m => m.role === 'assistant' && m.content && !m.isLoading)
|
|
|
- .map(m => m.content)
|
|
|
- .join('\n\n---\n\n');
|
|
|
-
|
|
|
- this.aiDesignReport = aiReplies;
|
|
|
- this.aiDesignReportConfirmed = true;
|
|
|
-
|
|
|
- // 保存到项目
|
|
|
- if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
- const projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- if (!projectData.designReports) {
|
|
|
- projectData.designReports = {};
|
|
|
- }
|
|
|
-
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id] = {
|
|
|
- report: this.aiDesignReport,
|
|
|
- analysisData: this.aiDesignAnalysisResult,
|
|
|
- images: this.aiDesignUploadedImages,
|
|
|
- files: this.aiDesignUploadedFiles,
|
|
|
- chatHistory: this.aiChatMessages.map(m => ({
|
|
|
- role: m.role,
|
|
|
- content: m.content,
|
|
|
- timestamp: m.timestamp.toISOString()
|
|
|
- })),
|
|
|
- confirmedAt: new Date().toISOString(),
|
|
|
- confirmedBy: this.currentUser?.id || 'unknown'
|
|
|
- };
|
|
|
-
|
|
|
- this.project.set('data', projectData);
|
|
|
- await this.project.save();
|
|
|
-
|
|
|
- console.log('✅ 分析结果已确认并保存');
|
|
|
-
|
|
|
- // 询问是否生成客户报告
|
|
|
- const result = await window?.fmode?.confirm(
|
|
|
- '分析结果已保存!\n\n是否立即生成客户报告?\n客户报告将整理分析结果为专业的结构化文档,可直接发送给客户。'
|
|
|
- );
|
|
|
-
|
|
|
- if (result) {
|
|
|
- await this.generateAndShowClientReport();
|
|
|
- } else {
|
|
|
- window?.fmode?.alert('已保存分析结果,您可以稍后在报告区域生成客户报告。');
|
|
|
- }
|
|
|
-
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('❌ 确认分析结果失败:', error);
|
|
|
- window?.fmode?.alert('确认失败,请重试');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成并显示客户报告
|
|
|
- */
|
|
|
- async generateAndShowClientReport(): Promise<void> {
|
|
|
- if (!this.aiDesignReport) {
|
|
|
- window?.fmode?.alert('请先确认分析结果');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 设置生成状态
|
|
|
- this.aiDesignGeneratingReport = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- try {
|
|
|
- console.log('🤖 正在生成客户报告...');
|
|
|
-
|
|
|
- // 调用AI服务生成结构化客户报告
|
|
|
- const clientReport = await this.designAnalysisAIService.generateClientReport({
|
|
|
- analysisData: {
|
|
|
- report: this.aiDesignReport,
|
|
|
- analysisResult: this.aiDesignAnalysisResult,
|
|
|
- spaceInfo: this.aiDesignCurrentSpace
|
|
|
- },
|
|
|
- spaceName: this.aiDesignCurrentSpace?.name || '未命名空间',
|
|
|
- onContentChange: (content) => {
|
|
|
- console.log('📝 报告生成中...', content.length, '字');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 保存客户报告
|
|
|
- if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
- const projectData = this.project.get('data') || {};
|
|
|
-
|
|
|
- if (!projectData.designReports) {
|
|
|
- projectData.designReports = {};
|
|
|
- }
|
|
|
-
|
|
|
- if (!projectData.designReports[this.aiDesignCurrentSpace.id]) {
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id] = {};
|
|
|
- }
|
|
|
-
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id].clientReport = clientReport;
|
|
|
- projectData.designReports[this.aiDesignCurrentSpace.id].clientReportGeneratedAt = new Date().toISOString();
|
|
|
-
|
|
|
- this.project.set('data', projectData);
|
|
|
- await this.project.save();
|
|
|
- }
|
|
|
-
|
|
|
- console.log('✅ 客户报告生成完成');
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- window?.fmode?.toast?.success?.('客户报告生成成功!');
|
|
|
-
|
|
|
- // 询问是否查看报告
|
|
|
- const result = await window?.fmode?.confirm(
|
|
|
- `客户报告已生成并保存!\n\n您可以:\n1. 在项目详情中查看完整报告\n2. 导出为PDF发送给客户\n3. 复制内容分享给客户\n\n是否立即查看报告?`
|
|
|
- );
|
|
|
-
|
|
|
- if (result) {
|
|
|
- // TODO: 这里可以打开报告预览对话框或跳转到报告页面
|
|
|
- window?.fmode?.alert('报告预览功能开发中,敬请期待!');
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ 生成客户报告失败:', error);
|
|
|
- window?.fmode?.alert('生成报告失败: ' + (error.message || '未知错误'));
|
|
|
- } finally {
|
|
|
- // 恢复状态
|
|
|
- this.aiDesignGeneratingReport = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 打开技能对话框
|
|
|
- */
|
|
|
- openSkillsDialog(): void {
|
|
|
- console.log('🧩 打开技能对话框');
|
|
|
- window?.fmode?.alert('技能功能开发中...');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 剪切文本
|
|
|
- */
|
|
|
- cutText(): void {
|
|
|
- const selection = window.getSelection();
|
|
|
- const selectedText = selection?.toString();
|
|
|
-
|
|
|
- if (selectedText) {
|
|
|
- // 复制到剪贴板
|
|
|
- navigator.clipboard.writeText(selectedText).then(() => {
|
|
|
- console.log('✂️ 文本已剪切:', selectedText);
|
|
|
- window?.fmode?.alert('文本已剪切');
|
|
|
-
|
|
|
- // 删除选中的文本
|
|
|
- const textarea = document.querySelector('.compact-editor-input') as HTMLTextAreaElement;
|
|
|
- if (textarea) {
|
|
|
- const start = textarea.selectionStart;
|
|
|
- const end = textarea.selectionEnd;
|
|
|
- const text = textarea.value;
|
|
|
- this.aiDesignTextDescription = text.substring(0, start) + text.substring(end);
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }).catch(err => {
|
|
|
- console.error('剪切失败:', err);
|
|
|
- });
|
|
|
- } else {
|
|
|
- window?.fmode?.alert('请先选择要剪切的文本');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 开始语音通话
|
|
|
- */
|
|
|
- startVoiceCall(): void {
|
|
|
- console.log('📞 开始语音通话');
|
|
|
- window?.fmode?.alert('语音通话功能开发中...');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 开始语音输入
|
|
|
- */
|
|
|
- startVoiceInput(): void {
|
|
|
- console.log('🎤 开始语音输入');
|
|
|
-
|
|
|
- // 检查浏览器是否支持语音识别
|
|
|
- const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
|
|
-
|
|
|
- if (!SpeechRecognition) {
|
|
|
- window?.fmode?.toast?.error?.('您的浏览器不支持语音识别功能,请使用Chrome或Edge浏览器');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const recognition = new SpeechRecognition();
|
|
|
- recognition.lang = 'zh-CN'; // 中文识别
|
|
|
- recognition.continuous = false; // 单次识别
|
|
|
- recognition.interimResults = false; // 不显示中间结果
|
|
|
-
|
|
|
- recognition.onstart = () => {
|
|
|
- console.log('🎤 语音识别已启动');
|
|
|
- window?.fmode?.toast?.success?.('🎤 正在录音,请说话...');
|
|
|
- };
|
|
|
-
|
|
|
- recognition.onresult = (event: any) => {
|
|
|
- const transcript = event.results[0][0].transcript;
|
|
|
- const confidence = event.results[0][0].confidence;
|
|
|
- console.log('🎤 识别结果:', transcript, '置信度:', confidence);
|
|
|
-
|
|
|
- // 将识别结果添加到对话输入框
|
|
|
- if (this.aiChatInput) {
|
|
|
- this.aiChatInput += ' ' + transcript;
|
|
|
- } else {
|
|
|
- this.aiChatInput = transcript;
|
|
|
- }
|
|
|
-
|
|
|
- // 更新UI
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- // 聚焦到输入框
|
|
|
- if (this.chatInputElement) {
|
|
|
- this.chatInputElement.nativeElement.focus();
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- window?.fmode?.toast?.success?.(`✅ 识别成功: ${transcript}`);
|
|
|
- };
|
|
|
-
|
|
|
- recognition.onerror = (event: any) => {
|
|
|
- console.error('🎤 语音识别错误:', event.error);
|
|
|
-
|
|
|
- // 根据不同的错误类型提供友好的提示
|
|
|
- let errorMessage = '语音识别失败';
|
|
|
-
|
|
|
- switch (event.error) {
|
|
|
- case 'no-speech':
|
|
|
- errorMessage = '未检测到语音,请重试并大声说话';
|
|
|
- break;
|
|
|
- case 'audio-capture':
|
|
|
- errorMessage = '无法访问麦克风,请检查权限设置';
|
|
|
- break;
|
|
|
- case 'not-allowed':
|
|
|
- errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许麦克风访问';
|
|
|
- break;
|
|
|
- case 'network':
|
|
|
- errorMessage = '网络错误,请检查网络连接';
|
|
|
- break;
|
|
|
- case 'aborted':
|
|
|
- errorMessage = '语音识别已中止';
|
|
|
- break;
|
|
|
- default:
|
|
|
- errorMessage = `语音识别错误: ${event.error}`;
|
|
|
- }
|
|
|
-
|
|
|
- window?.fmode?.toast?.error?.(errorMessage);
|
|
|
- };
|
|
|
-
|
|
|
- recognition.onend = () => {
|
|
|
- console.log('🎤 语音识别已结束');
|
|
|
- };
|
|
|
-
|
|
|
- try {
|
|
|
- recognition.start();
|
|
|
- } catch (error: any) {
|
|
|
- console.error('启动语音识别失败:', error);
|
|
|
-
|
|
|
- // 检查是否是因为正在进行中
|
|
|
- if (error.message && error.message.includes('already started')) {
|
|
|
- window?.fmode?.toast?.warning?.('语音识别已在进行中');
|
|
|
- } else {
|
|
|
- window?.fmode?.toast?.error?.('启动语音识别失败,请重试');
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 导出AI分析报告为Word文档
|
|
|
- */
|
|
|
- async exportReportToWord(): Promise<void> {
|
|
|
- if (!this.aiDesignReport && !this.aiDesignAnalysisResult) {
|
|
|
- window?.fmode?.alert('没有可导出的报告内容');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.exportingWord = true;
|
|
|
- this.cdr.markForCheck();
|
|
|
-
|
|
|
- try {
|
|
|
- // 动态导入docx库
|
|
|
- const { Document, Paragraph, TextRun, HeadingLevel, AlignmentType, Packer } = await import('docx');
|
|
|
-
|
|
|
- console.log('📄 开始生成Word文档...');
|
|
|
-
|
|
|
- // 准备报告内容
|
|
|
- const reportContent = this.aiDesignReport || this.aiDesignAnalysisResult?.formattedContent || this.aiDesignAnalysisResult?.rawContent || '';
|
|
|
-
|
|
|
- if (!reportContent) {
|
|
|
- window?.fmode?.alert('报告内容为空,无法导出');
|
|
|
- this.exportingWord = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 解析报告内容为段落
|
|
|
- const paragraphs: any[] = [];
|
|
|
-
|
|
|
- // 添加文档标题
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- text: 'AI设计分析报告',
|
|
|
- heading: HeadingLevel.HEADING_1,
|
|
|
- alignment: AlignmentType.CENTER,
|
|
|
- spacing: { after: 400 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 添加项目信息
|
|
|
- if (this.project) {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: '项目信息',
|
|
|
- bold: true,
|
|
|
- size: 28
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 200, after: 200 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: `项目名称: ${this.project.name || '未命名项目'}`,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 100 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- if (this.customer) {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: `客户姓名: ${this.customer.name || '未知'}`,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 100 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- if (this.aiDesignCurrentSpace) {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: `分析空间: ${this.getSpaceDisplayName(this.aiDesignCurrentSpace)}`,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 100 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: `生成时间: ${new Date().toLocaleString('zh-CN')}`,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 400 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 添加分隔线
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- text: '———————————————————————————————————————————',
|
|
|
- alignment: AlignmentType.CENTER,
|
|
|
- spacing: { before: 200, after: 200 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 🔥 添加快速总结(如果有)
|
|
|
- if (this.aiDesignAnalysisResult?.structuredData?.quickSummary) {
|
|
|
- const qs = this.aiDesignAnalysisResult.structuredData.quickSummary;
|
|
|
-
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: '【图片分析总结】',
|
|
|
- bold: true,
|
|
|
- size: 32,
|
|
|
- color: 'FF6600'
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 300, after: 200 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 色彩基调
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: '🎨 色彩基调: ',
|
|
|
- bold: true,
|
|
|
- size: 24
|
|
|
- }),
|
|
|
- new TextRun({
|
|
|
- text: qs.colorTone,
|
|
|
- size: 24,
|
|
|
- color: 'D84315'
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 150 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 主要材质
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: '🪵 主要材质: ',
|
|
|
- bold: true,
|
|
|
- size: 24
|
|
|
- }),
|
|
|
- new TextRun({
|
|
|
- text: qs.mainMaterials,
|
|
|
- size: 24,
|
|
|
- color: '5D4037'
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 150 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 整体氛围
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: '✨ 整体氛围: ',
|
|
|
- bold: true,
|
|
|
- size: 24
|
|
|
- }),
|
|
|
- new TextRun({
|
|
|
- text: qs.atmosphere,
|
|
|
- size: 24,
|
|
|
- color: '1976D2'
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { after: 400 }
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // 添加分隔线
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- text: '———————————————————————————————————————————',
|
|
|
- alignment: AlignmentType.CENTER,
|
|
|
- spacing: { before: 200, after: 300 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 解析并添加报告内容
|
|
|
- const lines = reportContent.split('\n');
|
|
|
-
|
|
|
- for (let i = 0; i < lines.length; i++) {
|
|
|
- const line = lines[i].trim();
|
|
|
-
|
|
|
- if (!line) {
|
|
|
- // 空行,添加一个空段落
|
|
|
- paragraphs.push(new Paragraph({ text: '' }));
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否是标题(一、二、三等)
|
|
|
- const titleMatch = line.match(/^([一二三四五六七八九十]+、)(.+)$/);
|
|
|
- if (titleMatch) {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: line,
|
|
|
- bold: true,
|
|
|
- size: 28
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 300, after: 200 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
- // 检查是否是子标题(包含":"的行)
|
|
|
- else if (line.includes(':') || line.includes(':')) {
|
|
|
- const parts = line.split(/[::]/);
|
|
|
- if (parts.length === 2) {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: parts[0] + ':',
|
|
|
- bold: true,
|
|
|
- size: 24
|
|
|
- }),
|
|
|
- new TextRun({
|
|
|
- text: parts[1],
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 150, after: 150 }
|
|
|
- })
|
|
|
- );
|
|
|
- } else {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: line,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 100, after: 100 }
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- // 普通段落
|
|
|
- else {
|
|
|
- paragraphs.push(
|
|
|
- new Paragraph({
|
|
|
- children: [
|
|
|
- new TextRun({
|
|
|
- text: line,
|
|
|
- size: 24
|
|
|
- })
|
|
|
- ],
|
|
|
- spacing: { before: 100, after: 100 },
|
|
|
- alignment: AlignmentType.LEFT
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 创建文档
|
|
|
- const doc = new Document({
|
|
|
- sections: [{
|
|
|
- properties: {},
|
|
|
- children: paragraphs
|
|
|
- }]
|
|
|
- });
|
|
|
-
|
|
|
- // 生成Blob
|
|
|
- const blob = await Packer.toBlob(doc);
|
|
|
-
|
|
|
- // 生成文件名
|
|
|
- const spaceName = this.aiDesignCurrentSpace ? this.getSpaceDisplayName(this.aiDesignCurrentSpace) : '未知空间';
|
|
|
- const projectName = this.project?.name || '未命名项目';
|
|
|
- const timestamp = new Date().toISOString().slice(0, 10);
|
|
|
- const fileName = `${projectName}-${spaceName}-AI设计分析报告-${timestamp}.docx`;
|
|
|
-
|
|
|
- // 下载文件
|
|
|
- const url = window.URL.createObjectURL(blob);
|
|
|
- const link = document.createElement('a');
|
|
|
- link.href = url;
|
|
|
- link.download = fileName;
|
|
|
- link.click();
|
|
|
-
|
|
|
- // 清理
|
|
|
- window.URL.revokeObjectURL(url);
|
|
|
-
|
|
|
- console.log('✅ Word文档已生成并下载');
|
|
|
- window?.fmode?.toast?.success?.('Word文档导出成功!');
|
|
|
-
|
|
|
- } catch (error: any) {
|
|
|
- console.error('❌ 导出Word文档失败:', error);
|
|
|
- window?.fmode?.alert(`导出失败: ${error.message || '未知错误'}\n\n提示:请确保已安装docx依赖包`);
|
|
|
- } finally {
|
|
|
- this.exportingWord = false;
|
|
|
- this.cdr.markForCheck();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 压缩图片
|
|
|
- * @param file 原始图片文件
|
|
|
- * @returns 压缩后的Blob文件
|
|
|
- */
|
|
|
- private async compressImage(file: File): Promise<File> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const reader = new FileReader();
|
|
|
-
|
|
|
- reader.onload = (e: any) => {
|
|
|
- const img = new Image();
|
|
|
-
|
|
|
- img.onload = () => {
|
|
|
- // 创建canvas
|
|
|
- const canvas = document.createElement('canvas');
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
-
|
|
|
- if (!ctx) {
|
|
|
- reject(new Error('无法创建Canvas上下文'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 计算压缩后的尺寸
|
|
|
- let width = img.width;
|
|
|
- let height = img.height;
|
|
|
- const maxDimension = 2048; // 最大宽度或高度
|
|
|
-
|
|
|
- // 如果图片尺寸超过限制,按比例缩小
|
|
|
- if (width > maxDimension || height > maxDimension) {
|
|
|
- if (width > height) {
|
|
|
- height = (height / width) * maxDimension;
|
|
|
- width = maxDimension;
|
|
|
- } else {
|
|
|
- width = (width / height) * maxDimension;
|
|
|
- height = maxDimension;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 设置canvas尺寸
|
|
|
- canvas.width = width;
|
|
|
- canvas.height = height;
|
|
|
-
|
|
|
- // 绘制图片
|
|
|
- ctx.drawImage(img, 0, 0, width, height);
|
|
|
-
|
|
|
- // 转换为Blob,质量设置为0.7(可调整)
|
|
|
- canvas.toBlob(
|
|
|
- (blob) => {
|
|
|
- if (!blob) {
|
|
|
- reject(new Error('图片压缩失败'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 创建新的File对象
|
|
|
- const compressedFile = new File([blob], file.name, {
|
|
|
- type: 'image/jpeg', // 统一转为JPEG以获得更好的压缩率
|
|
|
- lastModified: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`📊 压缩效果: ${(file.size / 1024 / 1024).toFixed(2)}MB → ${(compressedFile.size / 1024 / 1024).toFixed(2)}MB`);
|
|
|
- console.log(`📊 压缩比例: ${((1 - compressedFile.size / file.size) * 100).toFixed(1)}%`);
|
|
|
-
|
|
|
- resolve(compressedFile);
|
|
|
- },
|
|
|
- 'image/jpeg',
|
|
|
- 0.7 // 质量参数:0.7表示70%质量,平衡质量和文件大小
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- img.onerror = () => {
|
|
|
- reject(new Error('图片加载失败'));
|
|
|
- };
|
|
|
-
|
|
|
- img.src = e.target.result;
|
|
|
- };
|
|
|
-
|
|
|
- reader.onerror = () => {
|
|
|
- reject(new Error('文件读取失败'));
|
|
|
- };
|
|
|
-
|
|
|
- reader.readAsDataURL(file);
|
|
|
- });
|
|
|
- }
|
|
|
-}
|