Browse Source

feat: color-get & project-files

Future 12 hours ago
parent
commit
c9a1f8687e

+ 4 - 0
rules/schemas.md

@@ -943,10 +943,14 @@ Product.quotation 产品报价字段
 | fileSize | Number | 否 | 文件大小(字节) | 1024000 |
 | stage | String | 否 | 关联阶段 | "需求沟通" |
 | data | Object | 否 | 扩展数据 | { thumbnailUrl, ... } |
+| analysis | Object 
 | isDeleted | Boolean | 否 | 软删除标记 | false |
 | createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
 | updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
 
+
+analysis.color 存放色彩分析插件解析的结果
+
 **fileType 枚举值**:
 - `cad`: CAD图纸
 - `reference`: 参考图片

+ 4 - 6
src/app/pages/admin/dashboard/dashboard.ts

@@ -204,7 +204,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
   private async loadProjectStats(): Promise<void> {
     try {
       const projectQuery = new FmodeQuery('Project');
-
+      projectQuery.equalTo('company', localStorage.getItem("company") || 'unknonw');
       // 总项目数
       const totalProjects = await projectQuery.count();
       this.stats.totalProjects.set(totalProjects);
@@ -513,7 +513,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
   // 加载项目详情数据
   private async loadProjectDetailData(type: 'totalProjects' | 'active' | 'completed'): Promise<void> {
     const projectQuery = new FmodeQuery('Project');
-
+    projectQuery.include("onwer")
     if (type === 'active') {
       projectQuery.equalTo('status', '进行中');
     } else if (type === 'completed') {
@@ -538,7 +538,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
   // 加载设计师详情数据
   private async loadDesignerDetailData(): Promise<void> {
     const designerQuery = new FmodeQuery('Profile');
-    designerQuery.equalTo('role', 'designer');
+    designerQuery.equalTo('roleName', '组员');
 
     const designers = await designerQuery.descending('createdAt').find();
 
@@ -557,9 +557,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
 
   // 加载客户详情数据
   private async loadCustomerDetailData(): Promise<void> {
-    const customerQuery = new FmodeQuery('Profile');
-    customerQuery.equalTo('role', 'customer');
-
+    const customerQuery = new FmodeQuery('ContactInfo');
     const customers = await customerQuery.descending('createdAt').find();
 
     const detailItems = customers.map((customer: FmodeObject) => ({

+ 82 - 0
src/modules/project/components/color-get/color-get-dialog.component.html

@@ -0,0 +1,82 @@
+<div class="color-get-dialog">
+  <div class="dialog-header">
+    <div class="title">色彩分析报告</div>
+    <button class="close-btn" (click)="close()">×</button>
+  </div>
+
+  <div class="dialog-body">
+    <div class="layout">
+      <div class="image-panel">
+        <div class="panel-title">原图 / 马赛克</div>
+        <div class="image-wrapper">
+          <img [src]="imageUrl" [alt]="imageName" />
+        </div>
+        <div class="mosaic-wrapper">
+          <canvas #mosaicCanvas></canvas>
+        </div>
+      </div>
+
+      <div class="analysis-panel">
+        <div class="panel-title">色彩分析</div>
+
+        <div class="loading" *ngIf="loading">
+          <div class="spinner"></div>
+          <div class="progress">分析中... {{ progress }}%</div>
+        </div>
+
+        <div class="metrics" *ngIf="!loading && report">
+          <div class="metric-item">
+            <div class="label">冷暖平衡</div>
+            <div class="value" [class.cold]="report.metrics.warmCoolBalance < 0" [class.warm]="report.metrics.warmCoolBalance > 0">
+              {{ report.metrics.warmCoolBalance | number:'1.0-0' }}
+            </div>
+          </div>
+          <div class="metric-item">
+            <div class="label">平均亮度</div>
+            <div class="value">{{ report.metrics.averageBrightness | number:'1.0-0' }}</div>
+          </div>
+          <div class="metric-item">
+            <div class="label">平均饱和度</div>
+            <div class="value">{{ report.metrics.averageSaturation | number:'1.0-0' }}</div>
+          </div>
+          <div class="metric-item">
+            <div class="label">色彩多样度</div>
+            <div class="value">{{ report.metrics.diversity }}</div>
+          </div>
+        </div>
+
+        <div class="palette" *ngIf="!loading && report">
+          <div class="palette-title">主要色卡(Top 20)</div>
+          <div class="palette-grid">
+            <div class="palette-item" *ngFor="let c of report.palette">
+              <div class="swatch" [style.background]="c.hex"></div>
+              <div class="info">
+                <div class="hex">{{ c.hex }}</div>
+                <div class="pct">{{ c.percentage | number:'1.0-1' }}%</div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="charts" *ngIf="!loading && report">
+          <div class="chart-block">
+            <div class="chart-title">色彩分布直方图(亮度/饱和度)</div>
+            <div #histogramChart class="chart"></div>
+          </div>
+          <div class="chart-block">
+            <div class="chart-title">色彩拆分图(纵轴明暗,横轴冷暖)</div>
+            <div #splitChart class="chart"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="footer">
+      <div class="file-info">
+        <span class="name">{{ imageName }}</span>
+        <span class="size" *ngIf="imageSize">{{ imageSize / 1024 | number:'1.0-1' }} KB</span>
+      </div>
+      <button class="close-btn" (click)="close()">关闭</button>
+    </div>
+  </div>
+</div>

+ 169 - 0
src/modules/project/components/color-get/color-get-dialog.component.scss

@@ -0,0 +1,169 @@
+.color-get-dialog {
+  width: 90vw;
+  max-width: 1200px;
+  max-height: 80vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.dialog-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  border-bottom: 1px solid #eee;
+
+  .title {
+    font-size: 18px;
+    font-weight: 600;
+  }
+
+  .close-btn {
+    border: none;
+    background: transparent;
+    font-size: 20px;
+    cursor: pointer;
+  }
+}
+
+.dialog-body {
+  padding: 12px 16px;
+  overflow: auto;
+}
+
+.layout {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.image-panel, .analysis-panel {
+  border: 1px solid #eee;
+  border-radius: 8px;
+  padding: 12px;
+}
+
+.panel-title {
+  font-size: 14px;
+  font-weight: 600;
+  margin-bottom: 8px;
+}
+
+.image-wrapper {
+  border: 1px solid #f0f0f0;
+  border-radius: 8px;
+  overflow: hidden;
+  margin-bottom: 8px;
+
+  img {
+    width: 100%;
+    display: block;
+  }
+}
+
+.mosaic-wrapper {
+  border: 1px dashed #ddd;
+  border-radius: 8px;
+  padding: 8px;
+
+  canvas {
+    width: 100%;
+    height: auto;
+    display: block;
+  }
+}
+
+.loading {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+
+  .spinner {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    border: 2px solid #ddd;
+    border-top-color: #409eff;
+    animation: spin 1s linear infinite;
+  }
+}
+
+@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
+
+.metrics {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 8px;
+  margin-bottom: 12px;
+
+  .metric-item {
+    border: 1px solid #eee;
+    border-radius: 8px;
+    padding: 8px;
+    text-align: center;
+
+    .label { font-size: 12px; color: #666; }
+    .value { font-size: 16px; font-weight: 600; }
+    .value.cold { color: #409eff; }
+    .value.warm { color: #e67e22; }
+  }
+}
+
+.palette {
+  .palette-title { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
+  .palette-grid {
+    display: grid;
+    grid-template-columns: repeat(5, 1fr);
+    gap: 8px;
+
+    @media (max-width: 768px) {
+      grid-template-columns: repeat(3, 1fr);
+    }
+
+    .palette-item {
+      display: flex;
+      gap: 6px;
+      align-items: center;
+      border: 1px solid #eee;
+      border-radius: 8px;
+      padding: 6px;
+
+      .swatch { width: 28px; height: 28px; border-radius: 4px; }
+      .info { font-size: 12px; display: flex; gap: 10px; }
+      .hex { color: #333; }
+      .pct { color: #666; }
+    }
+  }
+}
+
+.charts {
+  margin-top: 12px;
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 12px;
+
+  .chart-block { border: 1px solid #eee; border-radius: 8px; padding: 8px; }
+  .chart-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
+  .chart { width: 100%; height: 260px; }
+}
+
+.footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+
+  .file-info { font-size: 12px; color: #666; display: flex; gap: 8px; }
+  .close-btn {
+    background: #409eff;
+    color: #fff;
+    border: none;
+    padding: 6px 12px;
+    border-radius: 6px;
+    cursor: pointer;
+  }
+}

+ 365 - 0
src/modules/project/components/color-get/color-get-dialog.component.ts

@@ -0,0 +1,365 @@
+import { Component, Inject, OnDestroy, OnInit, ViewChild, ElementRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import * as echarts from 'echarts';
+
+export interface ProjectFileLike {
+  id?: string;
+  name?: string;
+  originalName?: string;
+  url: string;
+  type?: string;
+  size?: number;
+}
+
+interface PaletteColor {
+  rgb: { r: number; g: number; b: number };
+  hex: string;
+  percentage: number;
+}
+
+interface ColorAnalysisReport {
+  palette: PaletteColor[];
+  mosaicUrl?: string;
+  metrics: {
+    warmCoolBalance: number; // -100(冷) 到 100(暖)
+    averageBrightness: number; // 0-100
+    averageSaturation: number; // 0-100
+    diversity: number; // 色彩多样度:主色数量
+  };
+  histogram: {
+    brightnessBins: number[]; // 0..100
+    saturationBins: number[]; // 0..100
+  };
+  splitPoints: Array<{ temp: number; brightness: number; size: number; color: string }>;
+}
+
+@Component({
+  selector: 'app-color-get-dialog',
+  standalone: true,
+  imports: [CommonModule, MatDialogModule],
+  templateUrl: './color-get-dialog.component.html',
+  styleUrls: ['./color-get-dialog.component.scss']
+})
+export class ColorGetDialogComponent implements OnInit, OnDestroy {
+  @ViewChild('mosaicCanvas') mosaicCanvasRef!: ElementRef<HTMLCanvasElement>;
+  @ViewChild('histogramChart') histogramChartRef!: ElementRef<HTMLDivElement>;
+  @ViewChild('splitChart') splitChartRef!: ElementRef<HTMLDivElement>;
+
+  imageUrl = '';
+  imageName = '';
+  imageSize?: number;
+  loading = true;
+  progress = 0;
+
+  pixelSize = 100; // 参考图马赛克像素块大小
+  report?: ColorAnalysisReport;
+
+  private histogramChart?: echarts.ECharts;
+  private splitChart?: echarts.ECharts;
+
+  constructor(
+    private dialogRef: MatDialogRef<ColorGetDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: { file?: ProjectFileLike; url?: string; name?: string }
+  ) {}
+
+  ngOnInit(): void {
+    const file = this.data?.file;
+    this.imageUrl = file?.url || this.data?.url || '';
+    this.imageName = file?.originalName || file?.name || this.data?.name || '未命名图片';
+    this.imageSize = file?.size;
+
+    if (!this.imageUrl) {
+      this.loading = false;
+      return;
+    }
+
+    // 开始分析
+    this.loadAndAnalyze(this.imageUrl).catch(err => {
+      console.error('色彩分析失败:', err);
+      this.loading = false;
+    });
+  }
+
+  ngOnDestroy(): void {
+    if (this.histogramChart) {
+      this.histogramChart.dispose();
+    }
+    if (this.splitChart) {
+      this.splitChart.dispose();
+    }
+  }
+
+  close(): void {
+    this.dialogRef.close();
+  }
+
+  private async loadAndAnalyze(url: string): Promise<void> {
+    this.progress = 5;
+    const img = await this.loadImage(url);
+    this.progress = 20;
+
+    const { mosaicCanvas, blockColors } = this.createMosaic(img, this.pixelSize);
+    this.progress = 50;
+
+    // 显示马赛克图
+    const canvas = this.mosaicCanvasRef.nativeElement;
+    canvas.width = mosaicCanvas.width;
+    canvas.height = mosaicCanvas.height;
+    const ctx = canvas.getContext('2d');
+    if (ctx) ctx.drawImage(mosaicCanvas, 0, 0);
+
+    // 构建色彩报告
+    this.report = this.buildReport(blockColors);
+    this.progress = 80;
+
+    // 渲染图表
+    setTimeout(() => {
+      this.renderHistogram();
+      this.renderSplitChart();
+      this.progress = 100;
+      this.loading = false;
+    }, 0);
+  }
+
+  private loadImage(url: string): Promise<HTMLImageElement> {
+    return new Promise((resolve, reject) => {
+      const img = new Image();
+      img.crossOrigin = 'anonymous';
+      img.onload = () => resolve(img);
+      img.onerror = reject;
+      img.src = url;
+    });
+  }
+
+  private createMosaic(image: HTMLImageElement, pixelSize: number): { mosaicCanvas: HTMLCanvasElement; blockColors: { r: number; g: number; b: number }[] } {
+    const width = image.width;
+    const height = image.height;
+    const canvas = document.createElement('canvas');
+    canvas.width = width;
+    canvas.height = height;
+    const ctx = canvas.getContext('2d')!;
+    ctx.drawImage(image, 0, 0, width, height);
+
+    const blockColors: { r: number; g: number; b: number }[] = [];
+
+    for (let y = 0; y < height; y += pixelSize) {
+      for (let x = 0; x < width; x += pixelSize) {
+        const bw = Math.min(pixelSize, width - x);
+        const bh = Math.min(pixelSize, height - y);
+        const data = ctx.getImageData(x, y, bw, bh).data;
+
+        let r = 0, g = 0, b = 0;
+        const count = bw * bh;
+        for (let i = 0; i < data.length; i += 4) {
+          r += data[i];
+          g += data[i + 1];
+          b += data[i + 2];
+        }
+        r = Math.round(r / count);
+        g = Math.round(g / count);
+        b = Math.round(b / count);
+
+        blockColors.push({ r, g, b });
+
+        ctx.fillStyle = `rgb(${r},${g},${b})`;
+        ctx.fillRect(x, y, bw, bh);
+      }
+    }
+
+    return { mosaicCanvas: canvas, blockColors };
+  }
+
+  private buildReport(blockColors: { r: number; g: number; b: number }[]): ColorAnalysisReport {
+    // 统计颜色频率
+    const colorMap = new Map<string, number>();
+    const totalBlocks = blockColors.length;
+
+    blockColors.forEach(({ r, g, b }) => {
+      const key = `${r},${g},${b}`; // 精确块平均色
+      colorMap.set(key, (colorMap.get(key) || 0) + 1);
+    });
+
+    // 合并近似色(降低抖动),按 16 级量化
+    const quantMap = new Map<string, number>();
+    colorMap.forEach((count, key) => {
+      const [r, g, b] = key.split(',').map(Number);
+      const qr = Math.round(r / 16) * 16;
+      const qg = Math.round(g / 16) * 16;
+      const qb = Math.round(b / 16) * 16;
+      const qkey = `${qr},${qg},${qb}`;
+      quantMap.set(qkey, (quantMap.get(qkey) || 0) + count);
+    });
+
+    const palette = Array.from(quantMap.entries())
+      .map(([k, c]) => {
+        const [r, g, b] = k.split(',').map(Number);
+        const hex = this.rgbToHex(r, g, b);
+        return {
+          rgb: { r, g, b },
+          hex,
+          percentage: +(c / totalBlocks * 100).toFixed(2)
+        } as PaletteColor;
+      })
+      .sort((a, b) => b.percentage - a.percentage)
+      .slice(0, 20);
+
+    // 计算指标与分布
+    const brightnessBins = new Array(10).fill(0); // 10等分
+    const saturationBins = new Array(10).fill(0);
+    let sumBrightness = 0;
+    let sumSaturation = 0;
+    let sumTempScore = 0;
+
+    const splitPoints: Array<{ temp: number; brightness: number; size: number; color: string }> = [];
+
+    blockColors.forEach(({ r, g, b }) => {
+      const { h, s, v } = this.rgbToHsv(r, g, b);
+      const brightness = v * 100;
+      const saturation = s * 100;
+      const temp = this.warmCoolScoreFromHue(h); // -100..100
+
+      sumBrightness += brightness;
+      sumSaturation += saturation;
+      sumTempScore += temp;
+
+      const bIdx = Math.min(9, Math.floor(brightness / 10));
+      const sIdx = Math.min(9, Math.floor(saturation / 10));
+      brightnessBins[bIdx]++;
+      saturationBins[sIdx]++;
+
+      splitPoints.push({ temp, brightness, size: 1, color: `rgb(${r},${g},${b})` });
+    });
+
+    const avgBrightness = +(sumBrightness / totalBlocks).toFixed(1);
+    const avgSaturation = +(sumSaturation / totalBlocks).toFixed(1);
+    const warmCoolBalance = +((sumTempScore / totalBlocks)).toFixed(1);
+
+    return {
+      palette,
+      metrics: {
+        warmCoolBalance,
+        averageBrightness: avgBrightness,
+        averageSaturation: avgSaturation,
+        diversity: palette.length
+      },
+      histogram: {
+        brightnessBins: brightnessBins.map(v => Math.round(v)),
+        saturationBins: saturationBins.map(v => Math.round(v))
+      },
+      splitPoints
+    };
+  }
+
+  private renderHistogram(): void {
+    if (!this.report) return;
+    const el = this.histogramChartRef.nativeElement;
+    this.histogramChart = echarts.init(el);
+
+    const option: echarts.EChartsOption = {
+      tooltip: {},
+      grid: { left: 40, right: 20, top: 30, bottom: 40 },
+      legend: { data: ['亮度', '饱和度'] },
+      xAxis: {
+        type: 'category',
+        data: ['0-10', '10-20', '20-30', '30-40', '40-50', '50-60', '60-70', '70-80', '80-90', '90-100']
+      },
+      yAxis: { type: 'value' },
+      series: [
+        {
+          name: '亮度',
+          type: 'bar',
+          data: this.report.histogram.brightnessBins,
+          itemStyle: { color: '#4ECDC4' }
+        },
+        {
+          name: '饱和度',
+          type: 'bar',
+          data: this.report.histogram.saturationBins,
+          itemStyle: { color: '#FF6B6B' }
+        }
+      ]
+    };
+
+    this.histogramChart.setOption(option);
+  }
+
+  private renderSplitChart(): void {
+    if (!this.report) return;
+    const el = this.splitChartRef.nativeElement;
+    this.splitChart = echarts.init(el);
+
+    const data = this.report.splitPoints.map(p => [p.temp, p.brightness, p.size, p.color]);
+
+    const option: echarts.EChartsOption = {
+      tooltip: {
+        formatter: (params: any) => {
+          const [temp, bright, , color] = params.data;
+          return `冷暖: ${temp.toFixed(1)}<br/>亮度: ${bright.toFixed(1)}<br/><span style="display:inline-block;width:12px;height:12px;background:${color};margin-right:6px"></span>${color}`;
+        }
+      },
+      xAxis: {
+        type: 'value',
+        min: -100,
+        max: 100,
+        name: '冷 ← → 暖'
+      },
+      yAxis: {
+        type: 'value',
+        min: 0,
+        max: 100,
+        name: '暗 ↑ ↓ 亮'
+      },
+      series: [
+        {
+          name: '色彩拆分',
+          type: 'scatter',
+          symbolSize: 8,
+          data,
+          itemStyle: {
+            color: (p: any) => p.data[3]
+          }
+        }
+      ]
+    };
+
+    this.splitChart.setOption(option);
+  }
+
+  // --- 色彩工具函数 ---
+  private rgbToHex(r: number, g: number, b: number): string {
+    const toHex = (n: number) => n.toString(16).padStart(2, '0');
+    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+  }
+
+  private rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {
+    r /= 255; g /= 255; b /= 255;
+    const max = Math.max(r, g, b), min = Math.min(r, g, b);
+    const v = max;
+    const d = max - min;
+    const s = max === 0 ? 0 : d / max;
+    let h = 0;
+    if (max === min) {
+      h = 0;
+    } else {
+      switch (max) {
+        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+        case g: h = (b - r) / d + 2; break;
+        case b: h = (r - g) / d + 4; break;
+      }
+      h /= 6;
+    }
+    return { h: h * 360, s, v };
+  }
+
+  private warmCoolScoreFromHue(hue: number): number {
+    // 简化冷暖评分:红橙黄(0-60)与洋红(300-360)偏暖;青蓝紫(180-300)偏冷;绿色(60-180)中性略冷
+    if (hue >= 0 && hue < 60) return 80 - (60 - hue) * (80 / 60); // 80→0
+    if (hue >= 60 && hue < 120) return 20 - (120 - hue) * (20 / 60); // 20→0
+    if (hue >= 120 && hue < 180) return -20 + (hue - 120) * (20 / 60); // -20→0
+    if (hue >= 180 && hue < 240) return -60 + (hue - 180) * (40 / 60); // -60→-20
+    if (hue >= 240 && hue < 300) return -80 + (hue - 240) * (20 / 60); // -80→-60
+    if (hue >= 300 && hue <= 360) return 80 - (360 - hue) * (80 / 60); // 80→0
+    return 0;
+  }
+}

+ 4 - 0
src/modules/project/components/project-files-modal/project-files-modal.component.html

@@ -277,4 +277,8 @@
       </div>
     </div>
   }
+</div>
+<div class="file-actions" *ngIf="selectedFile">
+  <button class="download-btn" (click)="downloadFile(selectedFile)">下载</button>
+  <button class="analyze-btn" *ngIf="isImageFile(selectedFile)" (click)="openColorAnalysis(selectedFile)">色彩分析</button>
 </div>

+ 23 - 0
src/modules/project/components/project-files-modal/project-files-modal.component.scss

@@ -850,4 +850,27 @@
       }
     }
   }
+}
+
+.file-actions {
+  display: flex;
+  gap: 8px;
+  margin-top: 8px;
+}
+
+.download-btn, .analyze-btn {
+  padding: 6px 12px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.download-btn {
+  background: #1976d2;
+  color: #fff;
+}
+
+.analyze-btn {
+  background: #6a1b9a;
+  color: #fff;
 }

+ 23 - 3
src/modules/project/components/project-files-modal/project-files-modal.component.ts

@@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FmodeObject, FmodeQuery, FmodeParse } from 'fmode-ng/parse';
 import { NovaStorage, NovaFile } from 'fmode-ng/core';
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { ColorGetDialogComponent } from '../color-get/color-get-dialog.component';
 
 const Parse = FmodeParse.with('nova');
 
@@ -29,7 +31,7 @@ export interface ProjectFile {
 @Component({
   selector: 'app-project-files-modal',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, MatDialogModule],
   templateUrl: './project-files-modal.component.html',
   styleUrls: ['./project-files-modal.component.scss']
 })
@@ -52,7 +54,7 @@ export class ProjectFilesModalComponent implements OnInit {
   imageCount: number = 0;
   documentCount: number = 0;
 
-  constructor() {}
+  constructor(private dialog: MatDialog) {}
 
   ngOnInit(): void {
     if (this.isVisible && this.project) {
@@ -144,6 +146,24 @@ export class ProjectFilesModalComponent implements OnInit {
     document.body.removeChild(link);
   }
 
+  openColorAnalysis(file: ProjectFile): void {
+    if (!this.isImageFile(file)) return;
+    this.dialog.open(ColorGetDialogComponent, {
+      data: {
+        file: {
+          id: file.id,
+          name: file.name,
+          originalName: file.originalName,
+          url: file.url,
+          type: file.type,
+          size: file.size
+        }
+      },
+      width: '90vw',
+      maxWidth: '1200px'
+    });
+  }
+
   formatFileSize(bytes: number): string {
     if (bytes === 0) return '0 Bytes';
     const k = 1024;
@@ -166,7 +186,7 @@ export class ProjectFilesModalComponent implements OnInit {
   }
 
   getFileExtension(name: string): string {
-    return name.split('.').pop()?.toUpperCase() || '';
+    return name.split('.')?.pop()?.toUpperCase() || '';
   }
 
   isImageFile(file: ProjectFile): boolean {