Explorar o código

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

徐福静0235668 hai 9 horas
pai
achega
103c0cddca

+ 12 - 1
src/app/shared/components/requirements-confirm-card/requirements-confirm-card.html

@@ -762,4 +762,15 @@
       </label>
     </div>
   </div>
-</div>
+</div>
+
+<!-- 上传成功弹窗 -->
+<app-upload-success-modal
+  [isVisible]="showUploadSuccessModal"
+  [uploadedFiles]="uploadedFiles"
+  [uploadType]="uploadType"
+  [analysisResult]="colorAnalysisResult"
+  (closeModal)="onModalClose()"
+  (analyzeColors)="onAnalyzeColors()"
+  (viewReport)="onViewReport()">
+</app-upload-success-modal>

+ 60 - 3
src/app/shared/components/requirements-confirm-card/requirements-confirm-card.ts

@@ -1,6 +1,8 @@
 import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { UploadSuccessModalComponent } from '../upload-success-modal/upload-success-modal.component';
+import { ColorAnalysisService, ColorAnalysisResult } from '../../services/color-analysis.service';
 
 // 素材文件接口
 interface MaterialFile {
@@ -73,7 +75,7 @@ interface RequirementItem {
 @Component({
   selector: 'app-requirements-confirm-card',
   standalone: true,
-  imports: [CommonModule, FormsModule, ReactiveFormsModule],
+  imports: [CommonModule, FormsModule, ReactiveFormsModule, UploadSuccessModalComponent],
   templateUrl: './requirements-confirm-card.html',
   styleUrls: ['./requirements-confirm-card.scss'],
   changeDetection: ChangeDetectionStrategy.Default // 确保使用默认变更检测策略
@@ -190,7 +192,13 @@ export class RequirementsConfirmCardComponent implements OnInit, OnDestroy {
   consistencyWarnings: string[] = [];
   historyStates: any[] = [];
 
-  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
+  // 上传成功弹窗相关
+  showUploadSuccessModal = false;
+  uploadedFiles: { id: string; name: string; url: string; size?: number; type?: 'image' | 'cad' | 'text'; preview?: string }[] = [];
+  uploadType: 'image' | 'document' | 'mixed' = 'image';
+  colorAnalysisResult?: ColorAnalysisResult;
+
+  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef, private colorAnalysisService: ColorAnalysisService) {}
 
   ngOnInit() {
     this.initializeForms();
@@ -342,6 +350,17 @@ export class RequirementsConfirmCardComponent implements OnInit, OnDestroy {
       this.materials.push(materialFile);
       this.isUploading = false;
       
+      // 显示上传成功弹窗
+      this.showUploadSuccessModal = true;
+      this.uploadedFiles = [{
+        id: materialFile.id,
+        name: file.name,
+        url: materialFile.url || '',
+        size: file.size,
+        type: type === 'text' ? 'text' : type === 'image' ? 'image' : 'cad'
+      }];
+      this.uploadType = type === 'text' ? 'document' : type === 'image' ? 'image' : 'mixed';
+      
       // 自动解析
       this.analyzeMaterial(materialFile);
     }, 1000);
@@ -1397,10 +1416,48 @@ export class RequirementsConfirmCardComponent implements OnInit, OnDestroy {
   getSaveStatusIcon(): string {
     switch (this.saveStatus) {
       case 'saved': return '✓';
-      case 'saving': return '';
+      case 'saving': return '';
       case 'error': return '⚠';
       case 'unsaved': return '●';
       default: return '';
     }
   }
+
+  // 上传成功弹窗相关方法
+  onModalClose(): void {
+    this.showUploadSuccessModal = false;
+    this.uploadedFiles = [];
+    this.colorAnalysisResult = undefined;
+  }
+
+  onAnalyzeColors(): void {
+    if (this.uploadedFiles.length > 0 && this.uploadType === 'image') {
+      const imageFile = this.uploadedFiles[0];
+      
+      // 创建一个File对象用于分析
+      const file = new File([], imageFile.name, { type: imageFile.type || 'image/jpeg' });
+      
+      // 使用模拟分析(在实际应用中应该调用真实的API)
+      this.colorAnalysisService.simulateAnalysis(file).subscribe({
+        next: (result) => {
+          this.colorAnalysisResult = result;
+          // 可以在这里更新颜色指标
+          if (result.colors.length > 0) {
+            const mainColor = result.colors[0];
+            this.updateColorIndicator('mainColor', mainColor.rgb);
+          }
+        },
+        error: (error) => {
+          console.error('颜色分析失败:', error);
+        }
+      });
+    }
+  }
+
+  onViewReport(): void {
+    if (this.colorAnalysisResult?.reportPath) {
+      // 在新窗口中打开报告
+      window.open(this.colorAnalysisResult.reportPath, '_blank');
+    }
+  }
 }

+ 250 - 0
src/app/shared/components/upload-success-modal/upload-success-modal.animations.ts

@@ -0,0 +1,250 @@
+import { trigger, state, style, transition, animate, keyframes, query, stagger } from '@angular/animations';
+
+export const modalAnimations = [
+  // 弹窗背景遮罩动画
+  trigger('backdropAnimation', [
+    transition(':enter', [
+      style({ opacity: 0 }),
+      animate('300ms ease-out', style({ opacity: 1 }))
+    ]),
+    transition(':leave', [
+      animate('200ms ease-in', style({ opacity: 0 }))
+    ])
+  ]),
+
+  // 弹窗容器动画
+  trigger('modalAnimation', [
+    transition(':enter', [
+      style({ 
+        opacity: 0, 
+        transform: 'scale(0.8) translateY(-20px)',
+        filter: 'blur(4px)'
+      }),
+      animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)', 
+        style({ 
+          opacity: 1, 
+          transform: 'scale(1) translateY(0)',
+          filter: 'blur(0px)'
+        })
+      )
+    ]),
+    transition(':leave', [
+      animate('250ms cubic-bezier(0.4, 0.0, 0.2, 1)', 
+        style({ 
+          opacity: 0, 
+          transform: 'scale(0.9) translateY(-10px)',
+          filter: 'blur(2px)'
+        })
+      )
+    ])
+  ]),
+
+  // 成功图标动画
+  trigger('successIconAnimation', [
+    transition(':enter', [
+      style({ 
+        opacity: 0, 
+        transform: 'scale(0) rotate(-180deg)' 
+      }),
+      animate('600ms 200ms cubic-bezier(0.68, -0.55, 0.265, 1.55)', 
+        style({ 
+          opacity: 1, 
+          transform: 'scale(1) rotate(0deg)' 
+        })
+      )
+    ])
+  ]),
+
+  // 文件列表动画
+  trigger('fileListAnimation', [
+    transition('* => *', [
+      query(':enter', [
+        style({ 
+          opacity: 0, 
+          transform: 'translateX(-20px)' 
+        }),
+        stagger(100, [
+          animate('300ms ease-out', 
+            style({ 
+              opacity: 1, 
+              transform: 'translateX(0)' 
+            })
+          )
+        ])
+      ], { optional: true })
+    ])
+  ]),
+
+  // 颜色分析按钮动画
+  trigger('analyzeButtonAnimation', [
+    state('idle', style({ transform: 'scale(1)' })),
+    state('loading', style({ transform: 'scale(0.95)' })),
+    transition('idle => loading', [
+      animate('150ms ease-in')
+    ]),
+    transition('loading => idle', [
+      animate('200ms ease-out')
+    ]),
+    transition(':enter', [
+      style({ 
+        opacity: 0, 
+        transform: 'translateY(20px)' 
+      }),
+      animate('400ms 300ms ease-out', 
+        style({ 
+          opacity: 1, 
+          transform: 'translateY(0)' 
+        })
+      )
+    ])
+  ]),
+
+  // 颜色分析结果动画
+  trigger('analysisResultAnimation', [
+    transition(':enter', [
+      style({ 
+        opacity: 0, 
+        transform: 'translateY(30px)',
+        height: 0 
+      }),
+      animate('500ms cubic-bezier(0.25, 0.8, 0.25, 1)', 
+        style({ 
+          opacity: 1, 
+          transform: 'translateY(0)',
+          height: '*' 
+        })
+      )
+    ]),
+    transition(':leave', [
+      animate('300ms ease-in', 
+        style({ 
+          opacity: 0, 
+          transform: 'translateY(-20px)',
+          height: 0 
+        })
+      )
+    ])
+  ]),
+
+  // 颜色色块动画
+  trigger('colorSwatchAnimation', [
+    transition('* => *', [
+      query(':enter', [
+        style({ 
+          opacity: 0, 
+          transform: 'scale(0) rotate(45deg)' 
+        }),
+        stagger(50, [
+          animate('300ms cubic-bezier(0.68, -0.55, 0.265, 1.55)', 
+            style({ 
+              opacity: 1, 
+              transform: 'scale(1) rotate(0deg)' 
+            })
+          )
+        ])
+      ], { optional: true })
+    ])
+  ]),
+
+  // 加载动画
+  trigger('loadingAnimation', [
+    transition(':enter', [
+      style({ 
+        opacity: 0, 
+        transform: 'scale(0.8)' 
+      }),
+      animate('300ms ease-out', 
+        style({ 
+          opacity: 1, 
+          transform: 'scale(1)' 
+        })
+      )
+    ]),
+    transition(':leave', [
+      animate('200ms ease-in', 
+        style({ 
+          opacity: 0, 
+          transform: 'scale(0.8)' 
+        })
+      )
+    ])
+  ]),
+
+  // 错误提示动画
+  trigger('errorAnimation', [
+    transition(':enter', [
+      animate('400ms ease-out', keyframes([
+        style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }),
+        style({ opacity: 1, transform: 'translateX(10px)', offset: 0.6 }),
+        style({ opacity: 1, transform: 'translateX(0)', offset: 1 })
+      ]))
+    ]),
+    transition(':leave', [
+      animate('300ms ease-in', 
+        style({ 
+          opacity: 0, 
+          transform: 'translateX(100%)' 
+        })
+      )
+    ])
+  ]),
+
+  // 按钮悬停动画
+  trigger('buttonHoverAnimation', [
+    state('normal', style({ transform: 'translateY(0)' })),
+    state('hover', style({ transform: 'translateY(-2px)' })),
+    transition('normal <=> hover', [
+      animate('200ms ease-out')
+    ])
+  ]),
+
+  // 进度条动画
+  trigger('progressAnimation', [
+    transition('* => *', [
+      style({ width: '0%' }),
+      animate('{{ duration }}ms ease-out', style({ width: '{{ width }}%' }))
+    ], { params: { duration: 1000, width: 100 } })
+  ]),
+
+  // 淡入淡出动画
+  trigger('fadeInOut', [
+    transition(':enter', [
+      style({ opacity: 0 }),
+      animate('300ms ease-in', style({ opacity: 1 }))
+    ]),
+    transition(':leave', [
+      animate('200ms ease-out', style({ opacity: 0 }))
+    ])
+  ]),
+
+  // 滑动动画
+  trigger('slideInOut', [
+    transition(':enter', [
+      style({ transform: 'translateY(100%)', opacity: 0 }),
+      animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)', 
+        style({ transform: 'translateY(0)', opacity: 1 })
+      )
+    ]),
+    transition(':leave', [
+      animate('300ms cubic-bezier(0.4, 0.0, 0.2, 1)', 
+        style({ transform: 'translateY(100%)', opacity: 0 })
+      )
+    ])
+  ])
+];
+
+// 动画配置常量
+export const ANIMATION_TIMINGS = {
+  fast: '200ms',
+  normal: '300ms',
+  slow: '500ms',
+  verySlow: '800ms'
+};
+
+export const ANIMATION_EASINGS = {
+  easeIn: 'ease-in',
+  easeOut: 'ease-out',
+  easeInOut: 'ease-in-out',
+  bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
+  smooth: 'cubic-bezier(0.25, 0.8, 0.25, 1)'
+};

