Przeglądaj źródła

feat(upload-success-modal): 添加颜色描述功能及复制按钮

在上传成功模态框中新增颜色描述区域,显示主要色彩分析结果并提供复制功能。包含以下改进:
- 添加颜色描述生成逻辑,将十六进制颜色转换为中文描述
- 实现复制到剪贴板功能,支持现代API和降级方案
- 优化模态框高度和滚动条样式以适应新内容
- 添加深色模式适配和响应式设计
0235711 1 dzień temu
rodzic
commit
3bf64a53e9

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

@@ -139,6 +139,51 @@
                     查看完整报告
                   </button>
                 </div>
+
+                <!-- 颜色描述文字区域 -->
+                <div class="color-description-section">
+                  <div class="description-header">
+                    <h6>颜色描述文字</h6>
+                    <p>适用于即梦等AI工具的颜色描述</p>
+                  </div>
+                  
+                  <div class="description-content">
+                    <div class="description-text" 
+                         [class.has-content]="generateColorDescription()"
+                         #descriptionText>
+                      {{ generateColorDescription() || '暂无颜色分析结果' }}
+                    </div>
+                    
+                    @if (generateColorDescription()) {
+                      <button class="copy-description-btn" 
+                              (click)="copyColorDescription()"
+                              [class.copied]="copySuccess"
+                              title="复制颜色描述">
+                        @if (copySuccess) {
+                          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                            <polyline points="20,6 9,17 4,12"></polyline>
+                          </svg>
+                          已复制
+                        } @else {
+                          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
+                            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+                          </svg>
+                          复制
+                        }
+                      </button>
+                    }
+                  </div>
+                  
+                  <div class="description-tips">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <circle cx="12" cy="12" r="10"></circle>
+                      <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
+                      <line x1="12" y1="17" x2="12.01" y2="17"></line>
+                    </svg>
+                    <span>复制后可直接粘贴到即梦等AI工具中,用于生成对应颜色风格的室内设计</span>
+                  </div>
+                </div>
               </div>
             }
 

+ 171 - 3
src/app/shared/components/upload-success-modal/upload-success-modal.component.scss

@@ -45,26 +45,29 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
   box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
   max-width: 600px;
   width: 100%;
-  max-height: 90vh;
+  max-height: 80vh; // 减少最大高度,为滚动留出更多空间
   overflow: hidden;
   position: relative;
   transform-origin: center;
+  display: flex;
+  flex-direction: column;
   
   // 响应式调整
   @media (max-width: $mobile-breakpoint) {
     max-width: 100%;
     border-radius: 16px 16px 0 0;
-    max-height: 85vh;
+    max-height: 75vh; // 移动端进一步减少高度
     margin-top: auto;
   }
   
   @media (min-width: $mobile-breakpoint) and (max-width: $tablet-breakpoint) {
     max-width: 90%;
-    max-height: 85vh;
+    max-height: 78vh; // 平板端适中的高度
   }
   
   @media (min-width: $tablet-breakpoint) {
     max-width: 600px;
+    max-height: 80vh; // 桌面端保持80vh
   }
   
   // 深色模式适配
@@ -194,6 +197,36 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
   padding: 0 32px 32px 32px; // 与头部保持一致的左右内边距
   overflow-y: auto;
   flex: 1;
+  min-height: 0; // 确保flex子元素可以收缩
+  
+  // 自定义滚动条样式
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+    
+    &:hover {
+      background: rgba(0, 0, 0, 0.3);
+    }
+  }
+  
+  // 深色模式滚动条
+  @media (prefers-color-scheme: dark) {
+    &::-webkit-scrollbar-thumb {
+      background: rgba(255, 255, 255, 0.3);
+      
+      &:hover {
+        background: rgba(255, 255, 255, 0.4);
+      }
+    }
+  }
   
   // 响应式调整
   @media (max-width: $mobile-breakpoint) {
@@ -633,6 +666,105 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
         }
       }
     }
