drag-upload-modal.component.ts 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218
  1. import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, OnChanges, OnDestroy, SimpleChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { ImageAnalysisService, ImageAnalysisResult } from '../../services/image-analysis.service';
  5. /**
  6. * 上传文件接口(增强版)
  7. */
  8. export interface UploadFile {
  9. file: File;
  10. id: string;
  11. name: string;
  12. size: number;
  13. type: string;
  14. preview?: string;
  15. fileUrl?: string; // 🔥 添加:上传后的文件URL
  16. status: 'pending' | 'analyzing' | 'uploading' | 'success' | 'error';
  17. progress: number;
  18. error?: string;
  19. analysisResult?: ImageAnalysisResult; // 图片分析结果
  20. suggestedStage?: string; // AI建议的阶段分类
  21. suggestedSpace?: string; // AI建议的空间(暂未实现)
  22. imageLoadError?: boolean; // 🔥 图片加载错误标记
  23. // 用户选择的空间和阶段(可修改)
  24. selectedSpace?: string;
  25. selectedStage?: string;
  26. }
  27. /**
  28. * 上传结果接口(增强版)
  29. */
  30. export interface UploadResult {
  31. files: Array<{
  32. file: UploadFile;
  33. spaceId: string;
  34. spaceName: string;
  35. stageType: string;
  36. stageName: string;
  37. // 新增:提交信息跟踪字段
  38. analysisResult?: ImageAnalysisResult;
  39. submittedAt?: string;
  40. submittedBy?: string;
  41. submittedByName?: string;
  42. deliveryListId?: string;
  43. }>;
  44. }
  45. /**
  46. * 空间选项接口
  47. */
  48. export interface SpaceOption {
  49. id: string;
  50. name: string;
  51. }
  52. /**
  53. * 阶段选项接口
  54. */
  55. export interface StageOption {
  56. id: string;
  57. name: string;
  58. }
  59. @Component({
  60. selector: 'app-drag-upload-modal',
  61. standalone: true,
  62. imports: [CommonModule, FormsModule],
  63. templateUrl: './drag-upload-modal.component.html',
  64. styleUrls: ['./drag-upload-modal.component.scss'],
  65. changeDetection: ChangeDetectionStrategy.OnPush
  66. })
  67. export class DragUploadModalComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
  68. @Input() visible: boolean = false;
  69. @Input() droppedFiles: File[] = [];
  70. @Input() availableSpaces: SpaceOption[] = []; // 可用空间列表
  71. @Input() availableStages: StageOption[] = []; // 可用阶段列表
  72. @Input() targetSpaceId: string = ''; // 拖拽目标空间ID
  73. @Input() targetSpaceName: string = ''; // 拖拽目标空间名称
  74. @Input() targetStageType: string = ''; // 拖拽目标阶段类型
  75. @Input() targetStageName: string = ''; // 拖拽目标阶段名称
  76. @Output() close = new EventEmitter<void>();
  77. @Output() confirm = new EventEmitter<UploadResult>();
  78. @Output() cancel = new EventEmitter<void>();
  79. // 上传文件列表
  80. uploadFiles: UploadFile[] = [];
  81. // 上传状态
  82. isUploading: boolean = false;
  83. uploadProgress: number = 0;
  84. uploadSuccess: boolean = false;
  85. uploadMessage: string = '';
  86. // 图片分析状态
  87. isAnalyzing: boolean = false;
  88. analysisProgress: string = '';
  89. analysisComplete: boolean = false;
  90. // JSON格式预览模式
  91. showJsonPreview: boolean = false;
  92. jsonPreviewData: any[] = [];
  93. // 🔥 图片查看器
  94. viewingImage: UploadFile | null = null;
  95. constructor(
  96. private cdr: ChangeDetectorRef,
  97. private imageAnalysisService: ImageAnalysisService
  98. ) {}
  99. ngOnInit() {
  100. console.log('🚀 DragUploadModal 初始化', {
  101. visible: this.visible,
  102. droppedFilesCount: this.droppedFiles.length,
  103. targetSpace: this.targetSpaceName,
  104. targetStage: this.targetStageName
  105. });
  106. }
  107. ngOnChanges(changes: SimpleChanges) {
  108. // 🔥 优化:只在关键变化时输出日志,避免控制台刷屏
  109. if (changes['visible'] || changes['droppedFiles']) {
  110. console.log('🔄 ngOnChanges (关键变化)', {
  111. visible: this.visible,
  112. droppedFilesCount: this.droppedFiles.length
  113. });
  114. }
  115. // 当弹窗显示或文件发生变化时处理
  116. if (changes['visible'] && this.visible && this.droppedFiles.length > 0) {
  117. console.log('📎 弹窗显示,开始处理文件');
  118. this.processDroppedFiles();
  119. } else if (changes['droppedFiles'] && this.droppedFiles.length > 0 && this.visible) {
  120. console.log('📎 文件变化,开始处理文件');
  121. this.processDroppedFiles();
  122. }
  123. }
  124. ngAfterViewInit() {
  125. // AI分析将在图片预览生成完成后自动开始
  126. // 不需要在这里手动启动
  127. }
  128. /**
  129. * 处理拖拽的文件
  130. */
  131. private async processDroppedFiles() {
  132. console.log('📎 开始处理拖拽文件:', {
  133. droppedFilesCount: this.droppedFiles.length,
  134. files: this.droppedFiles.map(f => ({ name: f.name, type: f.type, size: f.size }))
  135. });
  136. if (this.droppedFiles.length === 0) {
  137. console.warn('⚠️ 没有文件需要处理');
  138. return;
  139. }
  140. this.uploadFiles = this.droppedFiles.map((file, index) => ({
  141. file,
  142. id: `upload_${Date.now()}_${index}`,
  143. name: file.name,
  144. size: file.size,
  145. type: file.type,
  146. status: 'pending' as const,
  147. progress: 0,
  148. // 初始化选择的空间和阶段为空,等待AI分析或用户选择
  149. selectedSpace: '',
  150. selectedStage: ''
  151. }));
  152. console.log('🖼️ 开始生成图片预览...', {
  153. uploadFilesCount: this.uploadFiles.length,
  154. imageFiles: this.uploadFiles.filter(f => this.isImageFile(f.file)).map(f => f.name)
  155. });
  156. // 为图片文件生成预览
  157. const previewPromises = [];
  158. for (const uploadFile of this.uploadFiles) {
  159. if (this.isImageFile(uploadFile.file)) {
  160. console.log(`🖼️ 开始为 ${uploadFile.name} 生成预览`);
  161. previewPromises.push(this.generatePreview(uploadFile));
  162. } else {
  163. console.log(`📄 ${uploadFile.name} 不是图片文件,跳过预览生成`);
  164. }
  165. }
  166. try {
  167. // 等待所有预览生成完成
  168. await Promise.all(previewPromises);
  169. console.log('✅ 所有图片预览生成完成');
  170. // 检查预览生成结果
  171. this.uploadFiles.forEach(file => {
  172. if (this.isImageFile(file.file)) {
  173. console.log(`🖼️ ${file.name} 预览状态:`, {
  174. hasPreview: !!file.preview,
  175. previewLength: file.preview ? file.preview.length : 0
  176. });
  177. }
  178. });
  179. } catch (error) {
  180. console.error('❌ 图片预览生成失败:', error);
  181. }
  182. this.cdr.markForCheck();
  183. // 预览生成完成后,延迟一点开始AI分析
  184. setTimeout(() => {
  185. this.startAutoAnalysis();
  186. }, 300);
  187. }
  188. /**
  189. * 生成图片预览
  190. * 🔥 企业微信环境优先使用ObjectURL,避免CSP策略限制base64
  191. */
  192. private generatePreview(uploadFile: UploadFile): Promise<void> {
  193. return new Promise((resolve, reject) => {
  194. try {
  195. // 🔥 企业微信环境检测
  196. const isWxWork = this.isWxWorkEnvironment();
  197. if (isWxWork) {
  198. // 🔥 企业微信环境:直接使用ObjectURL(更快、更可靠)
  199. try {
  200. const objectUrl = URL.createObjectURL(uploadFile.file);
  201. uploadFile.preview = objectUrl;
  202. console.log(`✅ 图片预览生成成功 (ObjectURL): ${uploadFile.name}`, {
  203. objectUrl: objectUrl,
  204. environment: 'wxwork'
  205. });
  206. this.cdr.markForCheck();
  207. resolve();
  208. } catch (error) {
  209. console.error(`❌ ObjectURL生成失败: ${uploadFile.name}`, error);
  210. uploadFile.preview = undefined;
  211. this.cdr.markForCheck();
  212. resolve();
  213. }
  214. } else {
  215. // 🔥 非企业微信环境:使用base64 dataURL(兼容性更好)
  216. const reader = new FileReader();
  217. reader.onload = (e) => {
  218. try {
  219. const result = e.target?.result as string;
  220. if (result && result.startsWith('data:image')) {
  221. uploadFile.preview = result;
  222. console.log(`✅ 图片预览生成成功 (Base64): ${uploadFile.name}`, {
  223. previewLength: result.length,
  224. isBase64: result.includes('base64'),
  225. mimeType: result.substring(5, result.indexOf(';'))
  226. });
  227. this.cdr.markForCheck();
  228. resolve();
  229. } else {
  230. console.error(`❌ 预览数据格式错误: ${uploadFile.name}`, result?.substring(0, 50));
  231. uploadFile.preview = undefined; // 清除无效预览
  232. this.cdr.markForCheck();
  233. resolve(); // 仍然resolve,不阻塞流程
  234. }
  235. } catch (error) {
  236. console.error(`❌ 处理预览数据失败: ${uploadFile.name}`, error);
  237. uploadFile.preview = undefined;
  238. this.cdr.markForCheck();
  239. resolve();
  240. }
  241. };
  242. reader.onerror = (error) => {
  243. console.error(`❌ FileReader读取失败: ${uploadFile.name}`, error);
  244. uploadFile.preview = undefined;
  245. this.cdr.markForCheck();
  246. resolve(); // 不要reject,避免中断整个流程
  247. };
  248. reader.readAsDataURL(uploadFile.file);
  249. }
  250. } catch (error) {
  251. console.error(`❌ 图片预览生成初始化失败: ${uploadFile.name}`, error);
  252. uploadFile.preview = undefined;
  253. this.cdr.markForCheck();
  254. resolve();
  255. }
  256. });
  257. }
  258. /**
  259. * 检查是否为图片文件
  260. */
  261. isImageFile(file: File): boolean {
  262. return file.type.startsWith('image/');
  263. }
  264. /**
  265. * 自动开始AI分析
  266. */
  267. private async startAutoAnalysis(): Promise<void> {
  268. console.log('🤖 开始自动AI分析...');
  269. // 🔥 使用真实AI分析(豆包1.6视觉识别)
  270. await this.startImageAnalysis();
  271. // 分析完成后,自动设置空间和阶段
  272. this.autoSetSpaceAndStage();
  273. }
  274. /**
  275. * 自动设置空间和阶段(增强版,支持AI智能分类)
  276. */
  277. private autoSetSpaceAndStage(): void {
  278. for (const file of this.uploadFiles) {
  279. // 🤖 优先使用AI分析结果进行智能分类
  280. if (file.analysisResult) {
  281. // 使用AI推荐的空间
  282. if (this.targetSpaceId) {
  283. // 如果有指定的目标空间,使用指定空间
  284. file.selectedSpace = this.targetSpaceId;
  285. console.log(`🎯 使用指定空间: ${this.targetSpaceName}`);
  286. } else {
  287. // 否则使用AI推荐的空间
  288. const suggestedSpace = this.inferSpaceFromAnalysis(file.analysisResult);
  289. const spaceOption = this.availableSpaces.find(space =>
  290. space.name === suggestedSpace || space.name.includes(suggestedSpace)
  291. );
  292. if (spaceOption) {
  293. file.selectedSpace = spaceOption.id;
  294. console.log(`🤖 AI推荐空间: ${suggestedSpace}`);
  295. } else if (this.availableSpaces.length > 0) {
  296. file.selectedSpace = this.availableSpaces[0].id;
  297. }
  298. }
  299. // 🎯 使用AI推荐的阶段(这是核心功能)
  300. if (file.suggestedStage) {
  301. file.selectedStage = file.suggestedStage;
  302. console.log(`🤖 AI推荐阶段: ${file.name} -> ${file.suggestedStage} (置信度: ${file.analysisResult.content.confidence}%)`);
  303. }
  304. } else {
  305. // 如果没有AI分析结果,使用默认值
  306. if (this.targetSpaceId) {
  307. file.selectedSpace = this.targetSpaceId;
  308. } else if (this.availableSpaces.length > 0) {
  309. file.selectedSpace = this.availableSpaces[0].id;
  310. }
  311. if (this.targetStageType) {
  312. file.selectedStage = this.targetStageType;
  313. } else {
  314. file.selectedStage = 'white_model'; // 默认白模阶段
  315. }
  316. }
  317. }
  318. console.log('✅ AI智能分类完成');
  319. this.cdr.markForCheck();
  320. }
  321. /**
  322. * 生成JSON格式预览数据
  323. */
  324. private generateJsonPreview(): void {
  325. this.jsonPreviewData = this.uploadFiles.map((file, index) => ({
  326. id: file.id,
  327. fileName: file.name,
  328. fileSize: this.getFileSizeDisplay(file.size),
  329. fileType: this.getFileTypeFromName(file.name),
  330. status: "待分析",
  331. space: "客厅", // 默认空间,后续AI分析会更新
  332. stage: "白模", // 默认阶段,后续AI分析会更新
  333. confidence: 0,
  334. preview: file.preview || null,
  335. analysis: {
  336. quality: "未知",
  337. dimensions: "分析中...",
  338. category: "识别中...",
  339. suggestedStage: "分析中..."
  340. }
  341. }));
  342. this.showJsonPreview = true;
  343. console.log('JSON预览数据:', this.jsonPreviewData);
  344. }
  345. /**
  346. * 根据文件名获取文件类型
  347. */
  348. private getFileTypeFromName(fileName: string): string {
  349. const ext = fileName.toLowerCase().split('.').pop();
  350. switch (ext) {
  351. case 'jpg':
  352. case 'jpeg':
  353. case 'png':
  354. case 'gif':
  355. case 'webp':
  356. return '图片';
  357. case 'pdf':
  358. return 'PDF文档';
  359. case 'dwg':
  360. case 'dxf':
  361. return 'CAD图纸';
  362. case 'skp':
  363. return 'SketchUp模型';
  364. case 'max':
  365. return '3ds Max文件';
  366. default:
  367. return '其他文件';
  368. }
  369. }
  370. /**
  371. * 获取文件大小显示
  372. */
  373. getFileSizeDisplay(size: number): string {
  374. if (size < 1024) return `${size} B`;
  375. if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
  376. return `${(size / (1024 * 1024)).toFixed(1)} MB`;
  377. }
  378. /**
  379. * 获取文件类型图标
  380. */
  381. getFileTypeIcon(file: UploadFile): string {
  382. if (this.isImageFile(file.file)) return '🖼️';
  383. if (file.name.endsWith('.pdf')) return '📄';
  384. if (file.name.endsWith('.dwg') || file.name.endsWith('.dxf')) return '📐';
  385. if (file.name.endsWith('.skp')) return '🏗️';
  386. if (file.name.endsWith('.max')) return '🎨';
  387. return '📁';
  388. }
  389. /**
  390. * 移除文件
  391. */
  392. removeFile(fileId: string) {
  393. this.uploadFiles = this.uploadFiles.filter(f => f.id !== fileId);
  394. this.cdr.markForCheck();
  395. }
  396. /**
  397. * 添加更多文件
  398. */
  399. addMoreFiles(event: Event) {
  400. const input = event.target as HTMLInputElement;
  401. if (input.files) {
  402. const newFiles = Array.from(input.files);
  403. const newUploadFiles = newFiles.map((file, index) => ({
  404. file,
  405. id: `upload_${Date.now()}_${this.uploadFiles.length + index}`,
  406. name: file.name,
  407. size: file.size,
  408. type: file.type,
  409. status: 'pending' as const,
  410. progress: 0
  411. }));
  412. // 为新的图片文件生成预览
  413. newUploadFiles.forEach(uploadFile => {
  414. if (this.isImageFile(uploadFile.file)) {
  415. this.generatePreview(uploadFile);
  416. }
  417. });
  418. this.uploadFiles.push(...newUploadFiles);
  419. this.cdr.markForCheck();
  420. }
  421. // 重置input
  422. input.value = '';
  423. }
  424. /**
  425. * 确认上传
  426. */
  427. async confirmUpload(): Promise<void> {
  428. if (this.uploadFiles.length === 0 || this.isUploading) return;
  429. try {
  430. // 设置上传状态
  431. this.isUploading = true;
  432. this.uploadSuccess = false;
  433. this.uploadMessage = '正在上传文件...';
  434. this.cdr.markForCheck();
  435. // 生成交付清单ID
  436. const deliveryListId = `delivery_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  437. // 自动确认所有已分析的文件
  438. const result: UploadResult = {
  439. files: this.uploadFiles.map(file => ({
  440. file: file, // 传递完整的UploadFile对象
  441. spaceId: file.selectedSpace || (this.availableSpaces.length > 0 ? this.availableSpaces[0].id : ''),
  442. spaceName: this.getSpaceName(file.selectedSpace || (this.availableSpaces.length > 0 ? this.availableSpaces[0].id : '')),
  443. stageType: file.selectedStage || file.suggestedStage || 'white_model',
  444. stageName: this.getStageName(file.selectedStage || file.suggestedStage || 'white_model'),
  445. // 添加AI分析结果和提交信息
  446. analysisResult: file.analysisResult,
  447. submittedAt: new Date().toISOString(),
  448. submittedBy: 'current_user', // TODO: 获取当前用户ID
  449. submittedByName: 'current_user_name', // TODO: 获取当前用户名称
  450. deliveryListId: deliveryListId
  451. }))
  452. };
  453. console.log('📤 确认上传文件:', result);
  454. // 发送上传事件
  455. this.confirm.emit(result);
  456. // 模拟上传过程(实际上传完成后由父组件调用成功方法)
  457. await new Promise(resolve => setTimeout(resolve, 1000));
  458. this.uploadSuccess = true;
  459. this.uploadMessage = `上传成功!共上传 ${this.uploadFiles.length} 个文件`;
  460. // 2秒后自动关闭弹窗
  461. setTimeout(() => {
  462. this.close.emit();
  463. }, 2000);
  464. } catch (error) {
  465. console.error('上传失败:', error);
  466. this.uploadMessage = '上传失败,请重试';
  467. } finally {
  468. this.isUploading = false;
  469. this.cdr.markForCheck();
  470. }
  471. }
  472. /**
  473. * 取消上传
  474. */
  475. cancelUpload(): void {
  476. this.cleanupObjectURLs();
  477. this.cancel.emit();
  478. }
  479. /**
  480. * 关闭弹窗
  481. */
  482. closeModal(): void {
  483. this.cleanupObjectURLs();
  484. this.close.emit();
  485. }
  486. /**
  487. * 🔥 清理ObjectURL资源
  488. */
  489. private cleanupObjectURLs(): void {
  490. this.uploadFiles.forEach(file => {
  491. if (file.preview && file.preview.startsWith('blob:')) {
  492. try {
  493. URL.revokeObjectURL(file.preview);
  494. } catch (error) {
  495. console.error(`❌ 释放ObjectURL失败: ${file.name}`, error);
  496. }
  497. }
  498. });
  499. }
  500. /**
  501. * 阻止事件冒泡
  502. */
  503. preventDefault(event: Event): void {
  504. event.stopPropagation();
  505. }
  506. /**
  507. * 🔥 增强的快速分析(推荐,更准确且不阻塞界面)
  508. */
  509. private async startEnhancedMockAnalysis(): Promise<void> {
  510. const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
  511. if (imageFiles.length === 0) {
  512. this.analysisComplete = true;
  513. return;
  514. }
  515. console.log('🚀 开始增强快速分析...', {
  516. imageCount: imageFiles.length,
  517. targetSpace: this.targetSpaceName,
  518. targetStage: this.targetStageName
  519. });
  520. // 不显示全屏覆盖层,直接在表格中显示分析状态
  521. this.isAnalyzing = false; // 不显示全屏覆盖
  522. this.analysisComplete = false;
  523. this.analysisProgress = '正在分析图片...';
  524. this.cdr.markForCheck();
  525. try {
  526. // 并行处理所有图片,提高速度
  527. const analysisPromises = imageFiles.map(async (uploadFile, index) => {
  528. // 设置分析状态
  529. uploadFile.status = 'analyzing';
  530. this.cdr.markForCheck();
  531. // 模拟短暂分析过程(200-500ms)
  532. await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
  533. try {
  534. // 使用增强的分析算法
  535. const analysisResult = this.generateEnhancedAnalysisResult(uploadFile.file);
  536. // 保存分析结果
  537. uploadFile.analysisResult = analysisResult;
  538. uploadFile.suggestedStage = analysisResult.suggestedStage;
  539. uploadFile.selectedStage = analysisResult.suggestedStage;
  540. uploadFile.status = 'pending';
  541. // 更新JSON预览数据
  542. this.updateJsonPreviewData(uploadFile, analysisResult);
  543. console.log(`✨ ${uploadFile.name} 增强分析完成:`, {
  544. suggestedStage: analysisResult.suggestedStage,
  545. confidence: analysisResult.content.confidence,
  546. quality: analysisResult.quality.level,
  547. reason: analysisResult.suggestedReason
  548. });
  549. } catch (error) {
  550. console.error(`分析 ${uploadFile.name} 失败:`, error);
  551. uploadFile.status = 'pending';
  552. }
  553. this.cdr.markForCheck();
  554. });
  555. // 等待所有分析完成
  556. await Promise.all(analysisPromises);
  557. this.analysisProgress = `分析完成!共分析 ${imageFiles.length} 张图片`;
  558. this.analysisComplete = true;
  559. console.log('✅ 所有图片增强分析完成');
  560. } catch (error) {
  561. console.error('增强分析过程出错:', error);
  562. this.analysisProgress = '分析过程出错';
  563. this.analysisComplete = true;
  564. } finally {
  565. this.isAnalyzing = false;
  566. setTimeout(() => {
  567. this.analysisProgress = '';
  568. this.cdr.markForCheck();
  569. }, 2000);
  570. this.cdr.markForCheck();
  571. }
  572. }
  573. /**
  574. * 生成增强的分析结果(更准确的分类)
  575. */
  576. private generateEnhancedAnalysisResult(file: File): ImageAnalysisResult {
  577. const fileName = file.name.toLowerCase();
  578. const fileSize = file.size;
  579. // 获取目标空间信息
  580. const targetSpaceName = this.targetSpaceName || '客厅';
  581. console.log(`🔍 分析文件: ${fileName}`, {
  582. targetSpace: targetSpaceName,
  583. targetStage: this.targetStageName,
  584. fileSize: fileSize
  585. });
  586. // 增强的阶段分类算法
  587. let suggestedStage: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model';
  588. let confidence = 75;
  589. let reason = '基于文件名和特征分析';
  590. // 1. 文件名关键词分析
  591. if (fileName.includes('白模') || fileName.includes('white') || fileName.includes('model') ||
  592. fileName.includes('毛坯') || fileName.includes('空间') || fileName.includes('结构')) {
  593. suggestedStage = 'white_model';
  594. confidence = 90;
  595. reason = '文件名包含白模相关关键词';
  596. } else if (fileName.includes('软装') || fileName.includes('soft') || fileName.includes('decor') ||
  597. fileName.includes('家具') || fileName.includes('furniture') || fileName.includes('装饰')) {
  598. suggestedStage = 'soft_decor';
  599. confidence = 88;
  600. reason = '文件名包含软装相关关键词';
  601. } else if (fileName.includes('渲染') || fileName.includes('render') || fileName.includes('效果') ||
  602. fileName.includes('effect') || fileName.includes('光照')) {
  603. suggestedStage = 'rendering';
  604. confidence = 92;
  605. reason = '文件名包含渲染相关关键词';
  606. } else if (fileName.includes('后期') || fileName.includes('post') || fileName.includes('final') ||
  607. fileName.includes('最终') || fileName.includes('完成') || fileName.includes('成品')) {
  608. suggestedStage = 'post_process';
  609. confidence = 95;
  610. reason = '文件名包含后期处理相关关键词';
  611. }
  612. // 2. 文件大小分析(辅助判断)
  613. if (fileSize > 5 * 1024 * 1024) { // 大于5MB
  614. if (suggestedStage === 'white_model') {
  615. // 大文件更可能是渲染或后期
  616. suggestedStage = 'rendering';
  617. confidence = Math.min(confidence + 10, 95);
  618. reason += ',大文件更可能是高质量渲染图';
  619. }
  620. }
  621. // 3. 根据目标空间调整置信度
  622. if (this.targetStageName) {
  623. const targetStageMap: Record<string, 'white_model' | 'soft_decor' | 'rendering' | 'post_process'> = {
  624. '白模': 'white_model',
  625. '软装': 'soft_decor',
  626. '渲染': 'rendering',
  627. '后期': 'post_process'
  628. };
  629. const targetStage = targetStageMap[this.targetStageName];
  630. if (targetStage && targetStage === suggestedStage) {
  631. confidence = Math.min(confidence + 15, 98);
  632. reason += `,与目标阶段一致`;
  633. }
  634. }
  635. // 生成质量评分
  636. const qualityScore = this.calculateQualityScore(suggestedStage, fileSize);
  637. const result: ImageAnalysisResult = {
  638. fileName: file.name,
  639. fileSize: file.size,
  640. dimensions: {
  641. width: 1920,
  642. height: 1080
  643. },
  644. quality: {
  645. score: qualityScore,
  646. level: this.getQualityLevel(qualityScore),
  647. sharpness: qualityScore + 5,
  648. brightness: qualityScore - 5,
  649. contrast: qualityScore,
  650. detailLevel: qualityScore >= 90 ? 'ultra_detailed' : qualityScore >= 75 ? 'detailed' : qualityScore >= 60 ? 'basic' : 'minimal',
  651. pixelDensity: qualityScore >= 90 ? 'ultra_high' : qualityScore >= 75 ? 'high' : qualityScore >= 60 ? 'medium' : 'low',
  652. textureQuality: qualityScore,
  653. colorDepth: qualityScore
  654. },
  655. content: {
  656. category: suggestedStage,
  657. confidence: confidence,
  658. description: `${targetSpaceName}${this.getStageName(suggestedStage)}图`,
  659. tags: [this.getStageName(suggestedStage), targetSpaceName, '设计'],
  660. isArchitectural: true,
  661. hasInterior: true,
  662. hasFurniture: suggestedStage !== 'white_model',
  663. hasLighting: suggestedStage === 'rendering' || suggestedStage === 'post_process'
  664. },
  665. technical: {
  666. format: file.type,
  667. colorSpace: 'sRGB',
  668. dpi: 72,
  669. aspectRatio: '16:9',
  670. megapixels: 2.07
  671. },
  672. suggestedStage: suggestedStage,
  673. suggestedReason: reason,
  674. analysisTime: 100,
  675. analysisDate: new Date().toISOString()
  676. };
  677. return result;
  678. }
  679. /**
  680. * 计算质量评分
  681. */
  682. private calculateQualityScore(stage: string, fileSize: number): number {
  683. const baseScores = {
  684. 'white_model': 75,
  685. 'soft_decor': 82,
  686. 'rendering': 88,
  687. 'post_process': 95
  688. };
  689. let score = baseScores[stage as keyof typeof baseScores] || 75;
  690. // 根据文件大小调整
  691. if (fileSize > 10 * 1024 * 1024) score += 5; // 大于10MB
  692. else if (fileSize < 1024 * 1024) score -= 5; // 小于1MB
  693. return Math.max(60, Math.min(100, score));
  694. }
  695. /**
  696. * 获取质量等级
  697. */
  698. private getQualityLevel(score: number): 'low' | 'medium' | 'high' | 'ultra' {
  699. if (score >= 90) return 'ultra';
  700. if (score >= 80) return 'high';
  701. if (score >= 70) return 'medium';
  702. return 'low';
  703. }
  704. /**
  705. * 获取质量等级显示文本
  706. */
  707. getQualityLevelText(level: 'low' | 'medium' | 'high' | 'ultra'): string {
  708. const levelMap = {
  709. 'ultra': '优秀',
  710. 'high': '良好',
  711. 'medium': '中等',
  712. 'low': '较差'
  713. };
  714. return levelMap[level] || '未知';
  715. }
  716. /**
  717. * 获取质量等级颜色
  718. */
  719. getQualityLevelColor(level: 'low' | 'medium' | 'high' | 'ultra'): string {
  720. const colorMap = {
  721. 'ultra': '#52c41a', // 绿色 - 优秀
  722. 'high': '#1890ff', // 蓝色 - 良好
  723. 'medium': '#faad14', // 橙色 - 中等
  724. 'low': '#ff4d4f' // 红色 - 较差
  725. };
  726. return colorMap[level] || '#d9d9d9';
  727. }
  728. /**
  729. * 🔥 真实AI图片分析(使用豆包1.6视觉识别)
  730. */
  731. private async startImageAnalysis(): Promise<void> {
  732. const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
  733. if (imageFiles.length === 0) {
  734. this.analysisComplete = true;
  735. return;
  736. }
  737. // 🔥 不显示全屏遮罩,直接在表格中显示分析状态
  738. this.isAnalyzing = false; // 改为false,避免全屏阻塞
  739. this.analysisComplete = false;
  740. this.analysisProgress = '正在启动AI快速分析...';
  741. this.cdr.markForCheck();
  742. try {
  743. for (let i = 0; i < imageFiles.length; i++) {
  744. const uploadFile = imageFiles[i];
  745. // 更新文件状态为分析中
  746. uploadFile.status = 'analyzing';
  747. this.analysisProgress = `正在分析 ${uploadFile.name} (${i + 1}/${imageFiles.length})`;
  748. this.cdr.markForCheck();
  749. try {
  750. // 🔥 使用真正的AI分析(豆包1.6视觉识别)
  751. // 确保有预览URL,如果没有则跳过分析
  752. // 🔥 使用真实的AI分析服务(快速模式)
  753. const analysisResult = await this.imageAnalysisService.analyzeImage(
  754. uploadFile.preview, // 图片预览URL(Base64或ObjectURL)
  755. uploadFile.file, // 文件对象
  756. (progress) => {
  757. // 在表格行内显示进度,不阻塞界面
  758. this.analysisProgress = `[${i + 1}/${imageFiles.length}] ${progress}`;
  759. this.cdr.markForCheck();
  760. },
  761. true // 🔥 快速模式:跳过专业分析
  762. );
  763. // 保存分析结果
  764. uploadFile.analysisResult = analysisResult;
  765. uploadFile.suggestedStage = analysisResult.suggestedStage;
  766. // 自动设置为AI建议的阶段
  767. uploadFile.selectedStage = analysisResult.suggestedStage;
  768. uploadFile.status = 'pending';
  769. // 更新JSON预览数据
  770. this.updateJsonPreviewData(uploadFile, analysisResult);
  771. // 🔥 详细日志输出
  772. console.log(`✅ [${i + 1}/${imageFiles.length}] ${uploadFile.name}:`, {
  773. 建议阶段: analysisResult.suggestedStage,
  774. 置信度: `${analysisResult.content.confidence}%`,
  775. 空间类型: analysisResult.content.spaceType || '未识别',
  776. 有颜色: analysisResult.content.hasColor,
  777. 有纹理: analysisResult.content.hasTexture,
  778. 有灯光: analysisResult.content.hasLighting,
  779. 质量分数: analysisResult.quality.score,
  780. 分析耗时: `${analysisResult.analysisTime}ms`
  781. });
  782. } catch (error: any) {
  783. console.error(`❌ 分析 ${uploadFile.name} 失败 - 详细错误:`, {
  784. 错误类型: error?.constructor?.name,
  785. 错误信息: error?.message,
  786. 错误代码: error?.code || error?.status,
  787. 文件名: uploadFile.name,
  788. 文件大小: uploadFile.file.size
  789. });
  790. uploadFile.status = 'pending'; // 分析失败仍可上传
  791. // 分析失败时,设置为默认的渲染阶段
  792. uploadFile.selectedStage = 'rendering';
  793. uploadFile.suggestedStage = 'rendering';
  794. }
  795. this.cdr.markForCheck();
  796. }
  797. this.analysisProgress = 'AI分析完成,已生成智能建议';
  798. this.analysisComplete = true;
  799. } catch (error) {
  800. console.error('图片分析过程出错:', error);
  801. this.analysisProgress = '分析过程出错';
  802. this.analysisComplete = true;
  803. } finally {
  804. this.isAnalyzing = false;
  805. setTimeout(() => {
  806. this.analysisProgress = '';
  807. this.cdr.markForCheck();
  808. }, 2000);
  809. this.cdr.markForCheck();
  810. }
  811. }
  812. /**
  813. * 🔥 增强的快速分析(已废弃,仅保留作为参考)
  814. */
  815. private async startEnhancedMockAnalysis_DEPRECATED(): Promise<void> {
  816. const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
  817. if (imageFiles.length === 0) {
  818. this.analysisComplete = true;
  819. return;
  820. }
  821. this.isAnalyzing = true;
  822. this.analysisComplete = false;
  823. this.analysisProgress = '准备分析图片...';
  824. this.cdr.markForCheck();
  825. try {
  826. for (let i = 0; i < imageFiles.length; i++) {
  827. const uploadFile = imageFiles[i];
  828. // 更新文件状态为分析中
  829. uploadFile.status = 'analyzing';
  830. this.analysisProgress = `正在分析 ${uploadFile.name} (${i + 1}/${imageFiles.length})`;
  831. this.cdr.markForCheck();
  832. try {
  833. // 使用预览URL进行分析
  834. if (uploadFile.preview) {
  835. const analysisResult = await this.imageAnalysisService.analyzeImage(
  836. uploadFile.preview,
  837. uploadFile.file,
  838. (progress) => {
  839. this.analysisProgress = `${uploadFile.name}: ${progress}`;
  840. this.cdr.markForCheck();
  841. }
  842. );
  843. // 保存分析结果
  844. uploadFile.analysisResult = analysisResult;
  845. uploadFile.suggestedStage = analysisResult.suggestedStage;
  846. // 自动设置为AI建议的阶段
  847. uploadFile.selectedStage = analysisResult.suggestedStage;
  848. uploadFile.status = 'pending';
  849. // 更新JSON预览数据
  850. this.updateJsonPreviewData(uploadFile, analysisResult);
  851. console.log(`${uploadFile.name} 分析完成:`, analysisResult);
  852. }
  853. } catch (error) {
  854. console.error(`分析 ${uploadFile.name} 失败:`, error);
  855. uploadFile.status = 'pending'; // 分析失败仍可上传
  856. }
  857. this.cdr.markForCheck();
  858. }
  859. this.analysisProgress = '图片分析完成';
  860. this.analysisComplete = true;
  861. } catch (error) {
  862. console.error('图片分析过程出错:', error);
  863. this.analysisProgress = '分析过程出错';
  864. this.analysisComplete = true;
  865. } finally {
  866. this.isAnalyzing = false;
  867. setTimeout(() => {
  868. this.analysisProgress = '';
  869. this.cdr.markForCheck();
  870. }, 2000);
  871. this.cdr.markForCheck();
  872. }
  873. }
  874. /**
  875. * 更新JSON预览数据
  876. */
  877. private updateJsonPreviewData(uploadFile: UploadFile, analysisResult: ImageAnalysisResult): void {
  878. const jsonItem = this.jsonPreviewData.find(item => item.id === uploadFile.id);
  879. if (jsonItem) {
  880. // 根据AI分析结果更新空间和阶段
  881. jsonItem.stage = this.getSuggestedStageText(analysisResult.suggestedStage);
  882. jsonItem.space = this.inferSpaceFromAnalysis(analysisResult);
  883. jsonItem.confidence = analysisResult.content.confidence;
  884. jsonItem.status = "分析完成";
  885. jsonItem.analysis = {
  886. quality: analysisResult.quality.level,
  887. dimensions: `${analysisResult.dimensions.width}x${analysisResult.dimensions.height}`,
  888. category: analysisResult.content.category,
  889. suggestedStage: this.getSuggestedStageText(analysisResult.suggestedStage)
  890. };
  891. }
  892. }
  893. /**
  894. * 从AI分析结果推断空间类型
  895. */
  896. inferSpaceFromAnalysis(analysisResult: ImageAnalysisResult): string {
  897. const tags = analysisResult.content.tags;
  898. const description = analysisResult.content.description.toLowerCase();
  899. // 基于标签和描述推断空间类型
  900. if (tags.includes('客厅') || description.includes('客厅') || description.includes('living')) {
  901. return '客厅';
  902. } else if (tags.includes('卧室') || description.includes('卧室') || description.includes('bedroom')) {
  903. return '卧室';
  904. } else if (tags.includes('厨房') || description.includes('厨房') || description.includes('kitchen')) {
  905. return '厨房';
  906. } else if (tags.includes('卫生间') || description.includes('卫生间') || description.includes('bathroom')) {
  907. return '卫生间';
  908. } else if (tags.includes('餐厅') || description.includes('餐厅') || description.includes('dining')) {
  909. return '餐厅';
  910. } else {
  911. return '客厅'; // 默认空间
  912. }
  913. }
  914. /**
  915. * 获取分析状态显示文本
  916. */
  917. getAnalysisStatusText(file: UploadFile): string {
  918. if (file.status === 'analyzing') {
  919. return '分析中...';
  920. }
  921. if (file.analysisResult) {
  922. const result = file.analysisResult;
  923. const categoryText = this.getSuggestedStageText(result.content.category);
  924. const qualityText = this.getQualityLevelText(result.quality.level);
  925. return `${categoryText} (${qualityText}, ${result.content.confidence}%置信度)`;
  926. }
  927. return '';
  928. }
  929. /**
  930. * 获取建议阶段的显示文本
  931. */
  932. getSuggestedStageText(stageType: string): string {
  933. const stageMap: { [key: string]: string } = {
  934. 'white_model': '白模',
  935. 'soft_decor': '软装',
  936. 'rendering': '渲染',
  937. 'post_process': '后期'
  938. };
  939. return stageMap[stageType] || stageType;
  940. }
  941. /**
  942. * 计算文件总大小
  943. */
  944. getTotalSize(): number {
  945. try {
  946. return this.uploadFiles?.reduce((sum, f) => sum + (f?.size || 0), 0) || 0;
  947. } catch {
  948. let total = 0;
  949. for (const f of this.uploadFiles || []) total += f?.size || 0;
  950. return total;
  951. }
  952. }
  953. /**
  954. * 更新文件的选择空间
  955. */
  956. updateFileSpace(fileId: string, spaceId: string) {
  957. const file = this.uploadFiles.find(f => f.id === fileId);
  958. if (file) {
  959. file.selectedSpace = spaceId;
  960. this.cdr.markForCheck();
  961. }
  962. }
  963. /**
  964. * 更新文件的选择阶段
  965. */
  966. updateFileStage(fileId: string, stageId: string) {
  967. const file = this.uploadFiles.find(f => f.id === fileId);
  968. if (file) {
  969. file.selectedStage = stageId;
  970. this.cdr.markForCheck();
  971. }
  972. }
  973. /**
  974. * 获取空间名称
  975. */
  976. getSpaceName(spaceId: string): string {
  977. const space = this.availableSpaces.find(s => s.id === spaceId);
  978. return space?.name || '';
  979. }
  980. /**
  981. * 获取阶段名称
  982. */
  983. getStageName(stageId: string): string {
  984. const stage = this.availableStages.find(s => s.id === stageId);
  985. return stage?.name || '';
  986. }
  987. /**
  988. * 获取文件总数
  989. */
  990. getFileCount(): number {
  991. return this.uploadFiles.length;
  992. }
  993. /**
  994. * 检查是否可以确认上传
  995. */
  996. canConfirm(): boolean {
  997. if (this.uploadFiles.length === 0) return false;
  998. if (this.isAnalyzing) return false;
  999. // 检查是否所有文件都已选择空间和阶段
  1000. return this.uploadFiles.every(f => f.selectedSpace && f.selectedStage);
  1001. }
  1002. /**
  1003. * 获取分析进度百分比
  1004. */
  1005. getAnalysisProgressPercent(): number {
  1006. if (this.uploadFiles.length === 0) return 0;
  1007. const processedCount = this.uploadFiles.filter(f => f.status !== 'pending').length;
  1008. return Math.round((processedCount / this.uploadFiles.length) * 100);
  1009. }
  1010. /**
  1011. * 获取已分析文件数量
  1012. */
  1013. getAnalyzedFilesCount(): number {
  1014. return this.uploadFiles.filter(f => f.analysisResult).length;
  1015. }
  1016. /**
  1017. * 🔥 查看完整图片
  1018. */
  1019. viewFullImage(file: UploadFile): void {
  1020. if (file.preview) {
  1021. this.viewingImage = file;
  1022. this.cdr.markForCheck();
  1023. console.log('🖼️ 打开图片查看器:', file.name);
  1024. }
  1025. }
  1026. /**
  1027. * 🔥 关闭图片查看器
  1028. */
  1029. closeImageViewer(): void {
  1030. this.viewingImage = null;
  1031. this.cdr.markForCheck();
  1032. console.log('❌ 关闭图片查看器');
  1033. }
  1034. /**
  1035. * 🔥 图片加载错误处理
  1036. */
  1037. onImageError(event: Event, file: UploadFile): void {
  1038. console.error('❌ 图片加载失败:', file.name, {
  1039. preview: file.preview ? file.preview.substring(0, 100) + '...' : 'null',
  1040. fileUrl: file.fileUrl,
  1041. isWxWork: this.isWxWorkEnvironment()
  1042. });
  1043. // 🔥 设置错误标记,让HTML显示placeholder而不是破损图标
  1044. file.imageLoadError = true;
  1045. // 标记视图需要更新
  1046. this.cdr.markForCheck();
  1047. // 在企业微信环境中,尝试使用ObjectURL作为备选方案
  1048. if (this.isWxWorkEnvironment() && this.isImageFile(file.file)) {
  1049. try {
  1050. const objectUrl = URL.createObjectURL(file.file);
  1051. // 清除错误标记
  1052. file.imageLoadError = false;
  1053. file.preview = objectUrl;
  1054. console.log('🔄 使用ObjectURL作为预览:', objectUrl);
  1055. this.cdr.markForCheck();
  1056. } catch (error) {
  1057. console.error('❌ 生成ObjectURL失败:', error);
  1058. file.imageLoadError = true; // 确保显示placeholder
  1059. this.cdr.markForCheck();
  1060. }
  1061. }
  1062. }
  1063. /**
  1064. * 检测是否在企业微信环境
  1065. */
  1066. private isWxWorkEnvironment(): boolean {
  1067. const ua = navigator.userAgent.toLowerCase();
  1068. return ua.includes('wxwork') || ua.includes('micromessenger');
  1069. }
  1070. /**
  1071. * 🔥 组件销毁时清理ObjectURL,避免内存泄漏
  1072. */
  1073. ngOnDestroy(): void {
  1074. console.log('🧹 组件销毁,清理ObjectURL资源...');
  1075. this.cleanupObjectURLs();
  1076. }
  1077. }