+ 190 - 0
src/app/shared/components/upload-success-modal/upload-success-modal.component.html

@@ -0,0 +1,190 @@
+<!-- 上传成功弹窗 -->
+@if (isVisible) {
+  <div class="modal-backdrop" 
+       [@backdropAnimation]
+       (click)="onBackdropClick($event)">
+    
+    <div class="modal-container" 
+         [@modalAnimation]
+         [class.mobile]="isMobile"
+         [class.tablet]="isTablet"
+         (click)="$event.stopPropagation()">
+      
+      <!-- 弹窗头部 -->
+      <div class="modal-header" [@fadeInOut]>
+        <div class="header-content">
+          <div class="success-icon" [@successIconAnimation]>
+            <svg width="32" height="32" viewBox="0 0 24 24" fill="none">
+              <circle cx="12" cy="12" r="10" fill="#10B981"/>
+              <path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </div>
+          <div class="header-text">
+            <h2>上传成功!</h2>
+            <p>已成功上传 {{ uploadedFiles.length }} 个文件</p>
+          </div>
+        </div>
+        
+        <button class="close-button" 
+                (click)="onClose()"
+                [@buttonHoverAnimation]="buttonHoverState"
+                (mouseenter)="buttonHoverState = 'hover'"
+                (mouseleave)="buttonHoverState = 'normal'"
+                aria-label="关闭弹窗">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
+            <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+          </svg>
+        </button>
+      </div>
+
+      <!-- 弹窗内容 -->
+      <div class="modal-content">
+        <!-- 已上传文件列表 -->
+        <div class="uploaded-files" [@fileListAnimation]="uploadedFiles.length">
+          <h3>已上传文件</h3>
+          <div class="file-list">
+            @for (file of uploadedFiles; track file.id) {
+              <div class="file-item" [@slideInOut]>
+                <div class="file-icon">
+                  @if (file.type?.startsWith('image/')) {
+                    <img [src]="file.preview || '/assets/images/file-image.svg'" 
+                         [alt]="file.name"
+                         class="file-preview">
+                  } @else {
+                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
+                      <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" 
+                            fill="#6B7280"/>
+                      <polyline points="14,2 14,8 20,8" fill="#9CA3AF"/>
+                    </svg>
+                  }
+                </div>
+                <div class="file-info">
+                  <span class="file-name">{{ file.name }}</span>
+                  <span class="file-size">{{ formatFileSize(file.size || 0) }}</span>
+                </div>
+                <div class="file-status success">
+                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
+                    <circle cx="12" cy="12" r="10" fill="#10B981"/>
+                    <path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round"/>
+                  </svg>
+                </div>
+              </div>
+            }
+          </div>
+        </div>
+
+        <!-- 颜色分析功能区域 -->
+        @if (shouldShowColorAnalysis()) {
+          <div class="color-analysis-section">
+            <div class="section-header">
+              <h4>智能颜色分析</h4>
+              <p>基于上传的参考图片,自动提取主要色彩和材质信息</p>
+            </div>
+
+            <!-- 分析按钮或结果 -->
+            @if (!analysisResult && !isAnalyzing) {
+              <div class="analysis-action">
+                <button class="analyze-btn" 
+                        (click)="startColorAnalysis()"
+                        [disabled]="isAnalyzing">
+                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <circle cx="12" cy="12" r="3"></circle>
+                    <path d="M12 1v6m0 6v6m11-7h-6m-6 0H1m15.5-6.5l-4.24 4.24M7.76 16.24l-4.24 4.24m0-8.48l4.24 4.24m8.48 0l4.24-4.24"></path>
+                  </svg>
+                  开始颜色分析
+                </button>
+              </div>
+            }
+
+            <!-- 分析进行中 -->
+            @if (isAnalyzing) {
+              <div class="analysis-loading">
+                <div class="loading-spinner"></div>
+                <div class="loading-text">
+                  <h5>正在分析图片颜色...</h5>
+                  <p>请稍候,系统正在提取主要色彩信息</p>
+                </div>
+              </div>
+            }
+
+            <!-- 分析结果 -->
+            @if (analysisResult && !isAnalyzing) {
+              <div class="analysis-result">
+                <div class="result-header">
+                  <h5>颜色分析结果</h5>
+                  <span class="result-count">提取到 {{ analysisResult.colors.length }} 种主要颜色</span>
+                </div>
+                
+                <div class="color-palette">
+                  @for (colorInfo of analysisResult.colors; track colorInfo.hex) {
+                    <div class="color-item">
+                      <div class="color-swatch" 
+                           [style.background-color]="colorInfo.hex"
+                           [title]="colorInfo.hex + ' (' + colorInfo.percentage + '%)'">
+                      </div>
+                      <div class="color-info">
+                        <div class="color-value">{{ colorInfo.hex }}</div>
+                        <div class="color-percentage">{{ colorInfo.percentage }}%</div>
+                      </div>
+                    </div>
+                  }
+                </div>
+
+                <div class="result-actions">
+                  <button class="view-report-btn" (click)="onViewReportClick()">
+                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
+                      <polyline points="14,2 14,8 20,8"></polyline>
+                    </svg>
+                    查看完整报告
+                  </button>
+                </div>
+              </div>
+            }
+
+            <!-- 分析错误 -->
+            @if (analysisError) {
+              <div class="analysis-error">
+                <div class="error-icon">
+                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <circle cx="12" cy="12" r="10"></circle>
+                    <line x1="15" y1="9" x2="9" y2="15"></line>
+                    <line x1="9" y1="9" x2="15" y2="15"></line>
+                  </svg>
+                </div>
+                <div class="error-text">
+                  <h5>分析失败</h5>
+                  <p>{{ analysisError }}</p>
+                </div>
+                <button class="retry-btn" (click)="startColorAnalysis()">
+                  重试
+                </button>
+              </div>
+            }
+          </div>
+        }
+      </div>
+
+      <!-- 弹窗底部 -->
+      <div class="modal-footer">
+        <div class="footer-actions">
+          <button class="secondary-btn" (click)="onClose()">
+            关闭
+          </button>
+          @if (shouldShowColorAnalysis() && !analysisResult) {
+            <button class="primary-btn" 
+                    (click)="startColorAnalysis()"
+                    [disabled]="isAnalyzing">
+              @if (isAnalyzing) {
+                <div class="btn-spinner"></div>
+                分析中...
+              } @else {
+                开始分析
+              }
+            </button>
+          }
+        </div>
+      </div>
+    </div>
+  </div>
+}