+    
+    // 颜色描述区域
+    .color-description {
+      margin-top: 20px;
+      padding-top: 20px;
+      border-top: 1px solid #e5e5ea;
+      
+      .description-header {
+        margin-bottom: 12px;
+        
+        h6 {
+          margin: 0;
+          font-size: 16px;
+          font-weight: 600;
+          color: #1d1d1f;
+        }
+      }
+      
+      .description-content {
+        background: #f8f9fa;
+        border: 1px solid #e5e5ea;
+        border-radius: 8px;
+        padding: 16px;
+        margin-bottom: 12px;
+        font-size: 14px;
+        line-height: 1.6;
+        color: #1d1d1f;
+        white-space: pre-line;
+        word-break: break-word;
+        max-height: 200px;
+        overflow-y: auto;
+        
+        &.empty {
+          color: #8e8e93;
+          font-style: italic;
+          text-align: center;
+          padding: 24px 16px;
+        }
+      }
+      
+      .description-actions {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 12px;
+        
+        @media (max-width: $mobile-breakpoint) {
+          flex-direction: column;
+          align-items: stretch;
+        }
+        
+        .copy-btn {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          gap: 6px;
+          padding: 8px 16px;
+          background: #ffffff;
+          color: #007aff;
+          border: 1px solid #007aff;
+          border-radius: 6px;
+          font-size: 13px;
+          font-weight: 500;
+          cursor: pointer;
+          transition: all 0.2s ease;
+          min-width: 80px;
+          
+          &:hover {
+            background: #f0f8ff;
+          }
+          
+          &:active {
+            transform: scale(0.98);
+          }
+          
+          &.copied {
+            background: #34c759;
+            color: #ffffff;
+            border-color: #34c759;
+          }
+          
+          svg {
+            width: 14px;
+            height: 14px;
+          }
+        }
+        
+        .description-tip {
+          font-size: 12px;
+          color: #8e8e93;
+          flex: 1;
+          
+          @media (max-width: $mobile-breakpoint) {
+            text-align: center;
+            margin-top: 8px;
+          }
+        }
+      }
+    }
   }
   
   // 分析错误
@@ -979,6 +1111,42 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
             }
           }
         }
+        
+        .color-description {
+          .description-header {
+            h6 {
+              color: #f2f2f7;
+            }
+          }
+          
+          .description-content {
+            background: #2c2c2e;
+            border-color: #48484a;
+            color: #f2f2f7;
+          }
+          
+          .description-actions {
+            .copy-btn {
+              background: #2c2c2e;
+              color: #f2f2f7;
+              border-color: #48484a;
+              
+              &:hover {
+                background: #3a3a3c;
+              }
+              
+              &.copied {
+                background: #30d158;
+                color: #ffffff;
+                border-color: #30d158;
+              }
+            }
+          }
+          
+          .description-tip {
+            color: #8e8e93;
+          }
+        }
       }
     }
     

+ 283 - 1
src/app/shared/components/upload-success-modal/upload-success-modal.component.ts

@@ -13,8 +13,15 @@ export interface UploadedFile {
   preview?: string;
 }
 
+export interface ColorInfo {
+  hex: string;
+  rgb: { r: number; g: number; b: number };
+  percentage: number;
+  name?: string; // 颜色名称,如"深蓝色"、"暖白色"等
+}
+
 export interface ColorAnalysisResult {
-  colors: Array<{ hex: string; rgb: { r: number; g: number; b: number }; percentage: number }>;
+  colors: ColorInfo[];
   reportUrl?: string;
   mosaicUrl?: string;
 }
