drag-upload-modal.component.ts 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318
  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视觉分析(基于图片内容)
  730. * 专为交付执行阶段优化,根据图片真实内容判断阶段
  731. */
  732. private async startImageAnalysis(): Promise<void> {
  733. const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
  734. if (imageFiles.length === 0) {
  735. this.analysisComplete = true;
  736. return;
  737. }
  738. console.log('🤖 [真实AI分析] 开始分析...', {
  739. 文件数量: imageFiles.length,
  740. 目标空间: this.targetSpaceName,
  741. 目标阶段: this.targetStageName
  742. });
  743. // 🔥 不显示全屏遮罩,直接在表格中显示分析状态
  744. this.isAnalyzing = false;
  745. this.analysisComplete = false;
  746. this.analysisProgress = '正在启动AI视觉分析...';
  747. this.cdr.markForCheck();
  748. try {
  749. // 🚀 并行分析图片(提高速度,适合多图场景)
  750. this.analysisProgress = `正在分析 ${imageFiles.length} 张图片...`;
  751. this.cdr.markForCheck();
  752. const analysisPromises = imageFiles.map(async (uploadFile, i) => {
  753. // 更新文件状态为分析中
  754. uploadFile.status = 'analyzing';
  755. this.cdr.markForCheck();
  756. try {
  757. // 🤖 使用真实AI视觉分析(基于图片内容)
  758. console.log(`🤖 [${i + 1}/${imageFiles.length}] 开始AI视觉分析: ${uploadFile.name}`);
  759. if (!uploadFile.preview) {
  760. console.warn(`⚠️ ${uploadFile.name} 没有预览,跳过AI分析`);
  761. uploadFile.selectedStage = this.targetStageType || 'rendering';
  762. uploadFile.suggestedStage = this.targetStageType || 'rendering';
  763. uploadFile.status = 'pending';
  764. return;
  765. }
  766. // 🔥 调用真实的AI分析服务(analyzeImage)
  767. const analysisResult = await this.imageAnalysisService.analyzeImage(
  768. uploadFile.preview, // 图片预览URL(Base64或ObjectURL)
  769. uploadFile.file, // 文件对象
  770. (progress) => {
  771. // 在表格行内显示进度
  772. console.log(`[${i + 1}/${imageFiles.length}] ${progress}`);
  773. },
  774. true // 🔥 快速模式:跳过专业分析,加快速度
  775. );
  776. console.log(`✅ [${i + 1}/${imageFiles.length}] AI分析完成: ${uploadFile.name}`, {
  777. 建议阶段: analysisResult.suggestedStage,
  778. 置信度: `${analysisResult.content.confidence}%`,
  779. 空间类型: analysisResult.content.spaceType || '未识别',
  780. 有颜色: analysisResult.content.hasColor,
  781. 有纹理: analysisResult.content.hasTexture,
  782. 有灯光: analysisResult.content.hasLighting,
  783. 质量分数: analysisResult.quality.score,
  784. 分析耗时: `${analysisResult.analysisTime}ms`
  785. });
  786. // 保存分析结果
  787. uploadFile.analysisResult = analysisResult;
  788. uploadFile.suggestedStage = analysisResult.suggestedStage;
  789. uploadFile.selectedStage = analysisResult.suggestedStage; // 🔥 自动使用AI建议的阶段
  790. uploadFile.status = 'pending';
  791. // 更新JSON预览数据
  792. this.updateJsonPreviewData(uploadFile, analysisResult);
  793. } catch (error: any) {
  794. console.error(`❌ AI分析 ${uploadFile.name} 失败:`, error);
  795. uploadFile.status = 'pending';
  796. // 分析失败时,使用拖拽目标阶段或默认值
  797. uploadFile.selectedStage = this.targetStageType || 'rendering';
  798. uploadFile.suggestedStage = this.targetStageType || 'rendering';
  799. }
  800. this.cdr.markForCheck();
  801. });
  802. // 🚀 并行等待所有分析完成
  803. await Promise.all(analysisPromises);
  804. this.analysisProgress = `✅ AI视觉分析完成!共分析 ${imageFiles.length} 张图片`;
  805. this.analysisComplete = true;
  806. console.log('✅ [真实AI分析] 所有文件分析完成(并行)');
  807. } catch (error) {
  808. console.error('❌ [真实AI分析] 过程出错:', error);
  809. this.analysisProgress = '分析过程出错';
  810. this.analysisComplete = true;
  811. } finally {
  812. this.isAnalyzing = false;
  813. setTimeout(() => {
  814. this.analysisProgress = '';
  815. this.cdr.markForCheck();
  816. }, 2000);
  817. this.cdr.markForCheck();
  818. }
  819. }
  820. /**
  821. * 🔥 基于文件名的快速分析(返回简化JSON)
  822. * 返回格式: { "space": "客厅", "stage": "软装", "confidence": 95 }
  823. */
  824. private async quickAnalyzeByFileName(file: File): Promise<{ space: string; stage: string; confidence: number }> {
  825. const fileName = file.name.toLowerCase();
  826. // 🔥 阶段判断(优先级:白膜 > 软装 > 渲染 > 后期)
  827. let stage = this.targetStageType || 'rendering'; // 🔥 优先使用目标阶段,否则默认渲染
  828. let confidence = 50; // 🔥 默认低置信度,提示用户需要确认
  829. let hasKeyword = false; // 🔥 标记是否匹配到关键词
  830. // 白膜关键词(最高优先级)- 解决白膜被误判问题
  831. if (fileName.includes('白模') || fileName.includes('bm') || fileName.includes('whitemodel') ||
  832. fileName.includes('模型') || fileName.includes('建模') || fileName.includes('白膜')) {
  833. stage = 'white_model';
  834. confidence = 95;
  835. hasKeyword = true;
  836. }
  837. // 软装关键词
  838. else if (fileName.includes('软装') || fileName.includes('rz') || fileName.includes('softdecor') ||
  839. fileName.includes('家具') || fileName.includes('配饰') || fileName.includes('陈设')) {
  840. stage = 'soft_decor';
  841. confidence = 92;
  842. hasKeyword = true;
  843. }
  844. // 后期关键词
  845. else if (fileName.includes('后期') || fileName.includes('hq') || fileName.includes('postprocess') ||
  846. fileName.includes('修图') || fileName.includes('精修') || fileName.includes('调色')) {
  847. stage = 'post_process';
  848. confidence = 90;
  849. hasKeyword = true;
  850. }
  851. // 渲染关键词
  852. else if (fileName.includes('渲染') || fileName.includes('xr') || fileName.includes('rendering') ||
  853. fileName.includes('效果图') || fileName.includes('render')) {
  854. stage = 'rendering';
  855. confidence = 88;
  856. hasKeyword = true;
  857. }
  858. // 🔥 如果没有匹配到关键词,但有目标阶段,使用目标阶段并提升置信度
  859. else if (this.targetStageType) {
  860. stage = this.targetStageType;
  861. confidence = 70; // 🔥 使用目标阶段时,置信度提升到70%
  862. console.log(`⚠️ [文件名分析] 文件名无关键词,使用拖拽目标阶段: ${this.targetStageName}`);
  863. }
  864. // 🔥 空间判断
  865. let space = this.targetSpaceName || '未知空间';
  866. if (fileName.includes('客厅') || fileName.includes('kt') || fileName.includes('living')) {
  867. space = '客厅';
  868. confidence = Math.min(confidence + 5, 98);
  869. } else if (fileName.includes('卧室') || fileName.includes('ws') || fileName.includes('bedroom') ||
  870. fileName.includes('主卧') || fileName.includes('次卧')) {
  871. space = '卧室';
  872. confidence = Math.min(confidence + 5, 98);
  873. } else if (fileName.includes('餐厅') || fileName.includes('ct') || fileName.includes('dining')) {
  874. space = '餐厅';
  875. confidence = Math.min(confidence + 5, 98);
  876. } else if (fileName.includes('厨房') || fileName.includes('cf') || fileName.includes('kitchen')) {
  877. space = '厨房';
  878. confidence = Math.min(confidence + 5, 98);
  879. } else if (fileName.includes('卫生间') || fileName.includes('wsj') || fileName.includes('bathroom') ||
  880. fileName.includes('浴室') || fileName.includes('厕所')) {
  881. space = '卫生间';
  882. confidence = Math.min(confidence + 5, 98);
  883. } else if (fileName.includes('书房') || fileName.includes('sf') || fileName.includes('study')) {
  884. space = '书房';
  885. confidence = Math.min(confidence + 5, 98);
  886. } else if (fileName.includes('阳台') || fileName.includes('yt') || fileName.includes('balcony')) {
  887. space = '阳台';
  888. confidence = Math.min(confidence + 5, 98);
  889. } else if (fileName.includes('玄关') || fileName.includes('xg') || fileName.includes('entrance')) {
  890. space = '玄关';
  891. confidence = Math.min(confidence + 5, 98);
  892. }
  893. console.log(`🔍 [文件名分析] ${fileName} → 空间: ${space}, 阶段: ${stage}, 置信度: ${confidence}%`);
  894. return {
  895. space,
  896. stage,
  897. confidence
  898. };
  899. }
  900. /**
  901. * 🔥 增强的快速分析(已废弃,仅保留作为参考)
  902. */
  903. private async startEnhancedMockAnalysis_DEPRECATED(): Promise<void> {
  904. const imageFiles = this.uploadFiles.filter(f => this.isImageFile(f.file));
  905. if (imageFiles.length === 0) {
  906. this.analysisComplete = true;
  907. return;
  908. }
  909. this.isAnalyzing = true;
  910. this.analysisComplete = false;
  911. this.analysisProgress = '准备分析图片...';
  912. this.cdr.markForCheck();
  913. try {
  914. for (let i = 0; i < imageFiles.length; i++) {
  915. const uploadFile = imageFiles[i];
  916. // 更新文件状态为分析中
  917. uploadFile.status = 'analyzing';
  918. this.analysisProgress = `正在分析 ${uploadFile.name} (${i + 1}/${imageFiles.length})`;
  919. this.cdr.markForCheck();
  920. try {
  921. // 使用预览URL进行分析
  922. if (uploadFile.preview) {
  923. const analysisResult = await this.imageAnalysisService.analyzeImage(
  924. uploadFile.preview,
  925. uploadFile.file,
  926. (progress) => {
  927. this.analysisProgress = `${uploadFile.name}: ${progress}`;
  928. this.cdr.markForCheck();
  929. }
  930. );
  931. // 保存分析结果
  932. uploadFile.analysisResult = analysisResult;
  933. uploadFile.suggestedStage = analysisResult.suggestedStage;
  934. // 自动设置为AI建议的阶段
  935. uploadFile.selectedStage = analysisResult.suggestedStage;
  936. uploadFile.status = 'pending';
  937. // 更新JSON预览数据
  938. this.updateJsonPreviewData(uploadFile, analysisResult);
  939. console.log(`${uploadFile.name} 分析完成:`, analysisResult);
  940. }
  941. } catch (error) {
  942. console.error(`分析 ${uploadFile.name} 失败:`, error);
  943. uploadFile.status = 'pending'; // 分析失败仍可上传
  944. }
  945. this.cdr.markForCheck();
  946. }
  947. this.analysisProgress = '图片分析完成';
  948. this.analysisComplete = true;
  949. } catch (error) {
  950. console.error('图片分析过程出错:', error);
  951. this.analysisProgress = '分析过程出错';
  952. this.analysisComplete = true;
  953. } finally {
  954. this.isAnalyzing = false;
  955. setTimeout(() => {
  956. this.analysisProgress = '';
  957. this.cdr.markForCheck();
  958. }, 2000);
  959. this.cdr.markForCheck();
  960. }
  961. }
  962. /**
  963. * 更新JSON预览数据
  964. */
  965. private updateJsonPreviewData(uploadFile: UploadFile, analysisResult: ImageAnalysisResult): void {
  966. const jsonItem = this.jsonPreviewData.find(item => item.id === uploadFile.id);
  967. if (jsonItem) {
  968. // 根据AI分析结果更新空间和阶段
  969. jsonItem.stage = this.getSuggestedStageText(analysisResult.suggestedStage);
  970. jsonItem.space = this.inferSpaceFromAnalysis(analysisResult);
  971. jsonItem.confidence = analysisResult.content.confidence;
  972. jsonItem.status = "分析完成";
  973. jsonItem.analysis = {
  974. quality: analysisResult.quality.level,
  975. dimensions: `${analysisResult.dimensions.width}x${analysisResult.dimensions.height}`,
  976. category: analysisResult.content.category,
  977. suggestedStage: this.getSuggestedStageText(analysisResult.suggestedStage)
  978. };
  979. }
  980. }
  981. /**
  982. * 从AI分析结果推断空间类型
  983. */
  984. inferSpaceFromAnalysis(analysisResult: ImageAnalysisResult): string {
  985. const tags = analysisResult.content.tags;
  986. const description = analysisResult.content.description.toLowerCase();
  987. // 基于标签和描述推断空间类型
  988. if (tags.includes('客厅') || description.includes('客厅') || description.includes('living')) {
  989. return '客厅';
  990. } else if (tags.includes('卧室') || description.includes('卧室') || description.includes('bedroom')) {
  991. return '卧室';
  992. } else if (tags.includes('厨房') || description.includes('厨房') || description.includes('kitchen')) {
  993. return '厨房';
  994. } else if (tags.includes('卫生间') || description.includes('卫生间') || description.includes('bathroom')) {
  995. return '卫生间';
  996. } else if (tags.includes('餐厅') || description.includes('餐厅') || description.includes('dining')) {
  997. return '餐厅';
  998. } else {
  999. return '客厅'; // 默认空间
  1000. }
  1001. }
  1002. /**
  1003. * 获取分析状态显示文本
  1004. */
  1005. getAnalysisStatusText(file: UploadFile): string {
  1006. if (file.status === 'analyzing') {
  1007. return '分析中...';
  1008. }
  1009. if (file.analysisResult) {
  1010. const result = file.analysisResult;
  1011. const categoryText = this.getSuggestedStageText(result.content.category);
  1012. const qualityText = this.getQualityLevelText(result.quality.level);
  1013. return `${categoryText} (${qualityText}, ${result.content.confidence}%置信度)`;
  1014. }
  1015. return '';
  1016. }
  1017. /**
  1018. * 获取建议阶段的显示文本
  1019. */
  1020. getSuggestedStageText(stageType: string): string {
  1021. const stageMap: { [key: string]: string } = {
  1022. 'white_model': '白模',
  1023. 'soft_decor': '软装',
  1024. 'rendering': '渲染',
  1025. 'post_process': '后期'
  1026. };
  1027. return stageMap[stageType] || stageType;
  1028. }
  1029. /**
  1030. * 计算文件总大小
  1031. */
  1032. getTotalSize(): number {
  1033. try {
  1034. return this.uploadFiles?.reduce((sum, f) => sum + (f?.size || 0), 0) || 0;
  1035. } catch {
  1036. let total = 0;
  1037. for (const f of this.uploadFiles || []) total += f?.size || 0;
  1038. return total;
  1039. }
  1040. }
  1041. /**
  1042. * 更新文件的选择空间
  1043. */
  1044. updateFileSpace(fileId: string, spaceId: string) {
  1045. const file = this.uploadFiles.find(f => f.id === fileId);
  1046. if (file) {
  1047. file.selectedSpace = spaceId;
  1048. this.cdr.markForCheck();
  1049. }
  1050. }
  1051. /**
  1052. * 更新文件的选择阶段
  1053. */
  1054. updateFileStage(fileId: string, stageId: string) {
  1055. const file = this.uploadFiles.find(f => f.id === fileId);
  1056. if (file) {
  1057. file.selectedStage = stageId;
  1058. this.cdr.markForCheck();
  1059. }
  1060. }
  1061. /**
  1062. * 获取空间名称
  1063. */
  1064. getSpaceName(spaceId: string): string {
  1065. const space = this.availableSpaces.find(s => s.id === spaceId);
  1066. return space?.name || '';
  1067. }
  1068. /**
  1069. * 获取阶段名称
  1070. */
  1071. getStageName(stageId: string): string {
  1072. const stage = this.availableStages.find(s => s.id === stageId);
  1073. return stage?.name || '';
  1074. }
  1075. /**
  1076. * 获取文件总数
  1077. */
  1078. getFileCount(): number {
  1079. return this.uploadFiles.length;
  1080. }
  1081. /**
  1082. * 检查是否可以确认上传
  1083. */
  1084. canConfirm(): boolean {
  1085. if (this.uploadFiles.length === 0) return false;
  1086. if (this.isAnalyzing) return false;
  1087. // 检查是否所有文件都已选择空间和阶段
  1088. return this.uploadFiles.every(f => f.selectedSpace && f.selectedStage);
  1089. }
  1090. /**
  1091. * 获取分析进度百分比
  1092. */
  1093. getAnalysisProgressPercent(): number {
  1094. if (this.uploadFiles.length === 0) return 0;
  1095. const processedCount = this.uploadFiles.filter(f => f.status !== 'pending').length;
  1096. return Math.round((processedCount / this.uploadFiles.length) * 100);
  1097. }
  1098. /**
  1099. * 获取已分析文件数量
  1100. */
  1101. getAnalyzedFilesCount(): number {
  1102. return this.uploadFiles.filter(f => f.analysisResult).length;
  1103. }
  1104. /**
  1105. * 🔥 查看完整图片
  1106. */
  1107. viewFullImage(file: UploadFile): void {
  1108. if (file.preview) {
  1109. this.viewingImage = file;
  1110. this.cdr.markForCheck();
  1111. console.log('🖼️ 打开图片查看器:', file.name);
  1112. }
  1113. }
  1114. /**
  1115. * 🔥 关闭图片查看器
  1116. */
  1117. closeImageViewer(): void {
  1118. this.viewingImage = null;
  1119. this.cdr.markForCheck();
  1120. console.log('❌ 关闭图片查看器');
  1121. }
  1122. /**
  1123. * 🔥 图片加载错误处理
  1124. */
  1125. onImageError(event: Event, file: UploadFile): void {
  1126. console.error('❌ 图片加载失败:', file.name, {
  1127. preview: file.preview ? file.preview.substring(0, 100) + '...' : 'null',
  1128. fileUrl: file.fileUrl,
  1129. isWxWork: this.isWxWorkEnvironment()
  1130. });
  1131. // 🔥 设置错误标记,让HTML显示placeholder而不是破损图标
  1132. file.imageLoadError = true;
  1133. // 标记视图需要更新
  1134. this.cdr.markForCheck();
  1135. // 在企业微信环境中,尝试使用ObjectURL作为备选方案
  1136. if (this.isWxWorkEnvironment() && this.isImageFile(file.file)) {
  1137. try {
  1138. const objectUrl = URL.createObjectURL(file.file);
  1139. // 清除错误标记
  1140. file.imageLoadError = false;
  1141. file.preview = objectUrl;
  1142. console.log('🔄 使用ObjectURL作为预览:', objectUrl);
  1143. this.cdr.markForCheck();
  1144. } catch (error) {
  1145. console.error('❌ 生成ObjectURL失败:', error);
  1146. file.imageLoadError = true; // 确保显示placeholder
  1147. this.cdr.markForCheck();
  1148. }
  1149. }
  1150. }
  1151. /**
  1152. * 检测是否在企业微信环境
  1153. */
  1154. private isWxWorkEnvironment(): boolean {
  1155. const ua = navigator.userAgent.toLowerCase();
  1156. return ua.includes('wxwork') || ua.includes('micromessenger');
  1157. }
  1158. /**
  1159. * 🔥 组件销毁时清理ObjectURL,避免内存泄漏
  1160. */
  1161. ngOnDestroy(): void {
  1162. console.log('🧹 组件销毁,清理ObjectURL资源...');
  1163. this.cleanupObjectURLs();
  1164. }
  1165. }