drag-upload-modal.component.ts 44 KB

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