+ 1000 - 0
src/app/shared/components/upload-success-modal/upload-success-modal.component.scss

@@ -0,0 +1,1000 @@
+// 上传成功弹窗样式
+// @import '../../../styles/_variables';
+
+// 弹窗背景遮罩
+// 响应式断点
+$mobile-breakpoint: 768px;
+$tablet-breakpoint: 1024px;
+$desktop-breakpoint: 1200px;
+
+// 动画变量
+$animation-duration-fast: 0.2s;
+$animation-duration-normal: 0.3s;
+$animation-duration-slow: 0.5s;
+$animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
+
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(4px);
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 1rem;
+  
+  // 响应式调整
+  @media (max-width: $mobile-breakpoint) {
+    padding: 0.5rem;
+    align-items: flex-end;
+  }
+  
+  @media (min-width: $mobile-breakpoint) and (max-width: $tablet-breakpoint) {
+    padding: 1rem;
+  }
+}
+
+// 弹窗主容器
+.modal-container {
+  background: white;
+  border-radius: 16px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
+  max-width: 600px;
+  width: 100%;
+  max-height: 90vh;
+  overflow: hidden;
+  position: relative;
+  transform-origin: center;
+  
+  // 响应式调整
+  @media (max-width: $mobile-breakpoint) {
+    max-width: 100%;
+    border-radius: 16px 16px 0 0;
+    max-height: 85vh;
+    margin-top: auto;
+  }
+  
+  @media (min-width: $mobile-breakpoint) and (max-width: $tablet-breakpoint) {
+    max-width: 90%;
+    max-height: 85vh;
+  }
+  
+  @media (min-width: $tablet-breakpoint) {
+    max-width: 600px;
+  }
+  
+  // 深色模式适配
+  @media (prefers-color-scheme: dark) {
+    background: #1a1a1a;
+    color: #ffffff;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+  }
+}
+
+// 弹窗头部
+.modal-header {
+  padding: 32px 32px 24px 32px; // 增加左右内边距,确保对称
+  border-bottom: 1px solid #f0f0f0;
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  
+  // 响应式调整
+  @media (max-width: $mobile-breakpoint) {
+    padding: 24px 20px 20px 20px;
+  }
+  
+  @media (min-width: $mobile-breakpoint) and (max-width: $tablet-breakpoint) {
+    padding: 28px 28px 22px 28px;
+  }
+  
+  .header-content {
+    display: flex;
+    align-items: flex-start;
+    gap: 16px;
+    flex: 1;
+    min-width: 0; // 防止内容溢出
+    
+    .success-icon {
+      width: 48px;
+      height: 48px;
+      background: linear-gradient(135deg, #34C759, #30D158);
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      
+      svg {
+        color: white;
+        width: 24px;
+        height: 24px;
+      }
+    }
+    
+    .header-text {
+      flex: 1;
+      min-width: 0; // 防止文本溢出
+      
+      h2 {
+        margin: 0 0 8px 0;
+        font-size: 20px;
+        font-weight: 600;
+        color: #1d1d1f;
+        line-height: 1.2;
+        
+        // 响应式字体大小
+        @media (max-width: $mobile-breakpoint) {
+          font-size: 18px;
+        }
+      }
+      
+      p {
+        margin: 0;
+        font-size: 14px;
+        color: #86868b;
+        line-height: 1.4;
+        
+        @media (max-width: $mobile-breakpoint) {
+          font-size: 13px;
+        }
+      }
+    }
+  }
+  
+  .close-button {
+    width: 36px;
+    height: 36px;
+    border: none;
+    background: #f2f2f7;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    flex-shrink: 0;
+    margin-left: 16px; // 确保与内容区域有适当间距
+    
+    // 响应式调整
+    @media (max-width: $mobile-breakpoint) {
+      width: 32px;
+      height: 32px;
+      margin-left: 12px;
+    }
+    
+    &:hover {
+      background: #e5e5ea;
+      transform: scale(1.05);
+    }
+    
+    &:active {
+      transform: scale(0.95);
+    }
+    
+    svg {
+      color: #86868b;
+      width: 18px;
+      height: 18px;
+      
+      @media (max-width: $mobile-breakpoint) {
+        width: 16px;
+        height: 16px;
+      }
+    }
+  }
+}
+
+// 弹窗内容
+.modal-content {
+  padding: 0 32px 32px 32px; // 与头部保持一致的左右内边距
+  overflow-y: auto;
+  flex: 1;
+  
+  // 响应式调整
+  @media (max-width: $mobile-breakpoint) {
+    padding: 0 20px 24px 20px;
+  }
+  
+  @media (min-width: $mobile-breakpoint) and (max-width: $tablet-breakpoint) {
+    padding: 0 28px 28px 28px;
+  }
+}
+
+// 上传文件列表
+.uploaded-files {
+  margin-bottom: 32px;
+  
+  h3 {
+    margin: 0 0 20px 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d1d1f;
+    
+    // 响应式字体大小
+    @media (max-width: $mobile-breakpoint) {
+      font-size: 16px;
+      margin-bottom: 16px;
+    }
+  }
+  
+  .file-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+  
+  .file-item {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 16px;
+    background: #f9f9f9;
+    border-radius: 12px;
+    border: 1px solid #e5e5ea;
+    transition: all 0.2s ease;
+    
+    // 响应式调整
+    @media (max-width: $mobile-breakpoint) {
+      padding: 12px;
+      gap: 12px;
+    }
+    
+    &:hover {
+      background: #f5f5f7;
+      border-color: #d1d1d6;
+    }
+    
+    .file-icon {
+      width: 48px;
+      height: 48px;
+      background: #007aff;
+      border-radius: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      
+      // 响应式调整
+      @media (max-width: $mobile-breakpoint) {
+        width: 40px;
+        height: 40px;
+        border-radius: 8px;
+      }
+      
+      svg {
+        color: white;
+        width: 24px;
+        height: 24px;
+        
+        @media (max-width: $mobile-breakpoint) {
+          width: 20px;
+          height: 20px;
+        }
+      }
+    }
+    
+    .file-preview {
+      width: 48px;
+      height: 48px;
+      border-radius: 10px;
+      overflow: hidden;
+      flex-shrink: 0;
+      
+      @media (max-width: $mobile-breakpoint) {
+        width: 40px;
+        height: 40px;
+        border-radius: 8px;
+      }
+      
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+    
+    .file-info {
+      flex: 1;
+      min-width: 0;
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+      
+      .file-name {
+        font-size: 15px;
+        font-weight: 500;
+        color: #1d1d1f;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        line-height: 1.3;
+        
+        @media (max-width: $mobile-breakpoint) {
+          font-size: 14px;
+        }
+      }
+      
+      .file-size {
+        font-size: 13px;
+        color: #86868b;
+        line-height: 1.2;
+        
+        @media (max-width: $mobile-breakpoint) {
+          font-size: 12px;
+        }
+      }
+    }
+    
+    .file-status {
+      width: 24px;
+      height: 24px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      
+      &.success {
+        svg {
+          width: 20px;
+          height: 20px;
+          
+          @media (max-width: $mobile-breakpoint) {
+            width: 18px;
+            height: 18px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 颜色分析区域
+.color-analysis-section {
+  border-top: 1px solid #f0f0f0;
+  padding-top: 24px;
+  
+  .section-header {
+    margin-bottom: 24px;
+    text-align: center;
+    
+    h4 {
+      margin: 0 0 8px 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #1d1d1f;
+      
+      @media (max-width: $mobile-breakpoint) {
+        font-size: 16px;
+      }
+    }
+    
+    p {
+      margin: 0;
+      font-size: 14px;
+      color: #86868b;
+      line-height: 1.5;
+      max-width: 400px;
+      margin: 0 auto;
+      
+      @media (max-width: $mobile-breakpoint) {
+        font-size: 13px;
+      }
+    }
+  }
+  
+  // 分析按钮
+  .analysis-action {
+    text-align: center;
+    padding: 20px 0 32px 0;
+    
+    .analyze-btn {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      gap: 10px;
+      padding: 14px 28px;
+      background: linear-gradient(135deg, #007aff, #0056cc);
+      color: white;
+      border: none;
+      border-radius: 12px;
+      font-size: 15px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.3s ease;
+      min-width: 160px;
+      
+      @media (max-width: $mobile-breakpoint) {
+        padding: 12px 24px;
+        font-size: 14px;
+        min-width: 140px;
+      }
+      
+      &:hover:not(:disabled) {
+        transform: translateY(-2px);
+        box-shadow: 0 12px 24px rgba(0, 122, 255, 0.25);
+        background: linear-gradient(135deg, #0056cc, #003d99);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+      
+      &:disabled {
+        opacity: 0.6;
+        cursor: not-allowed;
+        transform: none;
+        box-shadow: none;
+      }
+      
+      svg {
+        width: 20px;
+        height: 20px;
+        
+        @media (max-width: $mobile-breakpoint) {
+          width: 18px;
+          height: 18px;
+        }
+      }
+    }
+  }
+  
+  // 分析加载状态
+  .analysis-loading {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 24px;
+    background: #f9f9f9;
+    border-radius: 12px;
+    
+    .loading-spinner {
+      width: 32px;
+      height: 32px;
+      border: 3px solid #e5e5ea;
+      border-top: 3px solid #007aff;
+      border-radius: 50%;
+      animation: spin 1s linear infinite;
+      flex-shrink: 0;
+    }
+    
+    .loading-text {
+      h5 {
+        margin: 0 0 4px 0;
+        font-size: 14px;
+        font-weight: 600;
+        color: #1d1d1f;
+      }
+      
+      p {
+        margin: 0;
+        font-size: 12px;
+        color: #86868b;
+      }
+    }
+  }
+  
+  // 分析结果
+  .analysis-result {
+    .result-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 20px;
+      
+      h5 {
+        margin: 0;
+        font-size: 16px;
+        font-weight: 600;
+        color: #1d1d1f;
+        
+        @media (max-width: $mobile-breakpoint) {
+          font-size: 15px;
+        }
+      }
+      
+      .result-badge {
+        background: #34c759;
+        color: white;
+        font-size: 11px;
+        font-weight: 600;
+        padding: 4px 10px;
+        border-radius: 12px;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+      }
+    }
+    
+    .color-palette {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+      gap: 16px;
+      margin-bottom: 24px;
+      
+      @media (max-width: $mobile-breakpoint) {
+        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+        gap: 12px;
+      }
+      
+      .color-item {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 12px;
+        background: white;
+        border: 1px solid #e5e5ea;
+        border-radius: 12px;
+        transition: all 0.2s ease;
+        
+        @media (max-width: $mobile-breakpoint) {
+          padding: 10px;
+          gap: 10px;
+        }
+        
+        &:hover {
+          border-color: #007aff;
+          box-shadow: 0 2px 8px rgba(0, 122, 255, 0.1);
+        }
+        
+        .color-swatch {
+          width: 32px;
+          height: 32px;
+          border-radius: 8px;
+          border: 2px solid rgba(255, 255, 255, 0.8);
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+          flex-shrink: 0;
+          
+          @media (max-width: $mobile-breakpoint) {
+            width: 28px;
+            height: 28px;
+            border-radius: 6px;
+          }
+        }
+        
+        .color-info {
+          flex: 1;
+          min-width: 0;
+          
+          .color-value {
+            font-size: 12px;
+            font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+            color: #1d1d1f;
+            margin-bottom: 4px;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            font-weight: 500;
+            
+            @media (max-width: $mobile-breakpoint) {
+              font-size: 11px;
+            }
+          }
+          
+          .color-percentage {
+            font-size: 11px;
+            color: #86868b;
+            font-weight: 600;
+            
+            @media (max-width: $mobile-breakpoint) {
+              font-size: 10px;
+            }
+          }
+        }
+      }
+    }
+    
+    .result-actions {
+      text-align: center;
+      padding-top: 8px;
+      
+      .view-report-btn {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+        padding: 12px 20px;
+        background: #f2f2f7;
+        color: #007aff;
+        border: none;
+        border-radius: 10px;
+        font-size: 14px;
+        font-weight: 600;
+        cursor: pointer;
+        transition: all 0.2s ease;
+        min-width: 140px;
+        
+        @media (max-width: $mobile-breakpoint) {
+          padding: 10px 16px;
+          font-size: 13px;
+          min-width: 120px;
+        }
+        
+        &:hover {
+          background: #e5e5ea;
+          transform: translateY(-1px);
+        }
+        
+        &:active {
+          transform: translateY(0);
+        }
+        
+        svg {
+          width: 18px;
+          height: 18px;
+          
+          @media (max-width: $mobile-breakpoint) {
+            width: 16px;
+            height: 16px;
+          }
+        }
+      }
+    }
+  }
+  
+  // 分析错误
+  .analysis-error {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 16px;
+    background: #fff2f2;
+    border: 1px solid #fecaca;
+    border-radius: 8px;
+    
+    .error-icon {
+      width: 32px;
+      height: 32px;
+      background: #ef4444;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      
+      svg {
+        color: white;
+        width: 16px;
+        height: 16px;
+      }
+    }
+    
+    .error-text {
+      flex: 1;
+      
+      h5 {
+        margin: 0 0 4px 0;
+        font-size: 14px;
+        font-weight: 600;
+        color: #dc2626;
+      }
+      
+      p {
+        margin: 0;
+        font-size: 12px;
+        color: #991b1b;
+      }
+    }
+    
+    .retry-btn {
+      padding: 6px 12px;
+      background: #ef4444;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      font-size: 12px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        background: #dc2626;
+      }
+    }
+  }
+}
+
+// 弹窗底部操作区域
+.modal-footer {
+  padding: 24px 32px 32px;
+  border-top: 1px solid #e5e5ea;
+  background: #fafafa;
+  border-radius: 0 0 20px 20px;
+  
+  @media (max-width: $tablet-breakpoint) {
+    padding: 20px 24px 24px;
+  }
+  
+  @media (max-width: $mobile-breakpoint) {
+    padding: 16px 20px 20px;
+  }
+  
+  .footer-actions {
+    display: flex;
+    gap: 16px;
+    justify-content: flex-end;
+    align-items: center;
+    
+    @media (max-width: $mobile-breakpoint) {
+      flex-direction: column-reverse;
+      gap: 12px;
+    }
+    
+    .secondary-btn,
+    .primary-btn {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      padding: 12px 24px;
+      border-radius: 10px;
+      font-size: 14px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      border: none;
+      min-width: 120px;
+      
+      @media (max-width: $mobile-breakpoint) {
+        width: 100%;
+        padding: 14px 24px;
+      }
+      
+      svg {
+        width: 18px;
+        height: 18px;
+      }
+    }
+    
+    .secondary-btn {
+      background: #f2f2f7;
+      color: #1d1d1f;
+      
+      &:hover {
+        background: #e5e5ea;
+        transform: translateY(-1px);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+    }
+    
+    .primary-btn {
+      background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
+      color: white;
+      box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
+      
+      &:hover {
+        background: linear-gradient(135deg, #0056cc 0%, #003d99 100%);
+        box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
+        transform: translateY(-1px);
+      }
+      
+      &:active {
+        transform: translateY(0);
+        box-shadow: 0 2px 6px rgba(0, 122, 255, 0.3);
+      }
+      
+      &:disabled {
+        background: #c7c7cc;
+        color: #8e8e93;
+        cursor: not-allowed;
+        box-shadow: none;
+        transform: none;
+      }
+      
+      .btn-spinner {
+        width: 14px;
+        height: 14px;
+        border: 2px solid rgba(255, 255, 255, 0.3);
+        border-top: 2px solid white;
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+      }
+    }
+  }
+}
+
+// 动画
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .modal-backdrop {
+    padding: 10px;
+  }
+  
+  .modal-header {
+    padding: 20px 16px 12px;
+    
+    .header-content {
+      gap: 12px;
+      
+      .success-icon {
+        width: 40px;
+        height: 40px;
+        
+        svg {
+          width: 20px;
+          height: 20px;
+        }
+      }
+      
+      .header-text {
+        h3 {
+          font-size: 18px;
+        }
+        
+        p {
+          font-size: 13px;
+        }
+      }
+    }
+  }
+  
+  .modal-body {
+    padding: 0 16px;
+  }
+  
+  .modal-footer {
+    padding: 12px 16px 20px;
+    
+    .footer-actions {
+      flex-direction: column-reverse;
+      
+      .secondary-btn,
+      .primary-btn {
+        width: 100%;
+        justify-content: center;
+      }
+    }
+  }
+  
+  .color-analysis-section {
+    .color-palette {
+      grid-template-columns: 1fr;
+    }
+  }
+}
+
+// 深色模式支持
+@media (prefers-color-scheme: dark) {
+  .modal-container {
+    background: #1c1c1e;
+    
+    .modal-header {
+      border-bottom-color: #38383a;
+      
+      .header-text {
+        h3 {
+          color: #f2f2f7;
+        }
+        
+        p {
+          color: #8e8e93;
+        }
+      }
+      
+      .close-btn {
+        background: #2c2c2e;
+        
+        &:hover {
+          background: #3a3a3c;
+        }
+        
+        svg {
+          color: #8e8e93;
+        }
+      }
+    }
+    
+    .uploaded-files-section {
+      h4 {
+        color: #f2f2f7;
+      }
+      
+      .file-item {
+        background: #2c2c2e;
+        border-color: #48484a;
+        
+        .file-info {
+          .file-name {
+            color: #f2f2f7;
+          }
+          
+          .file-size {
+            color: #8e8e93;
+          }
+        }
+      }
+    }
+    
+    .color-analysis-section {
+      .section-header {
+        h4 {
+          color: #f2f2f7;
+        }
+        
+        p {
+          color: #8e8e93;
+        }
+      }
+      
+      .analysis-loading {
+        background: #2c2c2e;
+        
+        .loading-text {
+          h5 {
+            color: #f2f2f7;
+          }
+          
+          p {
+            color: #8e8e93;
+          }
+        }
+      }
+      
+      .analysis-result {
+        .result-header {
+          h5 {
+            color: #f2f2f7;
+          }
+          
+          .result-count {
+            background: #2c2c2e;
+            color: #8e8e93;
+          }
+        }
+        
+        .color-palette {
+          .color-item {
+            background: #2c2c2e;
+            border-color: #48484a;
+            
+            .color-info {
+              .color-value {
+                color: #f2f2f7;
+              }
+              
+              .color-percentage {
+                color: #8e8e93;
+              }
+            }
+          }
+        }
+        
+        .result-actions {
+          .view-report-btn {
+            background: #2c2c2e;
+            
+            &:hover {
+              background: #3a3a3c;
+            }
+          }
+        }
+      }
+    }
+    
+    .modal-footer {
+      border-top-color: #38383a;
+      
+      .footer-actions {
+        .secondary-btn {
+          background: #2c2c2e;
+          color: #f2f2f7;
+          
+          &:hover {
+            background: #3a3a3c;
+          }
+        }
+      }
+    }
+  }
+}

+ 192 - 0
src/app/shared/components/upload-success-modal/upload-success-modal.component.ts

@@ -0,0 +1,192 @@
+import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy, HostListener } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ColorAnalysisService, AnalysisProgress } from '../../services/color-analysis.service';
+import { Subscription } from 'rxjs';
+import { modalAnimations } from './upload-success-modal.animations';
+
+export interface UploadedFile {
+  id: string;
+  name: string;
+  url: string;
+  size?: number;
+  type?: 'image' | 'cad' | 'text';
+  preview?: string;
+}
+
+export interface ColorAnalysisResult {
+  colors: Array<{ hex: string; rgb: { r: number; g: number; b: number }; percentage: number }>;
+  reportUrl?: string;
+  mosaicUrl?: string;
+}
+
+@Component({
+  selector: 'app-upload-success-modal',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './upload-success-modal.component.html',
+  styleUrls: ['./upload-success-modal.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  animations: modalAnimations
+})
+export class UploadSuccessModalComponent implements OnInit, OnDestroy {
+  @Input() isVisible: boolean = false;
+  @Input() uploadedFiles: UploadedFile[] = [];
+  @Input() uploadType: 'image' | 'document' | 'mixed' = 'image';
+  @Input() analysisResult?: ColorAnalysisResult;
+
+  @Output() closeModal = new EventEmitter<void>();
+  @Output() analyzeColors = new EventEmitter<UploadedFile[]>();
+  @Output() viewReport = new EventEmitter<ColorAnalysisResult>();
+
+  // 颜色分析状态
+  isAnalyzing = false;
+  analysisProgress: AnalysisProgress | null = null;
+  analysisError: string | null = null;
+  
+  // 响应式状态
+  isMobile = false;
+  isTablet = false;
+  
+  // 动画状态
+  animationState = 'idle';
+  buttonHoverState = 'normal';
+
+  private progressSubscription?: Subscription;
+  private resizeSubscription?: Subscription;
+
+  constructor(private colorAnalysisService: ColorAnalysisService) {}
+
+  ngOnInit() {
+    this.checkScreenSize();
+    this.setupResizeListener();
+  }
+
+  ngOnDestroy() {
+    this.progressSubscription?.unsubscribe();
+    this.resizeSubscription?.unsubscribe();
+  }
+
+  // 响应式布局检测
+  @HostListener('window:resize', ['$event'])
+  onResize(event: any) {
+    this.checkScreenSize();
+  }
+
+  @HostListener('document:keydown', ['$event'])
+  onKeyDown(event: KeyboardEvent) {
+    if (event.key === 'Escape' && this.isVisible) {
+      this.onClose();
+    }
+  }
+
+  // 开始颜色分析
+  async startColorAnalysis() {
+    if (this.uploadedFiles.length === 0 || this.uploadType !== 'image') {
+      return;
+    }
+
+    this.isAnalyzing = true;
+    this.analysisError = null;
+
+    try {
+      // 发射分析事件给父组件处理
+      this.analyzeColors.emit(this.uploadedFiles);
+      
+      // 模拟分析过程(实际应该由父组件处理并返回结果)
+      await this.simulateAnalysis();
+      
+    } catch (error) {
+      this.analysisError = '颜色分析失败,请重试';
+      console.error('Color analysis error:', error);
+    } finally {
+      this.isAnalyzing = false;
+    }
+  }
+
+  // 模拟分析过程
+  private async simulateAnalysis(): Promise<void> {
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        // 模拟分析结果
+        this.analysisResult = {
+          colors: [
+          { hex: '#8B4513', rgb: { r: 139, g: 69, b: 19 }, percentage: 35.2 },
+          { hex: '#A0522D', rgb: { r: 160, g: 82, b: 45 }, percentage: 27.1 },
+          { hex: '#D2B48C', rgb: { r: 210, g: 180, b: 140 }, percentage: 22.4 },
+          { hex: '#DEB887', rgb: { r: 222, g: 184, b: 135 }, percentage: 12.5 },
+          { hex: '#F5F5DC', rgb: { r: 245, g: 245, b: 220 }, percentage: 2.8 }
+        ],
+          reportUrl: '/assets/reports/color-analysis-report.html',
+          mosaicUrl: '/assets/reports/mosaic-image.png'
+        };
+        resolve();
+      }, 2000);
+    });
+  }
+
+  // 事件处理方法
+  onClose() {
+    this.closeModal.emit();
+  }
+
+  onBackdropClick(event: Event) {
+    // 点击背景遮罩关闭弹窗
+    this.onClose();
+  }
+
+  onAnalyzeColorsClick() {
+    if (this.isAnalyzing || this.uploadedFiles.length === 0) {
+      return;
+    }
+
+    this.animationState = 'loading';
+    this.analyzeColors.emit(this.uploadedFiles);
+  }
+
+  onViewReportClick() {
+    if (this.analysisResult) {
+      this.viewReport.emit(this.analysisResult);
+    }
+  }
+
+  // 工具方法
+  shouldShowColorAnalysis(): boolean {
+    return this.uploadType === 'image' || this.hasImageFiles();
+  }
+
+  formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 Bytes';
+    
+    const k = 1024;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+  }
+
+  getFileTypeIcon(file: UploadedFile): string {
+    if (file.type?.startsWith('image/')) {
+      return 'image';
+    } else if (file.type?.includes('pdf')) {
+      return 'pdf';
+    } else if (file.type?.includes('word') || file.type?.includes('doc')) {
+      return 'document';
+    } else {
+      return 'file';
+    }
+  }
+
+  hasImageFiles(): boolean {
+    return this.uploadedFiles.some(file => file.type?.startsWith('image/'));
+  }
+
+  private checkScreenSize() {
+    const width = window.innerWidth;
+    this.isMobile = width < 768;
+    this.isTablet = width >= 768 && width < 1024;
+  }
+
+  private setupResizeListener() {
+    // 可以添加更复杂的响应式逻辑
+  }
+}

+ 216 - 0
src/app/shared/services/color-analysis.service.ts

@@ -0,0 +1,216 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, BehaviorSubject, throwError } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+export interface UploadedFile {
+  id: string;
+  name: string;
+  url: string;
+  size?: number;
+  type?: string;
+  preview?: string;
+}
+
+export interface ColorAnalysisResult {
+  colors: Array<{
+    hex: string;
+    rgb: { r: number; g: number; b: number };
+    percentage: number;
+  }>;
+  originalImage: string;
+  mosaicImage: string;
+  reportPath: string;
+}
+
+export interface AnalysisProgress {
+  stage: 'preparing' | 'processing' | 'extracting' | 'generating' | 'completed' | 'error';
+  message: string;
+  progress: number;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ColorAnalysisService {
+  private readonly API_BASE = '/api/color-analysis';
+  private analysisProgress$ = new BehaviorSubject<AnalysisProgress>({
+    stage: 'preparing',
+    message: '准备分析...',
+    progress: 0
+  });
+
+  constructor(private http: HttpClient) {}
+
+  /**
+   * 获取分析进度
+   */
+  getAnalysisProgress(): Observable<AnalysisProgress> {
+    return this.analysisProgress$.asObservable();
+  }
+
+  /**
+   * 分析图片颜色
+   * @param imageFile 图片文件
+   * @param options 分析选项
+   */
+  analyzeImageColors(imageFile: File, options?: {
+    mosaicSize?: number;
+    maxColors?: number;
+  }): Observable<ColorAnalysisResult> {
+    const formData = new FormData();
+    formData.append('image', imageFile);
+    
+    if (options?.mosaicSize) {
+      formData.append('mosaicSize', options.mosaicSize.toString());
+    }
+    if (options?.maxColors) {
+      formData.append('maxColors', options.maxColors.toString());
+    }
+
+    return this.http.post<any>(`${this.API_BASE}/analyze`, formData).pipe(
+      map((response: any) => {
+        if (response && response.success) {
+          return this.parseAnalysisResult(response.data);
+        }
+        throw new Error('分析结果无效');
+      }),
+      catchError(error => {
+        console.error('颜色分析失败:', error);
+        return throwError(() => new Error('颜色分析服务暂时不可用'));
+      })
+    );
+  }
+
+  /**
+   * 分析上传的图片文件
+   * @param file 上传的文件信息
+   */
+  analyzeImage(file: UploadedFile): Observable<ColorAnalysisResult> {
+    return this.http.post<any>('/api/color-analysis/analyze', {
+      fileId: file.id,
+      fileName: file.name,
+      fileUrl: file.url
+    }).pipe(
+      map((response: any) => {
+        if (response && response.success) {
+          return response.data as ColorAnalysisResult;
+        }
+        throw new Error('分析结果无效');
+      }),
+      catchError(error => {
+        console.error('颜色分析失败:', error);
+        return throwError(() => new Error('颜色分析服务暂时不可用'));
+      })
+    );
+  }
+
+  /**
+   * 获取分析报告
+   * @param reportId 报告ID
+   */
+  getAnalysisReport(reportId: string): Observable<string> {
+    return this.http.get(`${this.API_BASE}/report/${reportId}`, {
+      responseType: 'text'
+    });
+  }
+
+  /**
+   * 批量分析多个图片
+   * @param imageFiles 图片文件数组
+   */
+  analyzeBatchImages(imageFiles: File[]): Observable<ColorAnalysisResult[]> {
+    this.updateProgress('preparing', '准备批量分析...', 0);
+
+    const formData = new FormData();
+    imageFiles.forEach((file, index) => {
+      formData.append(`images`, file);
+    });
+
+    return this.http.post<any>(`${this.API_BASE}/analyze-batch`, formData).pipe(
+      map(response => {
+        this.updateProgress('completed', '批量分析完成', 100);
+        return response.results.map((result: any) => this.parseAnalysisResult(result));
+      }),
+      catchError(error => {
+        this.updateProgress('error', '批量分析失败: ' + (error.message || '未知错误'), 0);
+        return throwError(() => error);
+      })
+    );
+  }
+
+  /**
+   * 检查color-get服务状态
+   */
+  checkServiceStatus(): Observable<boolean> {
+    return this.http.get<{ status: string }>(`${this.API_BASE}/status`).pipe(
+      map(response => response.status === 'ready'),
+      catchError(() => throwError(() => new Error('颜色分析服务不可用')))
+    );
+  }
+
+  /**
+   * 更新分析进度
+   */
+  private updateProgress(stage: AnalysisProgress['stage'], message: string, progress: number): void {
+    this.analysisProgress$.next({ stage, message, progress });
+  }
+
+  /**
+   * 解析分析结果
+   */
+  private parseAnalysisResult(data: any): ColorAnalysisResult {
+    return {
+      colors: data.colors || [],
+      originalImage: data.originalImage || '',
+      mosaicImage: data.mosaicImage || '',
+      reportPath: data.reportPath || ''
+    };
+  }
+
+  /**
+   * 模拟color-get分析过程(用于开发测试)
+   */
+  simulateAnalysis(imageFile: File): Observable<ColorAnalysisResult> {
+    return new Observable(observer => {
+      // 模拟分析步骤
+      const steps = [
+        { stage: 'preparing' as const, message: '准备分析图片...', progress: 10 },
+        { stage: 'processing' as const, message: '生成马赛克图片...', progress: 30 },
+        { stage: 'extracting' as const, message: '提取颜色信息...', progress: 60 },
+        { stage: 'generating' as const, message: '生成分析报告...', progress: 90 },
+      ];
+
+      let currentStep = 0;
+      const interval = setInterval(() => {
+        if (currentStep < steps.length) {
+          const step = steps[currentStep];
+          this.updateProgress(step.stage, step.message, step.progress);
+          currentStep++;
+        } else {
+          clearInterval(interval);
+          
+          // 模拟分析结果
+          const mockResult: ColorAnalysisResult = {
+            colors: [
+              { hex: '#FF6B6B', rgb: { r: 255, g: 107, b: 107 }, percentage: 25.5 },
+              { hex: '#4ECDC4', rgb: { r: 78, g: 205, b: 196 }, percentage: 18.3 },
+              { hex: '#45B7D1', rgb: { r: 69, g: 183, b: 209 }, percentage: 15.7 },
+              { hex: '#96CEB4', rgb: { r: 150, g: 206, b: 180 }, percentage: 12.1 },
+              { hex: '#FFEAA7', rgb: { r: 255, g: 234, b: 167 }, percentage: 10.8 },
+              { hex: '#DDA0DD', rgb: { r: 221, g: 160, b: 221 }, percentage: 8.9 },
+              { hex: '#98D8C8', rgb: { r: 152, g: 216, b: 200 }, percentage: 8.7 }
+            ],
+            originalImage: URL.createObjectURL(imageFile),
+            mosaicImage: URL.createObjectURL(imageFile), // 在实际应用中这里应该是处理后的图片
+            reportPath: '/mock-report.html'
+          };
+
+          this.updateProgress('completed', '分析完成', 100);
+          observer.next(mockResult);
+          observer.complete();
+        }
+      }, 800);
+    });
+  }
+}