|
|
@@ -1,25 +1,35 @@
|
|
|
-import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild } from '@angular/core';
|
|
|
+import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ElementRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
-import { WxworkAuth, FmodeParse } from 'fmode-ng/core';
|
|
|
+import { WxworkAuth, FmodeParse, NovaStorage } 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 { AiDesignAnalysisComponent } from './components/ai-design-analysis/ai-design-analysis.component';
|
|
|
-import { SpaceRequirementItemComponent } from './components/space-requirement-item/space-requirement-item.component';
|
|
|
+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');
|
|
|
+
|
|
|
+addIcons({
|
|
|
+ add,sparkles,colorPalette,trash,chevronDown,send
|
|
|
+})
|
|
|
+/**
|
|
|
+ * 确认需求阶段组件 - Product表统一空间管理
|
|
|
+ */
|
|
|
@Component({
|
|
|
selector: 'app-stage-requirements',
|
|
|
standalone: true,
|
|
|
- imports: [
|
|
|
- CommonModule,
|
|
|
- FormsModule,
|
|
|
- ReactiveFormsModule,
|
|
|
- AiDesignAnalysisComponent,
|
|
|
- SpaceRequirementItemComponent
|
|
|
- ],
|
|
|
+ imports: [CommonModule, FormsModule, ReactiveFormsModule, IonIcon],
|
|
|
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
|
+ providers: [],
|
|
|
templateUrl: './stage-requirements.component.html',
|
|
|
styleUrls: ['./stage-requirements.component.scss'],
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
@@ -30,31 +40,91 @@ 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[] } = {};
|
|
|
|
|
|
- // File Data
|
|
|
+ // 全局需求
|
|
|
+ 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[] = [];
|
|
|
+
|
|
|
+ // 参考图片(支持按空间分类)
|
|
|
referenceImages: Array<{
|
|
|
id: string;
|
|
|
url: string;
|
|
|
name: string;
|
|
|
- type: string;
|
|
|
+ type: string; // style | space | material
|
|
|
uploadTime: Date;
|
|
|
description?: string;
|
|
|
spaceId?: string;
|
|
|
tags: string[];
|
|
|
- projectFile?: any;
|
|
|
+ projectFile?: any; // ProjectFile对象引用
|
|
|
}> = [];
|
|
|
|
|
|
+ // CAD文件
|
|
|
cadFiles: Array<{
|
|
|
id: string;
|
|
|
url: string;
|
|
|
@@ -62,16 +132,123 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
uploadTime: Date;
|
|
|
size: number;
|
|
|
spaceId?: string;
|
|
|
- projectFile?: any;
|
|
|
+ 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;
|
|
|
}> = [];
|
|
|
+ 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 Analysis Data
|
|
|
- aiAnalysisResults: any = {};
|
|
|
+ // AI分析配置
|
|
|
+ private readonly AI_MODEL = 'fmode-1.6-cn';
|
|
|
|
|
|
+ // 加载状态
|
|
|
loading: boolean = true;
|
|
|
uploading: boolean = false;
|
|
|
+ generating: boolean = false;
|
|
|
+ saving: boolean = false;
|
|
|
|
|
|
- @ViewChild(AiDesignAnalysisComponent) aiDesignComponent!: AiDesignAnalysisComponent;
|
|
|
+ // 模板引用变量
|
|
|
+ @ViewChild('chatMessages') chatMessagesContainer!: ElementRef;
|
|
|
|
|
|
constructor(
|
|
|
private route: ActivatedRoute,
|
|
|
@@ -83,67 +260,145 @@ 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('权限检查失败', e);
|
|
|
+ console.error('❌ 权限检查失败,保留默认canEdit:', this.canEdit, 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);
|
|
|
- } catch (error: any) {
|
|
|
- console.error('加载项目失败:', error);
|
|
|
+ console.log('✅ [确认需求] 项目信息加载成功:', {
|
|
|
+ projectId: this.project.id,
|
|
|
+ name: this.project.get('name'),
|
|
|
+ currentStage: this.project.get('currentStage')
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ [确认需求] 加载项目失败:', error);
|
|
|
window?.fmode?.alert('加载项目失败: ' + (error.message || '未知错误'));
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!this.project) return;
|
|
|
+ if (!this.project) {
|
|
|
+ console.warn('⚠️ [确认需求] 项目对象为空,无法加载数据');
|
|
|
+ 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,
|
|
|
@@ -161,31 +416,77 @@ 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) {
|
|
|
@@ -195,164 +496,289 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
+ // ===== 多空间需求管理方法 =====
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换需求分段
|
|
|
+ */
|
|
|
+ onRequirementsSegmentChange(event: any): void {
|
|
|
+ this.requirementsSegment = event.detail.value;
|
|
|
+ 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 || [];
|
|
|
- });
|
|
|
- }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 选择需求分段
|
|
|
+ */
|
|
|
+ selectRequirementsSegment(segment: string): void {
|
|
|
+ this.requirementsSegment = segment;
|
|
|
+ this.cdr.markForCheck();
|
|
|
}
|
|
|
|
|
|
- // Space List Helpers
|
|
|
- getSpaceReferenceImages(spaceId: string): any[] {
|
|
|
- return this.referenceImages.filter(img => img.spaceId === spaceId);
|
|
|
+ /**
|
|
|
+ * 选择产品空间
|
|
|
+ */
|
|
|
+ selectProduct(productId: string): void {
|
|
|
+ this.activeProductId = productId;
|
|
|
+ this.cdr.markForCheck();
|
|
|
}
|
|
|
|
|
|
- getSpaceCADFiles(spaceId: string): any[] {
|
|
|
- return this.cadFiles.filter(file => file.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();
|
|
|
}
|
|
|
-
|
|
|
- getSpaceAnalysisResults(spaceId: string): any[] {
|
|
|
- return this.analysisResultsBySpace[spaceId] || [];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 选择图片类型标签
|
|
|
+ */
|
|
|
+ selectImageTab(spaceId: string, tabId: string): void {
|
|
|
+ this.activeImageTab[spaceId] = tabId;
|
|
|
+ this.cdr.markForCheck();
|
|
|
}
|
|
|
|
|
|
- isSpaceExpanded(spaceId: string): boolean {
|
|
|
- return this.expandedSpaces.has(spaceId);
|
|
|
+ /**
|
|
|
+ * 获取空间的总文件数(图片+CAD)
|
|
|
+ */
|
|
|
+ getTotalSpaceFileCount(spaceId: string): number {
|
|
|
+ const imageCount = this.getSpaceReferenceImages(spaceId).length;
|
|
|
+ const cadCount = this.getSpaceCADFiles(spaceId).length;
|
|
|
+ return imageCount + cadCount;
|
|
|
}
|
|
|
|
|
|
- toggleSpaceExpansion(spaceId: string) {
|
|
|
- if (this.expandedSpaces.has(spaceId)) {
|
|
|
- this.expandedSpaces.delete(spaceId);
|
|
|
+ /**
|
|
|
+ * 获取指定空间和类型的图片数量
|
|
|
+ */
|
|
|
+ getImageCountByType(spaceId: string, typeId: string): number {
|
|
|
+ if (typeId === 'all') {
|
|
|
+ return this.getSpaceReferenceImages(spaceId).length;
|
|
|
+ } else if (typeId === 'cad') {
|
|
|
+ return this.getSpaceCADFiles(spaceId).length;
|
|
|
} else {
|
|
|
- this.expandedSpaces.add(spaceId);
|
|
|
+ return this.getImagesByType(spaceId, typeId).length;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Event Handlers from Children
|
|
|
- onAnalysisComplete(result: any) {
|
|
|
- console.log('AI Analysis Complete', result);
|
|
|
- this.loadData();
|
|
|
+ /**
|
|
|
+ * 按类型获取图片
|
|
|
+ */
|
|
|
+ getImagesByType(spaceId: string, type: string): any[] {
|
|
|
+ return this.getSpaceReferenceImages(spaceId).filter(img => img.type === type);
|
|
|
}
|
|
|
|
|
|
- async handleUploadImages(event: {spaceId: string, files: File[], type?: string}) {
|
|
|
- console.log('Upload Images', event);
|
|
|
- await this.uploadAndAnalyzeImages(event.files, event.spaceId, event.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;
|
|
|
}
|
|
|
|
|
|
- async handleUploadCAD(event: {spaceId: string, files: File[]}) {
|
|
|
- console.log('Upload CAD', event);
|
|
|
- await this.uploadCADFiles(event.files, event.spaceId);
|
|
|
+ /**
|
|
|
+ * 获取图片类型徽章样式类
|
|
|
+ */
|
|
|
+ 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';
|
|
|
}
|
|
|
|
|
|
- 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('删除失败');
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 拖拽悬停处理
|
|
|
+ */
|
|
|
+ onDragOver(event: DragEvent, spaceId: string): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.isDragOver = true;
|
|
|
+ this.dragOverSpaceId = 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('删除失败');
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 拖拽离开处理
|
|
|
+ */
|
|
|
+ onDragLeave(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.isDragOver = false;
|
|
|
+ this.dragOverSpaceId = '';
|
|
|
}
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 拖拽放下处理
|
|
|
+ */
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传CAD文件
|
|
|
+ if (cadFiles.length > 0) {
|
|
|
+ await this.uploadCADFiles(cadFiles, spaceId);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
}
|
|
|
|
|
|
- handleViewAnalysis(imageId: string) {
|
|
|
- console.log('View Analysis for', imageId);
|
|
|
- window?.fmode?.toast('查看分析详情功能开发中');
|
|
|
+ /**
|
|
|
+ * 判断是否为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;
|
|
|
}
|
|
|
|
|
|
- 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' });
|
|
|
+ /**
|
|
|
+ * 上传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
|
|
|
+ };
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
}
|
|
|
- // Select the space in AI component
|
|
|
- this.aiDesignComponent.selectAISpace(space);
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('CAD文件上传失败:', error);
|
|
|
+ window?.fmode?.alert('CAD文件上传失败,请重试');
|
|
|
+ } finally {
|
|
|
+ this.uploading = false;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- 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,
|
|
|
@@ -362,64 +788,4074 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
{
|
|
|
uploadedFor: 'requirements_analysis',
|
|
|
spaceId: spaceId,
|
|
|
- imageType: type
|
|
|
+ uploadStage: 'requirements'
|
|
|
+ },
|
|
|
+ (progress) => {
|
|
|
+ console.log(`上传进度: ${progress}%`);
|
|
|
}
|
|
|
);
|
|
|
|
|
|
- this.referenceImages.push({
|
|
|
+ // 创建参考图片记录
|
|
|
+ const uploadedFile = {
|
|
|
id: projectFile.id || '',
|
|
|
url: projectFile.get('fileUrl') || '',
|
|
|
name: projectFile.get('fileName') || file.name,
|
|
|
- type: type,
|
|
|
+ type: 'other', // 默认类型,AI分析后会更新
|
|
|
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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async uploadCADFiles(files: File[], spaceId: string): Promise<void> {
|
|
|
- this.uploading = true;
|
|
|
- const targetProjectId = this.projectId || this.project?.id;
|
|
|
+ /**
|
|
|
+ * 对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 {
|
|
|
- for (const file of files) {
|
|
|
+ // 调用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;
|
|
|
+
|
|
|
+ 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上传到服务器
|
|
|
const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
file,
|
|
|
targetProjectId,
|
|
|
- 'cad_file',
|
|
|
- spaceId,
|
|
|
+ 'reference_image',
|
|
|
+ targetProductId,
|
|
|
'requirements',
|
|
|
{
|
|
|
- uploadedFor: 'requirements_cad',
|
|
|
- spaceId: spaceId
|
|
|
+ imageType: finalImageType,
|
|
|
+ uploadedFor: 'requirements_analysis',
|
|
|
+ spaceId: targetProductId,
|
|
|
+ deliveryType: 'requirements_reference',
|
|
|
+ uploadStage: 'requirements'
|
|
|
+ },
|
|
|
+ (progress) => {
|
|
|
+ console.log(`上传进度: ${progress}%`);
|
|
|
}
|
|
|
);
|
|
|
-
|
|
|
- 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
|
|
|
+
|
|
|
+ // 为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: []
|
|
|
+ }
|
|
|
});
|
|
|
+ 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('CAD上传失败:', error);
|
|
|
- window?.fmode?.alert('CAD上传失败');
|
|
|
+ console.error('上传失败:', error);
|
|
|
+ window?.fmode?.alert('文件上传失败,请重试');
|
|
|
} 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);
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|