@@ -50,6 +57,7 @@ export class UploadSuccessModalComponent implements OnInit, OnDestroy {
   // 动画状态
   animationState = 'idle';
   buttonHoverState = 'normal';
+  copySuccess = false; // 复制成功状态
 
   private progressSubscription?: Subscription;
   private resizeSubscription?: Subscription;
@@ -180,6 +188,280 @@ export class UploadSuccessModalComponent implements OnInit, OnDestroy {
     return this.uploadedFiles.some(file => file.type?.startsWith('image/'));
   }
 
+  // 生成颜色描述文字
+  generateColorDescription(): string {
+    if (!this.analysisResult || !this.analysisResult.colors.length) {
+      return '';
+    }
+
+    const colorDescriptions = this.analysisResult.colors.map(color => {
+      const colorName = color.name || this.getColorName(color.hex);
+      return `${colorName}(${color.hex}) ${color.percentage}%`;
+    });
+
+    return `主要色彩:${colorDescriptions.join('、')}`;
+  }
+
+  // 根据色值获取颜色名称
+  private getColorName(hex: string): string {
+    const colorMap: { [key: string]: string } = {
+      '#FFFFFF': '纯白色',
+      '#F5F5F5': '白烟',
+      '#E5E5E5': '浅灰色',
+      '#CCCCCC': '中灰色',
+      '#999999': '深灰色',
+      '#666666': '暗灰色',
+      '#333333': '深暗灰',
+      '#000000': '纯黑色',
+      '#FF0000': '红色',
+      '#00FF00': '酸橙色',
+      '#0000FF': '蓝色',
+      '#FFFF00': '黄色',
+      '#FF00FF': '品红色',
+      '#00FFFF': '青色',
+      '#FFA500': '橙色',
+      '#800080': '紫色',
+      '#008000': '绿色',
+      '#000080': '海军蓝',
+      '#800000': '栗色',
+      '#808000': '橄榄色',
+      '#008080': '水鸭色',
+      '#C0C0C0': '银色',
+      '#808080': '灰色',
+      '#FFE4E1': '雾玫瑰',
+      '#F0F8FF': '爱丽丝蓝',
+      '#FAEBD7': '古董白',
+      '#F5F5DC': '米色',
+      '#DEB887': '硬木色',
+      '#A52A2A': '棕色',
+      '#D2691E': '巧克力色',
+      '#FF7F50': '珊瑚色',
+      '#6495ED': '矢车菊蓝',
+      '#DC143C': '深红色',
+      '#00008B': '深蓝色',
+      '#B8860B': '深金色',
+      '#A9A9A9': '深灰色',
+      '#006400': '深绿色',
+      '#BDB76B': '深卡其色',
+      '#8B008B': '深品红',
+      '#556B2F': '深橄榄绿',
+      '#FF8C00': '深橙色',
+      '#9932CC': '深兰花紫',
+      '#8B0000': '深红色2',
+      '#E9967A': '深鲑鱼色',
+      '#8FBC8F': '深海绿',
+      '#483D8B': '深石板蓝',
+      '#2F4F4F': '深石板灰',
+      '#00CED1': '深绿松石',
+      '#9400D3': '深紫罗兰',
+      '#FF1493': '深粉红',
+      '#00BFFF': '深天蓝',
+      '#696969': '暗灰色2',
+      '#1E90FF': '道奇蓝',
+      '#B22222': '火砖色',
+      '#FFFAF0': '花白色',
+      '#228B22': '森林绿',
+      '#DCDCDC': '淡灰色',
+      '#F8F8FF': '幽灵白',
+      '#FFD700': '金色',
+      '#DAA520': '金麒麟色',
+      '#ADFF2F': '绿黄色',
+      '#F0FFF0': '蜜瓜色',
+      '#FF69B4': '热粉红',
+      '#CD5C5C': '印度红',
+      '#4B0082': '靛青色',
+      '#FFFFF0': '象牙色',
+      '#F0E68C': '卡其色',
+      '#E6E6FA': '薰衣草色',
+      '#FFF0F5': '薰衣草红',
+      '#7CFC00': '草坪绿',
+      '#FFFACD': '柠檬绸',
+      '#ADD8E6': '浅蓝色',
+      '#F08080': '浅珊瑚色',
+      '#E0FFFF': '浅青色',
+      '#FAFAD2': '浅金菊黄',
+      '#D3D3D3': '浅灰色2',
+      '#90EE90': '浅绿色',
+      '#FFB6C1': '浅粉红',
+      '#FFA07A': '浅鲑鱼色',
+      '#20B2AA': '浅海绿',
+      '#87CEFA': '浅天蓝',
+      '#778899': '浅石板灰',
+      '#B0C4DE': '浅钢蓝',
+      '#FFFFE0': '浅黄色',
+      '#32CD32': '酸橙绿',
+      '#FAF0E6': '亚麻色',
+      '#66CDAA': '中海绿',
+      '#0000CD': '中蓝色',
+      '#BA55D3': '中兰花紫',
+      '#9370DB': '中紫色',
+      '#3CB371': '中海春绿',
+      '#7B68EE': '中石板蓝',
+      '#00FA9A': '中春绿',
+      '#48D1CC': '中绿松石',
+      '#C71585': '中紫罗兰红',
+      '#191970': '午夜蓝',
+      '#F5FFFA': '薄荷奶油',
+      '#FFDEAD': '那瓦霍白',
+      '#FDF5E6': '老花边',
+      '#6B8E23': '橄榄褐色',
+      '#FF4500': '橙红色',
+      '#DA70D6': '兰花紫',
+      '#EEE8AA': '灰秋麒麟',
+      '#98FB98': '灰绿色',
+      '#AFEEEE': '灰绿松石',
+      '#DB7093': '灰紫罗兰红',
+      '#FFEFD5': '番木瓜鞭',
+      '#FFDAB9': '桃扑',
+      '#CD853F': '秘鲁色',
+      '#FFC0CB': '粉红色',
+      '#DDA0DD': '洋李色',
+      '#B0E0E6': '粉蓝色',
+      '#BC8F8F': '玫瑰棕色',
+      '#4169E1': '皇家蓝',
+      '#8B4513': '马鞍棕色',
+      '#FA8072': '鲑鱼色',
+      '#F4A460': '沙棕色',
+      '#2E8B57': '海绿色',
+      '#FFF5EE': '海贝色',
+      '#A0522D': '赭色',
+      '#87CEEB': '天蓝色',
+      '#6A5ACD': '石板蓝',
+      '#708090': '石板灰',
+      '#FFFAFA': '雪色',
+      '#00FF7F': '春绿色',
+      '#4682B4': '钢蓝色',
+      '#D2B48C': '棕褐色',
+      '#D8BFD8': '蓟色',
+      '#FF6347': '番茄色',
+      '#40E0D0': '绿松石',
+      '#EE82EE': '紫罗兰',
+      '#F5DEB3': '小麦色',
+      '#9ACD32': '黄绿色'
+    };
+
+    // 如果找到精确匹配,返回对应名称
+    if (colorMap[hex.toUpperCase()]) {
+      return colorMap[hex.toUpperCase()];
+    }
+
+    // 否则根据RGB值判断颜色类型
+    const rgb = this.hexToRgb(hex);
+    if (!rgb) return '未知颜色';
+
+    const { r, g, b } = rgb;
+    const brightness = (r * 299 + g * 587 + b * 114) / 1000;
+
+    // 判断是否为灰色系
+    const isGray = Math.abs(r - g) < 30 && Math.abs(g - b) < 30 && Math.abs(r - b) < 30;
+    if (isGray) {
+      if (brightness > 240) return '浅灰白';
+      if (brightness > 200) return '浅灰色';
+      if (brightness > 160) return '中灰色';
+      if (brightness > 120) return '深灰色';
+      if (brightness > 80) return '暗灰色';
+      return '深暗灰';
+    }
+
+    // 判断主要颜色倾向
+    const max = Math.max(r, g, b);
+    const min = Math.min(r, g, b);
+    const saturation = max === 0 ? 0 : (max - min) / max;
+
+    if (saturation < 0.2) {
+      // 低饱和度,偏向灰色
+      if (brightness > 200) return '浅灰色';
+      if (brightness > 100) return '中灰色';
+      return '深灰色';
+    }
+
+    // 高饱和度,判断色相
+    let colorName = '';
+    if (r >= g && r >= b) {
+      if (g > b) {
+        colorName = brightness > 150 ? '浅橙色' : '橙色';
+      } else {
+        colorName = brightness > 150 ? '浅红色' : '红色';
+      }
+    } else if (g >= r && g >= b) {
+      if (r > b) {
+        colorName = brightness > 150 ? '浅黄绿' : '黄绿色';
+      } else {
+        colorName = brightness > 150 ? '浅绿色' : '绿色';
+      }
+    } else {
+      if (r > g) {
+        colorName = brightness > 150 ? '浅紫色' : '紫色';
+      } else {
+        colorName = brightness > 150 ? '浅蓝色' : '蓝色';
+      }
+    }
+
+    return colorName;
+  }
+
+  // 将十六进制颜色转换为RGB
+  private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
+    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+    return result ? {
+      r: parseInt(result[1], 16),
+      g: parseInt(result[2], 16),
+      b: parseInt(result[3], 16)
+    } : null;
+  }
+
+  // 复制颜色描述到剪贴板
+  async copyColorDescription(): Promise<void> {
+    const description = this.generateColorDescription();
+    if (!description) return;
+
+    try {
+      await navigator.clipboard.writeText(description);
+      this.copySuccess = true;
+      console.log('颜色描述已复制到剪贴板');
+      
+      // 2秒后重置复制状态
+      setTimeout(() => {
+        this.copySuccess = false;
+      }, 2000);
+    } catch (err) {
+      console.error('复制失败:', err);
+      // 降级方案:使用传统方法
+      this.fallbackCopyTextToClipboard(description);
+    }
+  }
+
+  // 降级复制方案
+  private fallbackCopyTextToClipboard(text: string): void {
+    const textArea = document.createElement('textarea');
+    textArea.value = text;
+    textArea.style.top = '0';
+    textArea.style.left = '0';
+    textArea.style.position = 'fixed';
+    textArea.style.opacity = '0';
+
+    document.body.appendChild(textArea);
+    textArea.focus();
+    textArea.select();
+
+    try {
+      const successful = document.execCommand('copy');
+      if (successful) {
+        this.copySuccess = true;
+        console.log('颜色描述已复制到剪贴板(降级方案)');
+        
+        // 2秒后重置复制状态
+        setTimeout(() => {
+          this.copySuccess = false;
+        }, 2000);
+      }
+    } catch (err) {
+      console.error('降级复制方案也失败了:', err);
+    }
+
+    document.body.removeChild(textArea);
+  }
+
   private checkScreenSize() {
     const width = window.innerWidth;
     this.isMobile = width < 768;