upload-success-modal.component.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  1. import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy, HostListener } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { ColorAnalysisService, AnalysisProgress, ColorAnalysisResult } from '../../services/color-analysis.service';
  4. import { FormAnalysisService } from '../../services/form-analysis.service';
  5. import { TextureAnalysisService } from '../../services/texture-analysis.service';
  6. import { PatternAnalysisService } from '../../services/pattern-analysis.service';
  7. import { LightingAnalysisService } from '../../services/lighting-analysis.service';
  8. import { RequirementMappingService } from '../../../services/requirement-mapping.service';
  9. import { AtmospherePreviewService } from '../../../services/atmosphere-preview.service';
  10. import { RequirementMapping, SceneTemplate } from '../../../models/requirement-mapping.interface';
  11. import { Subscription } from 'rxjs';
  12. import { modalAnimations } from './upload-success-modal.animations';
  13. export interface UploadedFile {
  14. id: string;
  15. name: string;
  16. url: string;
  17. size?: number;
  18. type?: 'image' | 'cad' | 'text';
  19. preview?: string;
  20. }
  21. export interface ColorInfo {
  22. hex: string;
  23. rgb: { r: number; g: number; b: number };
  24. percentage: number;
  25. name?: string; // 颜色名称,如"深蓝色"、"暖白色"等
  26. }
  27. @Component({
  28. selector: 'app-upload-success-modal',
  29. standalone: true,
  30. imports: [CommonModule],
  31. templateUrl: './upload-success-modal.component.html',
  32. styleUrls: ['./upload-success-modal.component.scss'],
  33. changeDetection: ChangeDetectionStrategy.OnPush,
  34. animations: modalAnimations
  35. })
  36. export class UploadSuccessModalComponent implements OnInit, OnDestroy {
  37. @Input() isVisible: boolean = false;
  38. @Input() uploadedFiles: UploadedFile[] = [];
  39. @Input() uploadType: 'image' | 'document' | 'mixed' = 'image';
  40. @Input() analysisResult?: ColorAnalysisResult;
  41. @Input() isAnalyzing: boolean = false; // 新增:从父组件接收分析状态
  42. @Output() closeModal = new EventEmitter<void>();
  43. @Output() analyzeColors = new EventEmitter<UploadedFile[]>();
  44. @Output() viewReport = new EventEmitter<ColorAnalysisResult>();
  45. @Output() generateRequirementMapping = new EventEmitter<RequirementMapping>(); // 新增:需求映射生成事件
  46. // 移除本地的isAnalyzing属性,使用@Input()的isAnalyzing
  47. // isAnalyzing = false; // 已移除,现在从父组件接收
  48. analysisProgress: AnalysisProgress | null = null;
  49. analysisError: string | null = null;
  50. // 需求映射相关状态
  51. requirementMapping: RequirementMapping | null = null;
  52. isGeneratingMapping = false;
  53. mappingError: string | null = null;
  54. // 增强分析标签页状态
  55. activeTab: 'color' | 'form' | 'texture' | 'pattern' | 'lighting' | 'mapping' = 'color';
  56. // 响应式状态
  57. isMobile = false;
  58. isTablet = false;
  59. // 动画状态
  60. animationState = 'idle';
  61. buttonHoverState = 'normal';
  62. copySuccess = false; // 复制成功状态
  63. private progressSubscription?: Subscription;
  64. private resizeSubscription?: Subscription;
  65. constructor(
  66. private colorAnalysisService: ColorAnalysisService,
  67. private formAnalysisService: FormAnalysisService,
  68. private textureAnalysisService: TextureAnalysisService,
  69. private patternAnalysisService: PatternAnalysisService,
  70. private lightingAnalysisService: LightingAnalysisService,
  71. private requirementMappingService: RequirementMappingService,
  72. private atmospherePreviewService: AtmospherePreviewService
  73. ) {}
  74. ngOnInit() {
  75. this.checkScreenSize();
  76. this.setupResizeListener();
  77. }
  78. ngOnDestroy() {
  79. this.progressSubscription?.unsubscribe();
  80. this.resizeSubscription?.unsubscribe();
  81. }
  82. // 响应式布局检测
  83. @HostListener('window:resize', ['$event'])
  84. onResize(event: any) {
  85. this.checkScreenSize();
  86. }
  87. @HostListener('document:keydown', ['$event'])
  88. onKeyDown(event: KeyboardEvent) {
  89. if (event.key === 'Escape' && this.isVisible) {
  90. this.onClose();
  91. }
  92. }
  93. // 开始颜色分析
  94. async startColorAnalysis() {
  95. if (this.uploadedFiles.length === 0 || this.uploadType !== 'image') {
  96. return;
  97. }
  98. this.analysisError = null;
  99. try {
  100. // 发射分析事件给父组件处理
  101. this.analyzeColors.emit(this.uploadedFiles);
  102. // 注意:不再调用本地的simulateAnalysis,而是等待父组件传递分析结果
  103. // 父组件会通过@Input() analysisResult传递分析结果
  104. // 父组件也会通过@Input() isAnalyzing控制分析状态
  105. } catch (error) {
  106. this.analysisError = '颜色分析失败,请重试';
  107. console.error('Color analysis error:', error);
  108. }
  109. }
  110. // 模拟分析过程
  111. private async simulateAnalysis(): Promise<void> {
  112. return new Promise((resolve) => {
  113. setTimeout(() => {
  114. // 模拟分析结果
  115. this.analysisResult = {
  116. colors: [
  117. { hex: '#8B4513', rgb: { r: 139, g: 69, b: 19 }, percentage: 35.2 },
  118. { hex: '#A0522D', rgb: { r: 160, g: 82, b: 45 }, percentage: 27.1 },
  119. { hex: '#D2B48C', rgb: { r: 210, g: 180, b: 140 }, percentage: 22.4 },
  120. { hex: '#DEB887', rgb: { r: 222, g: 184, b: 135 }, percentage: 12.5 },
  121. { hex: '#F5F5DC', rgb: { r: 245, g: 245, b: 220 }, percentage: 2.8 }
  122. ],
  123. originalImage: this.uploadedFiles[0]?.url || '',
  124. mosaicImage: '/assets/images/mock-mosaic.jpg',
  125. reportPath: '/reports/color-analysis-' + Date.now() + '.html',
  126. enhancedAnalysis: {
  127. colorWheel: {
  128. dominantHue: 45,
  129. saturationRange: { min: 20, max: 80 },
  130. brightnessRange: { min: 40, max: 90 },
  131. colorDistribution: [
  132. { hue: 45, saturation: 65, brightness: 75, percentage: 62.3 },
  133. { hue: 30, saturation: 45, brightness: 85, percentage: 22.4 },
  134. { hue: 40, saturation: 55, brightness: 70, percentage: 12.5 }
  135. ]
  136. },
  137. colorHarmony: {
  138. harmonyType: 'analogous',
  139. harmonyScore: 78,
  140. suggestions: ['保持温暖色调的统一性', '可适当增加对比色作为点缀'],
  141. relationships: [
  142. { color1: '#8B4513', color2: '#D2B48C', relationship: '相似色', strength: 85 }
  143. ]
  144. },
  145. colorTemperature: {
  146. averageTemperature: 3200,
  147. temperatureRange: { min: 2800, max: 3600 },
  148. warmCoolBalance: 65,
  149. temperatureDescription: '温暖色调,适合营造舒适氛围',
  150. lightingRecommendations: ['适合使用暖白光照明(2700K-3000K)', '营造温馨舒适的氛围']
  151. },
  152. colorPsychology: {
  153. mood: '温馨',
  154. atmosphere: '舒适',
  155. suitableSpaces: ['客厅', '卧室', '餐厅'],
  156. psychologicalEffects: ['放松身心', '增强温暖感', '促进交流'],
  157. emotionalImpact: {
  158. energy: 45,
  159. warmth: 85,
  160. sophistication: 60,
  161. comfort: 90
  162. }
  163. }
  164. },
  165. // 模拟其他分析结果
  166. formAnalysis: {
  167. lineAnalysis: {
  168. dominantLines: ['直线', '曲线'],
  169. dominantLineType: '直线',
  170. visualFlow: '流畅'
  171. },
  172. overallAssessment: {
  173. complexity: 65,
  174. visualImpact: 78
  175. }
  176. },
  177. textureAnalysis: {
  178. surfaceProperties: {
  179. roughness: { level: 'moderate', value: 50, description: '中等粗糙度' },
  180. glossiness: { level: 'satin', value: 40, reflectivity: 35 },
  181. transparency: { level: 'opaque', value: 10, description: '基本不透明' },
  182. reflectivity: { level: 'low', value: 25, description: '低反射' }
  183. },
  184. materialClassification: {
  185. primaryMaterials: ['木材', '织物'],
  186. secondaryMaterials: ['金属', '玻璃']
  187. }
  188. },
  189. patternAnalysis: {
  190. patternRecognition: {
  191. primaryPatterns: [{ type: 'geometric', confidence: 80, coverage: 60, characteristics: ['规则', '对称'] }],
  192. patternComplexity: { level: 'moderate', value: 60, description: '中等复杂度' }
  193. },
  194. visualRhythm: {
  195. rhythmType: { primary: 'regular', secondary: 'flowing' },
  196. movement: { direction: 'horizontal', intensity: 65 }
  197. }
  198. },
  199. lightingAnalysis: {
  200. lightSourceIdentification: {
  201. primarySources: ['自然光', '人工光'],
  202. lightingSetup: '混合照明'
  203. },
  204. ambientAnalysis: {
  205. ambientLight: '柔和',
  206. lightingMood: '温馨'
  207. }
  208. }
  209. };
  210. // 添加调试输出
  211. console.log('=== 分析结果数据结构调试 ===');
  212. console.log('完整分析结果:', this.analysisResult);
  213. console.log('形体分析:', this.analysisResult.formAnalysis);
  214. console.log('质感分析:', this.analysisResult.textureAnalysis);
  215. console.log('纹理分析:', this.analysisResult.patternAnalysis);
  216. console.log('灯光分析:', this.analysisResult.lightingAnalysis);
  217. console.log('色彩分析:', this.analysisResult.enhancedAnalysis);
  218. console.log('主要光源:', this.analysisResult.lightingAnalysis.lightSourceIdentification.primarySources);
  219. console.log('材质分类:', this.analysisResult.textureAnalysis.materialClassification);
  220. console.log('视觉节奏:', this.analysisResult.patternAnalysis.visualRhythm);
  221. // 生成需求映射
  222. this.generateRequirementMappingFromAnalysis();
  223. resolve();
  224. }, 2000);
  225. });
  226. }
  227. // 根据分析结果生成需求映射
  228. private generateRequirementMappingFromAnalysis(): void {
  229. if (!this.analysisResult) {
  230. return;
  231. }
  232. try {
  233. this.isGeneratingMapping = true;
  234. this.mappingError = null;
  235. // 使用需求映射服务生成映射
  236. this.requirementMappingService.generateRequirementMapping(this.analysisResult).subscribe({
  237. next: (mapping) => {
  238. this.requirementMapping = mapping;
  239. console.log('=== 需求映射生成成功 ===');
  240. console.log('需求映射结果:', this.requirementMapping);
  241. // 发出需求映射生成事件
  242. this.generateRequirementMapping.emit(this.requirementMapping);
  243. },
  244. error: (error) => {
  245. console.error('需求映射生成失败:', error);
  246. this.mappingError = '需求映射生成失败,请稍后重试';
  247. },
  248. complete: () => {
  249. this.isGeneratingMapping = false;
  250. }
  251. });
  252. } catch (error) {
  253. console.error('需求映射初始化失败:', error);
  254. this.mappingError = '需求映射初始化失败,请稍后重试';
  255. this.isGeneratingMapping = false;
  256. }
  257. }
  258. // 手动重新生成需求映射
  259. regenerateRequirementMapping(): void {
  260. if (!this.analysisResult) {
  261. return;
  262. }
  263. this.generateRequirementMappingFromAnalysis();
  264. }
  265. // 事件处理方法
  266. onClose() {
  267. this.closeModal.emit();
  268. }
  269. onBackdropClick(event: Event) {
  270. // 点击背景遮罩关闭弹窗
  271. this.onClose();
  272. }
  273. onAnalyzeColorsClick() {
  274. if (this.isAnalyzing || this.uploadedFiles.length === 0) {
  275. return;
  276. }
  277. this.animationState = 'loading';
  278. this.analyzeColors.emit(this.uploadedFiles);
  279. }
  280. onViewReportClick() {
  281. if (this.analysisResult) {
  282. // 创建一个新的窗口来显示完整报告
  283. const reportWindow = window.open('', '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes');
  284. if (reportWindow) {
  285. // 生成完整的HTML报告内容
  286. const reportHtml = this.generateFullReport();
  287. reportWindow.document.write(reportHtml);
  288. reportWindow.document.close();
  289. reportWindow.focus();
  290. } else {
  291. // 如果弹窗被阻止,则下载报告文件
  292. this.downloadReport();
  293. }
  294. // 触发事件给父组件
  295. this.viewReport.emit(this.analysisResult);
  296. }
  297. }
  298. // 工具方法
  299. shouldShowColorAnalysis(): boolean {
  300. return this.uploadType === 'image' || this.hasImageFiles();
  301. }
  302. formatFileSize(bytes: number): string {
  303. if (bytes === 0) return '0 Bytes';
  304. const k = 1024;
  305. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  306. const i = Math.floor(Math.log(bytes) / Math.log(k));
  307. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  308. }
  309. getFileTypeIcon(file: UploadedFile): string {
  310. if (file.type?.startsWith('image/')) {
  311. return 'image';
  312. } else if (file.type?.includes('pdf')) {
  313. return 'pdf';
  314. } else if (file.type?.includes('word') || file.type?.includes('doc')) {
  315. return 'document';
  316. } else {
  317. return 'file';
  318. }
  319. }
  320. hasImageFiles(): boolean {
  321. return this.uploadedFiles.some(file => file.type?.startsWith('image/'));
  322. }
  323. // 生成颜色描述文字
  324. generateColorDescription(): string {
  325. if (!this.analysisResult || !this.analysisResult.colors.length) {
  326. return '';
  327. }
  328. const colorDescriptions = this.analysisResult.colors.map(color => {
  329. const colorName = color.name || this.getColorName(color.hex);
  330. return `${colorName}(${color.hex}) ${color.percentage}%`;
  331. });
  332. return `主要色彩:${colorDescriptions.join('、')}`;
  333. }
  334. // 根据色值获取颜色名称
  335. private getColorName(hex: string): string {
  336. const colorMap: { [key: string]: string } = {
  337. '#FFFFFF': '纯白色',
  338. '#F5F5F5': '白烟',
  339. '#E5E5E5': '浅灰色',
  340. '#CCCCCC': '中灰色',
  341. '#999999': '深灰色',
  342. '#666666': '暗灰色',
  343. '#333333': '深暗灰',
  344. '#000000': '纯黑色',
  345. '#FF0000': '红色',
  346. '#00FF00': '酸橙色',
  347. '#0000FF': '蓝色',
  348. '#FFFF00': '黄色',
  349. '#FF00FF': '品红色',
  350. '#00FFFF': '青色',
  351. '#FFA500': '橙色',
  352. '#800080': '紫色',
  353. '#008000': '绿色',
  354. '#000080': '海军蓝',
  355. '#800000': '栗色',
  356. '#808000': '橄榄色',
  357. '#008080': '水鸭色',
  358. '#C0C0C0': '银色',
  359. '#808080': '灰色',
  360. '#FFE4E1': '雾玫瑰',
  361. '#F0F8FF': '爱丽丝蓝',
  362. '#FAEBD7': '古董白',
  363. '#F5F5DC': '米色',
  364. '#DEB887': '硬木色',
  365. '#A52A2A': '棕色',
  366. '#D2691E': '巧克力色',
  367. '#FF7F50': '珊瑚色',
  368. '#6495ED': '矢车菊蓝',
  369. '#DC143C': '深红色',
  370. '#00008B': '深蓝色',
  371. '#B8860B': '深金色',
  372. '#A9A9A9': '深灰色',
  373. '#006400': '深绿色',
  374. '#BDB76B': '深卡其色',
  375. '#8B008B': '深品红',
  376. '#556B2F': '深橄榄绿',
  377. '#FF8C00': '深橙色',
  378. '#9932CC': '深兰花紫',
  379. '#8B0000': '深红色2',
  380. '#E9967A': '深鲑鱼色',
  381. '#8FBC8F': '深海绿',
  382. '#483D8B': '深石板蓝',
  383. '#2F4F4F': '深石板灰',
  384. '#00CED1': '深绿松石',
  385. '#9400D3': '深紫罗兰',
  386. '#FF1493': '深粉红',
  387. '#00BFFF': '深天蓝',
  388. '#696969': '暗灰色2',
  389. '#1E90FF': '道奇蓝',
  390. '#B22222': '火砖色',
  391. '#FFFAF0': '花白色',
  392. '#228B22': '森林绿',
  393. '#DCDCDC': '淡灰色',
  394. '#F8F8FF': '幽灵白',
  395. '#FFD700': '金色',
  396. '#DAA520': '金麒麟色',
  397. '#ADFF2F': '绿黄色',
  398. '#F0FFF0': '蜜瓜色',
  399. '#FF69B4': '热粉红',
  400. '#CD5C5C': '印度红',
  401. '#4B0082': '靛青色',
  402. '#FFFFF0': '象牙色',
  403. '#F0E68C': '卡其色',
  404. '#E6E6FA': '薰衣草色',
  405. '#FFF0F5': '薰衣草红',
  406. '#7CFC00': '草坪绿',
  407. '#FFFACD': '柠檬绸',
  408. '#ADD8E6': '浅蓝色',
  409. '#F08080': '浅珊瑚色',
  410. '#E0FFFF': '浅青色',
  411. '#FAFAD2': '浅金菊黄',
  412. '#D3D3D3': '浅灰色2',
  413. '#90EE90': '浅绿色',
  414. '#FFB6C1': '浅粉红',
  415. '#FFA07A': '浅鲑鱼色',
  416. '#20B2AA': '浅海绿',
  417. '#87CEFA': '浅天蓝',
  418. '#778899': '浅石板灰',
  419. '#B0C4DE': '浅钢蓝',
  420. '#FFFFE0': '浅黄色',
  421. '#32CD32': '酸橙绿',
  422. '#FAF0E6': '亚麻色',
  423. '#66CDAA': '中海绿',
  424. '#0000CD': '中蓝色',
  425. '#BA55D3': '中兰花紫',
  426. '#9370DB': '中紫色',
  427. '#3CB371': '中海春绿',
  428. '#7B68EE': '中石板蓝',
  429. '#00FA9A': '中春绿',
  430. '#48D1CC': '中绿松石',
  431. '#C71585': '中紫罗兰红',
  432. '#191970': '午夜蓝',
  433. '#F5FFFA': '薄荷奶油',
  434. '#FFDEAD': '那瓦霍白',
  435. '#FDF5E6': '老花边',
  436. '#6B8E23': '橄榄褐色',
  437. '#FF4500': '橙红色',
  438. '#DA70D6': '兰花紫',
  439. '#EEE8AA': '灰秋麒麟',
  440. '#98FB98': '灰绿色',
  441. '#AFEEEE': '灰绿松石',
  442. '#DB7093': '灰紫罗兰红',
  443. '#FFEFD5': '番木瓜鞭',
  444. '#FFDAB9': '桃扑',
  445. '#CD853F': '秘鲁色',
  446. '#FFC0CB': '粉红色',
  447. '#DDA0DD': '洋李色',
  448. '#B0E0E6': '粉蓝色',
  449. '#BC8F8F': '玫瑰棕色',
  450. '#4169E1': '皇家蓝',
  451. '#8B4513': '马鞍棕色',
  452. '#FA8072': '鲑鱼色',
  453. '#F4A460': '沙棕色',
  454. '#2E8B57': '海绿色',
  455. '#FFF5EE': '海贝色',
  456. '#A0522D': '赭色',
  457. '#87CEEB': '天蓝色',
  458. '#6A5ACD': '石板蓝',
  459. '#708090': '石板灰',
  460. '#FFFAFA': '雪色',
  461. '#00FF7F': '春绿色',
  462. '#4682B4': '钢蓝色',
  463. '#D2B48C': '棕褐色',
  464. '#D8BFD8': '蓟色',
  465. '#FF6347': '番茄色',
  466. '#40E0D0': '绿松石',
  467. '#EE82EE': '紫罗兰',
  468. '#F5DEB3': '小麦色',
  469. '#9ACD32': '黄绿色'
  470. };
  471. // 如果找到精确匹配,返回对应名称
  472. if (colorMap[hex.toUpperCase()]) {
  473. return colorMap[hex.toUpperCase()];
  474. }
  475. // 否则根据RGB值判断颜色类型
  476. const rgb = this.hexToRgb(hex);
  477. if (!rgb) return '未知颜色';
  478. const { r, g, b } = rgb;
  479. const brightness = (r * 299 + g * 587 + b * 114) / 1000;
  480. // 判断是否为灰色系
  481. const isGray = Math.abs(r - g) < 30 && Math.abs(g - b) < 30 && Math.abs(r - b) < 30;
  482. if (isGray) {
  483. if (brightness > 240) return '浅灰白';
  484. if (brightness > 200) return '浅灰色';
  485. if (brightness > 160) return '中灰色';
  486. if (brightness > 120) return '深灰色';
  487. if (brightness > 80) return '暗灰色';
  488. return '深暗灰';
  489. }
  490. // 判断主要颜色倾向
  491. const max = Math.max(r, g, b);
  492. const min = Math.min(r, g, b);
  493. const saturation = max === 0 ? 0 : (max - min) / max;
  494. if (saturation < 0.2) {
  495. // 低饱和度,偏向灰色
  496. if (brightness > 200) return '浅灰色';
  497. if (brightness > 100) return '中灰色';
  498. return '深灰色';
  499. }
  500. // 高饱和度,判断色相
  501. let colorName = '';
  502. if (r >= g && r >= b) {
  503. if (g > b) {
  504. colorName = brightness > 150 ? '浅橙色' : '橙色';
  505. } else {
  506. colorName = brightness > 150 ? '浅红色' : '红色';
  507. }
  508. } else if (g >= r && g >= b) {
  509. if (r > b) {
  510. colorName = brightness > 150 ? '浅黄绿' : '黄绿色';
  511. } else {
  512. colorName = brightness > 150 ? '浅绿色' : '绿色';
  513. }
  514. } else {
  515. if (r > g) {
  516. colorName = brightness > 150 ? '浅紫色' : '紫色';
  517. } else {
  518. colorName = brightness > 150 ? '浅蓝色' : '蓝色';
  519. }
  520. }
  521. return colorName;
  522. }
  523. // 将十六进制颜色转换为RGB
  524. private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
  525. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  526. return result ? {
  527. r: parseInt(result[1], 16),
  528. g: parseInt(result[2], 16),
  529. b: parseInt(result[3], 16)
  530. } : null;
  531. }
  532. // 复制颜色描述到剪贴板
  533. async copyColorDescription(): Promise<void> {
  534. const description = this.generateColorDescription();
  535. if (!description) return;
  536. try {
  537. await navigator.clipboard.writeText(description);
  538. this.copySuccess = true;
  539. console.log('颜色描述已复制到剪贴板');
  540. // 2秒后重置复制状态
  541. setTimeout(() => {
  542. this.copySuccess = false;
  543. }, 2000);
  544. } catch (err) {
  545. console.error('复制失败:', err);
  546. // 降级方案:使用传统方法
  547. this.fallbackCopyTextToClipboard(description);
  548. }
  549. }
  550. // 降级复制方案
  551. private fallbackCopyTextToClipboard(text: string): void {
  552. const textArea = document.createElement('textarea');
  553. textArea.value = text;
  554. textArea.style.top = '0';
  555. textArea.style.left = '0';
  556. textArea.style.position = 'fixed';
  557. textArea.style.opacity = '0';
  558. document.body.appendChild(textArea);
  559. textArea.focus();
  560. textArea.select();
  561. try {
  562. const successful = document.execCommand('copy');
  563. if (successful) {
  564. this.copySuccess = true;
  565. console.log('颜色描述已复制到剪贴板(降级方案)');
  566. // 2秒后重置复制状态
  567. setTimeout(() => {
  568. this.copySuccess = false;
  569. }, 2000);
  570. }
  571. } catch (err) {
  572. console.error('降级复制方案也失败了:', err);
  573. }
  574. document.body.removeChild(textArea);
  575. }
  576. // 生成完整的HTML报告
  577. private generateFullReport(): string {
  578. if (!this.analysisResult) return '';
  579. const colors = this.analysisResult.colors;
  580. const colorDescription = this.generateColorDescription();
  581. return `
  582. <!DOCTYPE html>
  583. <html lang="zh-CN">
  584. <head>
  585. <meta charset="UTF-8">
  586. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  587. <title>图片颜色分析完整报告</title>
  588. <style>
  589. body {
  590. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  591. line-height: 1.6;
  592. color: #333;
  593. max-width: 1200px;
  594. margin: 0 auto;
  595. padding: 40px 20px;
  596. background: #f8f9fa;
  597. }
  598. .report-container {
  599. background: white;
  600. border-radius: 16px;
  601. padding: 40px;
  602. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
  603. }
  604. h1 {
  605. color: #1d1d1f;
  606. font-size: 32px;
  607. font-weight: 700;
  608. margin-bottom: 8px;
  609. text-align: center;
  610. }
  611. .subtitle {
  612. color: #666;
  613. font-size: 16px;
  614. text-align: center;
  615. margin-bottom: 40px;
  616. }
  617. .section {
  618. margin-bottom: 40px;
  619. }
  620. .section-title {
  621. color: #1d1d1f;
  622. font-size: 24px;
  623. font-weight: 600;
  624. margin-bottom: 20px;
  625. border-bottom: 2px solid #007AFF;
  626. padding-bottom: 8px;
  627. }
  628. .color-grid {
  629. display: grid;
  630. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  631. gap: 20px;
  632. margin-bottom: 30px;
  633. }
  634. .color-item {
  635. background: #f9f9f9;
  636. border-radius: 12px;
  637. padding: 20px;
  638. text-align: center;
  639. border: 1px solid #e5e5ea;
  640. transition: transform 0.2s ease;
  641. }
  642. .color-item:hover {
  643. transform: translateY(-2px);
  644. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
  645. }
  646. .color-swatch {
  647. width: 80px;
  648. height: 80px;
  649. border-radius: 50%;
  650. margin: 0 auto 16px;
  651. border: 3px solid white;
  652. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  653. }
  654. .color-name {
  655. font-weight: 600;
  656. font-size: 16px;
  657. color: #1d1d1f;
  658. margin-bottom: 4px;
  659. }
  660. .color-hex {
  661. font-family: 'Monaco', 'Menlo', monospace;
  662. font-size: 14px;
  663. color: #666;
  664. margin-bottom: 4px;
  665. }
  666. .color-percentage {
  667. font-size: 18px;
  668. font-weight: 700;
  669. color: #007AFF;
  670. }
  671. .description-section {
  672. background: #f9f9f9;
  673. border-radius: 12px;
  674. padding: 24px;
  675. border-left: 4px solid #007AFF;
  676. }
  677. .description-text {
  678. font-size: 16px;
  679. line-height: 1.8;
  680. color: #333;
  681. white-space: pre-wrap;
  682. }
  683. .stats-grid {
  684. display: grid;
  685. grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  686. gap: 16px;
  687. margin-top: 30px;
  688. }
  689. .stat-item {
  690. text-align: center;
  691. padding: 16px;
  692. background: #f0f8ff;
  693. border-radius: 8px;
  694. }
  695. .stat-value {
  696. font-size: 24px;
  697. font-weight: 700;
  698. color: #007AFF;
  699. }
  700. .stat-label {
  701. font-size: 14px;
  702. color: #666;
  703. margin-top: 4px;
  704. }
  705. .enhanced-section {
  706. background: #f8f9fa;
  707. border-radius: 12px;
  708. padding: 24px;
  709. margin-bottom: 30px;
  710. }
  711. .enhanced-title {
  712. font-size: 20px;
  713. font-weight: 600;
  714. color: #333;
  715. margin-bottom: 20px;
  716. display: flex;
  717. align-items: center;
  718. gap: 8px;
  719. }
  720. .enhanced-title::before {
  721. content: '';
  722. width: 4px;
  723. height: 20px;
  724. background: #007AFF;
  725. border-radius: 2px;
  726. }
  727. .analysis-grid {
  728. display: grid;
  729. grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  730. gap: 20px;
  731. }
  732. .analysis-card {
  733. background: white;
  734. border-radius: 8px;
  735. padding: 20px;
  736. border: 1px solid #e5e5ea;
  737. }
  738. .analysis-card h4 {
  739. font-size: 16px;
  740. font-weight: 600;
  741. color: #333;
  742. margin-bottom: 12px;
  743. }
  744. .info-row {
  745. display: flex;
  746. justify-content: space-between;
  747. align-items: center;
  748. padding: 8px 0;
  749. border-bottom: 1px solid #f0f0f0;
  750. }
  751. .info-row:last-child {
  752. border-bottom: none;
  753. }
  754. .info-label {
  755. font-weight: 500;
  756. color: #666;
  757. font-size: 14px;
  758. }
  759. .info-value {
  760. font-weight: 600;
  761. color: #333;
  762. font-size: 14px;
  763. }
  764. .tag-list {
  765. display: flex;
  766. flex-wrap: wrap;
  767. gap: 8px;
  768. margin-top: 8px;
  769. }
  770. .tag {
  771. padding: 4px 10px;
  772. background: #e3f2fd;
  773. color: #1976d2;
  774. border-radius: 12px;
  775. font-size: 12px;
  776. font-weight: 500;
  777. }
  778. .progress-bar {
  779. width: 100%;
  780. height: 8px;
  781. background: #f0f0f0;
  782. border-radius: 4px;
  783. overflow: hidden;
  784. margin: 8px 0;
  785. }
  786. .progress-fill {
  787. height: 100%;
  788. background: linear-gradient(90deg, #007AFF, #34C759);
  789. border-radius: 4px;
  790. }
  791. .footer {
  792. text-align: center;
  793. margin-top: 40px;
  794. padding-top: 20px;
  795. border-top: 1px solid #e5e5ea;
  796. color: #666;
  797. font-size: 14px;
  798. }
  799. @media print {
  800. body { background: white; }
  801. .report-container { box-shadow: none; }
  802. }
  803. </style>
  804. </head>
  805. <body>
  806. <div class="report-container">
  807. <h1>图片颜色分析报告</h1>
  808. <p class="subtitle">基于AI智能分析生成的详细颜色报告</p>
  809. <div class="section">
  810. <h2 class="section-title">颜色分析结果</h2>
  811. <div class="color-grid">
  812. ${colors.map(color => `
  813. <div class="color-item">
  814. <div class="color-swatch" style="background-color: ${color.hex}"></div>
  815. <div class="color-name">${this.getColorName(color.hex)}</div>
  816. <div class="color-hex">${color.hex}</div>
  817. <div class="color-percentage">${color.percentage}%</div>
  818. </div>
  819. `).join('')}
  820. </div>
  821. <div class="stats-grid">
  822. <div class="stat-item">
  823. <div class="stat-value">${colors.length}</div>
  824. <div class="stat-label">主要颜色数量</div>
  825. </div>
  826. <div class="stat-item">
  827. <div class="stat-value">${colors[0]?.percentage || 0}%</div>
  828. <div class="stat-label">主色占比</div>
  829. </div>
  830. <div class="stat-item">
  831. <div class="stat-value">${colors.reduce((sum, c) => sum + c.percentage, 0).toFixed(1)}%</div>
  832. <div class="stat-label">总覆盖率</div>
  833. </div>
  834. </div>
  835. </div>
  836. ${this.analysisResult.enhancedAnalysis ? `
  837. <div class="section">
  838. <h2 class="section-title">增强色彩分析</h2>
  839. <div class="enhanced-section">
  840. <div class="enhanced-title">色轮分析</div>
  841. <div class="analysis-grid">
  842. <div class="analysis-card">
  843. <h4>色相分布</h4>
  844. <div class="info-row">
  845. <span class="info-label">主色调:</span>
  846. <span class="info-value">${this.analysisResult.enhancedAnalysis.colorWheel.dominantHue}°</span>
  847. </div>
  848. <div class="info-row">
  849. <span class="info-label">饱和度范围:</span>
  850. <span class="info-value">${this.analysisResult.enhancedAnalysis.colorWheel.saturationRange.min}% - ${this.analysisResult.enhancedAnalysis.colorWheel.saturationRange.max}%</span>
  851. </div>
  852. </div>
  853. <div class="analysis-card">
  854. <h4>色彩心理学</h4>
  855. <div class="info-row">
  856. <span class="info-label">情绪:</span>
  857. <span class="info-value">${this.analysisResult.enhancedAnalysis.colorPsychology.mood}</span>
  858. </div>
  859. <div class="info-row">
  860. <span class="info-label">氛围:</span>
  861. <span class="info-value">${this.analysisResult.enhancedAnalysis.colorPsychology.atmosphere}</span>
  862. </div>
  863. <div class="tag-list">
  864. ${this.analysisResult.enhancedAnalysis.colorPsychology.suitableSpaces.map(space => `<span class="tag">${space}</span>`).join('')}
  865. </div>
  866. </div>
  867. </div>
  868. </div>
  869. </div>
  870. ` : ''}
  871. ${this.analysisResult.formAnalysis ? `
  872. <div class="section">
  873. <h2 class="section-title">形体分析</h2>
  874. <div class="enhanced-section">
  875. <div class="analysis-grid">
  876. <div class="analysis-card">
  877. <h4>线条分析</h4>
  878. <div class="info-row">
  879. <span class="info-label">主导线条:</span>
  880. <span class="info-value">${this.analysisResult.formAnalysis.lineAnalysis.dominantLineType}</span>
  881. </div>
  882. <div class="info-row">
  883. <span class="info-label">视觉流动:</span>
  884. <span class="info-value">${this.analysisResult.formAnalysis.lineAnalysis.visualFlow}</span>
  885. </div>
  886. </div>
  887. <div class="analysis-card">
  888. <h4>整体评估</h4>
  889. <div class="info-row">
  890. <span class="info-label">复杂度:</span>
  891. <span class="info-value">${this.analysisResult.formAnalysis.overallAssessment.complexity}%</span>
  892. </div>
  893. <div class="progress-bar">
  894. <div class="progress-fill" style="width: ${this.analysisResult.formAnalysis.overallAssessment.complexity}%"></div>
  895. </div>
  896. <div class="info-row">
  897. <span class="info-label">视觉冲击:</span>
  898. <span class="info-value">${this.analysisResult.formAnalysis.overallAssessment.visualImpact}%</span>
  899. </div>
  900. <div class="progress-bar">
  901. <div class="progress-fill" style="width: ${this.analysisResult.formAnalysis.overallAssessment.visualImpact}%"></div>
  902. </div>
  903. </div>
  904. </div>
  905. </div>
  906. </div>
  907. ` : ''}
  908. ${this.analysisResult.textureAnalysis ? `
  909. <div class="section">
  910. <h2 class="section-title">质感分析</h2>
  911. <div class="enhanced-section">
  912. <div class="analysis-grid">
  913. <div class="analysis-card">
  914. <h4>表面属性</h4>
  915. <div class="info-row">
  916. <span class="info-label">粗糙度:</span>
  917. <span class="info-value">${this.analysisResult.textureAnalysis.surfaceProperties.roughness}%</span>
  918. </div>
  919. <div class="progress-bar">
  920. <div class="progress-fill" style="width: ${this.analysisResult.textureAnalysis.surfaceProperties.roughness}%"></div>
  921. </div>
  922. <div class="info-row">
  923. <span class="info-label">光泽度:</span>
  924. <span class="info-value">${this.analysisResult.textureAnalysis.surfaceProperties.glossiness}%</span>
  925. </div>
  926. <div class="progress-bar">
  927. <div class="progress-fill" style="width: ${this.analysisResult.textureAnalysis.surfaceProperties.glossiness}%"></div>
  928. </div>
  929. </div>
  930. <div class="analysis-card">
  931. <h4>材质分类</h4>
  932. <div class="info-row">
  933. <span class="info-label">主要材质:</span>
  934. </div>
  935. <div class="tag-list">
  936. ${this.analysisResult.textureAnalysis.materialClassification.primaryMaterials.map((material: string) => `<span class="tag">${material}</span>`).join('')}
  937. </div>
  938. <div class="info-row">
  939. <span class="info-label">次要材质:</span>
  940. </div>
  941. <div class="tag-list">
  942. ${this.analysisResult.textureAnalysis.materialClassification.secondaryMaterials.map((material: string) => `<span class="tag">${material}</span>`).join('')}
  943. </div>
  944. </div>
  945. </div>
  946. </div>
  947. </div>
  948. ` : ''}
  949. ${this.analysisResult.patternAnalysis ? `
  950. <div class="section">
  951. <h2 class="section-title">纹理分析</h2>
  952. <div class="enhanced-section">
  953. <div class="analysis-grid">
  954. <div class="analysis-card">
  955. <h4>图案识别</h4>
  956. <div class="info-row">
  957. <span class="info-label">主要图案:</span>
  958. <span class="info-value">${this.analysisResult.patternAnalysis.patternRecognition.primaryPatterns[0]?.type || '无'}</span>
  959. </div>
  960. <div class="info-row">
  961. <span class="info-label">复杂度:</span>
  962. <span class="info-value">${this.analysisResult.patternAnalysis.patternRecognition.complexity}</span>
  963. </div>
  964. </div>
  965. <div class="analysis-card">
  966. <h4>视觉节奏</h4>
  967. <div class="info-row">
  968. <span class="info-label">节奏类型:</span>
  969. <span class="info-value">${this.analysisResult.patternAnalysis.visualRhythm.rhythmType}</span>
  970. </div>
  971. <div class="info-row">
  972. <span class="info-label">运动感:</span>
  973. <span class="info-value">${this.analysisResult.patternAnalysis.visualRhythm.movement}</span>
  974. </div>
  975. </div>
  976. </div>
  977. </div>
  978. </div>
  979. ` : ''}
  980. ${this.analysisResult.lightingAnalysis ? `
  981. <div class="section">
  982. <h2 class="section-title">灯光分析</h2>
  983. <div class="enhanced-section">
  984. <div class="analysis-grid">
  985. <div class="analysis-card">
  986. <h4>光源识别</h4>
  987. <div class="info-row">
  988. interface RequirementMapping {
  989. sceneGeneration: {
  990. baseScene: string; // 固定场景模板
  991. parameters: SceneParams; // 场景参数
  992. atmospherePreview: string; // 氛围感预览图
  993. };
  994. parameterMapping: {
  995. colorParams: ColorMappingParams;
  996. spaceParams: SpaceMappingParams;
  997. materialParams: MaterialMappingParams;
  998. };
  999. }interface RequirementMapping {
  1000. sceneGeneration: {
  1001. baseScene: string; // 固定场景模板
  1002. parameters: SceneParams; // 场景参数
  1003. atmospherePreview: string; // 氛围感预览图
  1004. };
  1005. parameterMapping: {
  1006. colorParams: ColorMappingParams;
  1007. spaceParams: SpaceMappingParams;
  1008. materialParams: MaterialMappingParams;
  1009. };
  1010. }interface RequirementMapping {
  1011. sceneGeneration: {
  1012. baseScene: string; // 固定场景模板
  1013. parameters: SceneParams; // 场景参数
  1014. atmospherePreview: string; // 氛围感预览图
  1015. };
  1016. parameterMapping: {
  1017. colorParams: ColorMappingParams;
  1018. spaceParams: SpaceMappingParams;
  1019. materialParams: MaterialMappingParams;
  1020. };
  1021. } <span class="info-label">主要光源:</span>
  1022. <span class="info-value">${this.analysisResult.lightingAnalysis.lightSourceIdentification.primarySources[0]?.type || '无'} - ${this.analysisResult.lightingAnalysis.lightSourceIdentification.primarySources[0]?.subtype || ''}</span>
  1023. </div>
  1024. <div class="info-row">
  1025. <span class="info-label">灯光复杂度:</span>
  1026. <span class="info-value">${this.analysisResult.lightingAnalysis.lightSourceIdentification.lightingSetup.complexity}</span>
  1027. </div>
  1028. <div class="info-row">
  1029. <span class="info-label">灯光风格:</span>
  1030. <span class="info-value">${this.analysisResult.lightingAnalysis.lightSourceIdentification.lightingSetup.lightingStyle}</span>
  1031. </div>
  1032. </div>
  1033. <div class="analysis-card">
  1034. <h4>环境分析</h4>
  1035. <div class="info-row">
  1036. <span class="info-label">环境光强度:</span>
  1037. <span class="info-value">${this.analysisResult.lightingAnalysis.ambientAnalysis.ambientLevel.strength}%</span>
  1038. </div>
  1039. <div class="info-row">
  1040. <span class="info-label">灯光情绪:</span>
  1041. <span class="info-value">${this.analysisResult.lightingAnalysis.ambientAnalysis.lightingMood.primary}</span>
  1042. </div>
  1043. <div class="info-row">
  1044. <span class="info-label">情绪强度:</span>
  1045. <span class="info-value">${this.analysisResult.lightingAnalysis.ambientAnalysis.lightingMood.intensity}%</span>
  1046. </div>
  1047. </div>
  1048. </div>
  1049. </div>
  1050. </div>
  1051. ` : ''}
  1052. <div class="section">
  1053. <h2 class="section-title">颜色描述文字</h2>
  1054. <div class="description-section">
  1055. <div class="description-text">${colorDescription}</div>
  1056. </div>
  1057. </div>
  1058. <div class="footer">
  1059. <p>报告生成时间:${new Date().toLocaleString('zh-CN')}</p>
  1060. <p>由颜色分析系统自动生成</p>
  1061. </div>
  1062. </div>
  1063. </body>
  1064. </html>`;
  1065. }
  1066. // 下载报告文件
  1067. private downloadReport(): void {
  1068. if (!this.analysisResult) return;
  1069. const reportHtml = this.generateFullReport();
  1070. const blob = new Blob([reportHtml], { type: 'text/html;charset=utf-8' });
  1071. const url = URL.createObjectURL(blob);
  1072. const link = document.createElement('a');
  1073. link.href = url;
  1074. link.download = `颜色分析报告_${new Date().toISOString().slice(0, 10)}.html`;
  1075. document.body.appendChild(link);
  1076. link.click();
  1077. document.body.removeChild(link);
  1078. URL.revokeObjectURL(url);
  1079. }
  1080. private checkScreenSize(): void {
  1081. const width = window.innerWidth;
  1082. this.isMobile = width < 768;
  1083. this.isTablet = width >= 768 && width < 1024;
  1084. }
  1085. private setupResizeListener(): void {
  1086. // 可以添加更复杂的响应式逻辑
  1087. }
  1088. // 获取质感分析属性数组,用于模板中的循环显示
  1089. getTextureProperties(): Array<{name: string, value: number}> {
  1090. if (!this.analysisResult?.textureAnalysis?.surfaceProperties) {
  1091. return [];
  1092. }
  1093. const properties = this.analysisResult.textureAnalysis.surfaceProperties;
  1094. return [
  1095. { name: '粗糙度', value: properties.roughness?.value || 0 },
  1096. { name: '光泽度', value: properties.glossiness?.value || 0 },
  1097. { name: '透明度', value: properties.transparency?.value || 0 },
  1098. { name: '反射率', value: properties.reflectivity?.value || 0 }
  1099. ];
  1100. }
  1101. }