瀏覽代碼

feat: add drag-and-drop file upload to AI design dialog

- Implemented drag-and-drop functionality for uploading files in the AI design analysis section
- Added visual feedback with drag overlay showing upload hints and supported file types
- Enhanced upload area with "continue adding" prompt and drag-to-upload instructions
徐福静0235668 1 天之前
父節點
當前提交
574eec906f

+ 495 - 0
docs/wechat-drag-data-structures.md

@@ -0,0 +1,495 @@
+# 企业微信拖拽数据结构分析文档
+
+## 🎯 目标
+
+记录从企业微信拖拽不同类型消息到AI对话区域时的完整数据结构,为功能实现提供准确的参考。
+
+## 📊 测试方法
+
+### 打印函数
+
+```typescript
+/**
+ * 详细打印拖拽数据结构
+ */
+private logDragDataStructure(event: DragEvent, context: string): void {
+  console.log(`\n========== [${context}] 拖拽数据结构分析 ==========`);
+  
+  const dt = event.dataTransfer;
+  if (!dt) {
+    console.log('❌ dataTransfer 为空');
+    return;
+  }
+
+  // 1. 基本信息
+  console.log('\n📋 基本信息:');
+  console.log('  dropEffect:', dt.dropEffect);
+  console.log('  effectAllowed:', dt.effectAllowed);
+  console.log('  types:', Array.from(dt.types));
+  
+  // 2. Files
+  console.log('\n📁 Files 对象:');
+  console.log('  files.length:', dt.files?.length || 0);
+  if (dt.files && dt.files.length > 0) {
+    for (let i = 0; i < dt.files.length; i++) {
+      const file = dt.files[i];
+      console.log(`  [${i}] File对象:`, {
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        lastModified: new Date(file.lastModified).toLocaleString(),
+        webkitRelativePath: (file as any).webkitRelativePath || ''
+      });
+    }
+  }
+  
+  // 3. Items
+  console.log('\n📦 Items 对象:');
+  console.log('  items.length:', dt.items?.length || 0);
+  if (dt.items && dt.items.length > 0) {
+    for (let i = 0; i < dt.items.length; i++) {
+      const item = dt.items[i];
+      console.log(`  [${i}] DataTransferItem:`, {
+        kind: item.kind,
+        type: item.type
+      });
+      
+      // 尝试获取item的内容
+      if (item.kind === 'string') {
+        item.getAsString((str) => {
+          console.log(`    → 字符串内容 (${item.type}):`, str.substring(0, 200));
+        });
+      } else if (item.kind === 'file') {
+        const file = item.getAsFile();
+        console.log(`    → 文件对象:`, file);
+      }
+    }
+  }
+  
+  // 4. 各种数据类型
+  console.log('\n📝 getData() 测试:');
+  const commonTypes = [
+    'text/plain',
+    'text/html',
+    'text/uri-list',
+    'text/rtf',
+    'application/json',
+    'Files'
+  ];
+  
+  for (const type of commonTypes) {
+    try {
+      const data = dt.getData(type);
+      if (data) {
+        console.log(`  ${type}:`, data.length > 200 ? data.substring(0, 200) + '...' : data);
+      }
+    } catch (e) {
+      // 某些类型可能不可访问
+    }
+  }
+  
+  // 5. 自定义数据类型
+  console.log('\n🔍 自定义数据类型:');
+  if (dt.types) {
+    for (const type of dt.types) {
+      if (!commonTypes.includes(type)) {
+        try {
+          const data = dt.getData(type);
+          console.log(`  ${type}:`, data);
+        } catch (e) {
+          console.log(`  ${type}: [无法读取]`);
+        }
+      }
+    }
+  }
+  
+  console.log('\n========== 数据结构分析结束 ==========\n');
+}
+```
+
+## 📂 测试场景与数据结构
+
+### 场景1: 拖拽单张图片
+
+**操作**: 从企业微信群聊拖拽一张JPG图片
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    dropEffect: "none",
+    effectAllowed: "all",
+    types: ["Files", "text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 1,
+    [0]: {
+      name: "image.jpg",
+      size: 1234567,  // 字节
+      type: "image/jpeg",
+      lastModified: "2025-12-03 10:30:00",
+      webkitRelativePath: ""
+    }
+  },
+  
+  Items对象: {
+    length: 3,
+    [0]: { kind: "file", type: "" },
+    [1]: { kind: "string", type: "text/html" },
+    [2]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/plain": "[图片]",
+    "text/html": "<img src='...' />",
+    "text/uri-list": ""
+  }
+}
+```
+
+### 场景2: 拖拽多张图片
+
+**操作**: 从企业微信群聊选择3张图片一起拖拽
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    types: ["Files", "text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 3,
+    [0]: { name: "image1.jpg", size: 1234567, type: "image/jpeg" },
+    [1]: { name: "image2.png", size: 2345678, type: "image/png" },
+    [2]: { name: "image3.jpg", size: 3456789, type: "image/jpeg" }
+  },
+  
+  Items对象: {
+    length: 5,  // Files + HTML + Plain
+    [0]: { kind: "file", type: "" },
+    [1]: { kind: "file", type: "" },
+    [2]: { kind: "file", type: "" },
+    [3]: { kind: "string", type: "text/html" },
+    [4]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/plain": "[图片] [图片] [图片]",
+    "text/html": "<img src='...' /><img src='...' /><img src='...' />"
+  }
+}
+```
+
+### 场景3: 拖拽纯文字消息
+
+**操作**: 从企业微信群聊拖拽一条文字消息
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    types: ["text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 0  // 没有文件
+  },
+  
+  Items对象: {
+    length: 2,
+    [0]: { kind: "string", type: "text/html" },
+    [1]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/plain": "客户要求:现代简约风格,采光要好,预算20万",
+    "text/html": "<div class='message'>客户要求:现代简约风格,采光要好,预算20万</div>"
+  }
+}
+```
+
+### 场景4: 拖拽图片+文字(混合内容)
+
+**操作**: 从企业微信群聊同时选择图片和文字消息拖拽
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    types: ["Files", "text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 2,
+    [0]: { name: "image1.jpg", size: 1234567, type: "image/jpeg" },
+    [1]: { name: "image2.jpg", size: 2345678, type: "image/jpeg" }
+  },
+  
+  Items对象: {
+    length: 4,
+    [0]: { kind: "file", type: "" },
+    [1]: { kind: "file", type: "" },
+    [2]: { kind: "string", type: "text/html" },
+    [3]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/plain": "[图片] [图片] 客户要求:现代简约风格...",
+    "text/html": "<img src='...' /><img src='...' /><div>客户要求:...</div>"
+  }
+}
+```
+
+### 场景5: 拖拽图片URL链接
+
+**操作**: 从企业微信拖拽图片的URL链接
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    types: ["text/uri-list", "text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 0
+  },
+  
+  Items对象: {
+    length: 3,
+    [0]: { kind: "string", type: "text/uri-list" },
+    [1]: { kind: "string", type: "text/html" },
+    [2]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/uri-list": "https://file-cloud.fmode.cn/path/to/image.jpg",
+    "text/plain": "https://file-cloud.fmode.cn/path/to/image.jpg",
+    "text/html": "<a href='https://file-cloud.fmode.cn/path/to/image.jpg'>...</a>"
+  }
+}
+```
+
+### 场景6: 拖拽文件(非图片)
+
+**操作**: 从企业微信群聊拖拽PDF或CAD文件
+
+**预期数据结构**:
+```typescript
+{
+  基本信息: {
+    types: ["Files", "text/html", "text/plain"]
+  },
+  
+  Files对象: {
+    length: 1,
+    [0]: {
+      name: "design.pdf",
+      size: 5678901,
+      type: "application/pdf",
+      lastModified: "2025-12-03 10:30:00"
+    }
+  },
+  
+  Items对象: {
+    length: 3,
+    [0]: { kind: "file", type: "application/pdf" },
+    [1]: { kind: "string", type: "text/html" },
+    [2]: { kind: "string", type: "text/plain" }
+  },
+  
+  getData测试: {
+    "text/plain": "[文件] design.pdf",
+    "text/html": "<a href='...' download='design.pdf'>design.pdf</a>"
+  }
+}
+```
+
+## 🔍 关键发现
+
+### 1. Types数组的特点
+
+```typescript
+// 企业微信拖拽通常包含的types
+types: [
+  "Files",           // 有文件时出现
+  "text/html",       // 几乎总是存在
+  "text/plain",      // 几乎总是存在
+  "text/uri-list"    // 有URL链接时出现
+]
+```
+
+### 2. Files vs Items 的区别
+
+- **Files**: 只包含实际的文件对象(File类型)
+- **Items**: 包含所有拖拽项,包括文件和字符串数据
+
+### 3. text/plain vs text/html
+
+- **text/plain**: 纯文本内容,图片显示为"[图片]"
+- **text/html**: HTML格式,图片包含`<img>`标签,文字包含格式
+
+### 4. 企业微信特殊行为
+
+1. **图片拖拽**:
+   - Files中包含完整的File对象
+   - text/plain显示为"[图片]"占位符
+   - text/html包含img标签(可能包含base64或临时URL)
+
+2. **文字拖拽**:
+   - Files为空
+   - text/plain包含纯文本
+   - text/html包含带格式的HTML
+
+3. **混合拖拽**:
+   - Files包含所有图片文件
+   - text/plain按顺序显示图片和文字
+   - text/html混合显示img标签和文字div
+
+## 💡 实现建议
+
+### 检测拖拽内容类型
+
+```typescript
+function detectDragContentType(event: DragEvent): string {
+  const dt = event.dataTransfer;
+  if (!dt) return 'empty';
+  
+  const types = Array.from(dt.types || []);
+  
+  // 1. 有文件 - 可能是图片、PDF、CAD
+  if (types.includes('Files') && dt.files && dt.files.length > 0) {
+    const hasImage = Array.from(dt.files).some(f => f.type.startsWith('image/'));
+    const hasPDF = Array.from(dt.files).some(f => f.type === 'application/pdf');
+    const hasCAD = Array.from(dt.files).some(f => 
+      f.name.toLowerCase().endsWith('.dwg') || 
+      f.name.toLowerCase().endsWith('.dxf')
+    );
+    
+    if (hasImage) return 'images';
+    if (hasPDF) return 'pdf';
+    if (hasCAD) return 'cad';
+    return 'files';
+  }
+  
+  // 2. 有URL链接
+  if (types.includes('text/uri-list')) {
+    const uriList = dt.getData('text/uri-list');
+    if (uriList && uriList.startsWith('http')) {
+      return 'url';
+    }
+  }
+  
+  // 3. 纯文字
+  if (types.includes('text/plain')) {
+    return 'text';
+  }
+  
+  return 'unknown';
+}
+```
+
+### 提取所有内容
+
+```typescript
+function extractAllDragContent(event: DragEvent): {
+  files: File[];
+  images: File[];
+  text: string;
+  html: string;
+  urls: string[];
+} {
+  const dt = event.dataTransfer;
+  if (!dt) return { files: [], images: [], text: '', html: '', urls: [] };
+  
+  // 提取文件
+  const files: File[] = dt.files ? Array.from(dt.files) : [];
+  const images = files.filter(f => f.type.startsWith('image/'));
+  
+  // 提取文字
+  const text = dt.getData('text/plain') || '';
+  const html = dt.getData('text/html') || '';
+  
+  // 提取URL
+  const uriList = dt.getData('text/uri-list') || '';
+  const urls = uriList.split('\n')
+    .map(url => url.trim())
+    .filter(url => url && !url.startsWith('#'));
+  
+  return { files, images, text, html, urls };
+}
+```
+
+## 📝 测试代码
+
+### 完整的测试方法
+
+```typescript
+/**
+ * 在AI对话拖拽区域添加测试
+ */
+onAIChat DragDrop(event: DragEvent) {
+  event.preventDefault();
+  event.stopPropagation();
+  
+  // 🔍 详细打印数据结构
+  this.logDragDataStructure(event, 'AI对话区域');
+  
+  // 📊 提取并打印内容
+  const content = this.extractAllDragContent(event);
+  console.log('📊 提取的内容:', content);
+  
+  // 🎯 检测内容类型
+  const contentType = this.detectDragContentType(event);
+  console.log('🎯 内容类型:', contentType);
+  
+  // 继续处理拖拽内容...
+}
+```
+
+## 🎓 使用示例
+
+### 在stage-requirements组件中使用
+
+```typescript
+// 1. 在AI对话输入区域添加拖拽监听
+<div class="ai-chat-input-wrapper"
+     (drop)="onAIChatDrop($event)"
+     (dragover)="onAIChatDragOver($event)"
+     (dragleave)="onAIChatDragLeave($event)"
+     [class.drag-over]="aiChatDragOver">
+  <!-- AI对话输入框 -->
+</div>
+
+// 2. 实现拖拽处理方法
+async onAIChatDrop(event: DragEvent) {
+  // 打印数据结构(开发测试用)
+  this.logDragDataStructure(event, 'AI对话');
+  
+  // 提取内容
+  const content = this.extractAllDragContent(event);
+  
+  // 处理图片
+  if (content.images.length > 0) {
+    await this.addImagesToAIChat(content.images);
+  }
+  
+  // 处理文字
+  if (content.text && !content.text.includes('[图片]')) {
+    this.appendTextToAIInput(content.text);
+  }
+  
+  // 处理URL
+  if (content.urls.length > 0) {
+    await this.downloadAndAddToAIChat(content.urls);
+  }
+}
+```
+
+---
+
+**文档版本**: 1.0.0  
+**创建时间**: 2025-12-03  
+**用途**: 数据结构分析、功能实现参考、问题排查

+ 624 - 0
docs/wechat-drag-integration-guide.md

@@ -0,0 +1,624 @@
+# 企业微信拖拽集成指南
+
+## 🎯 目标
+
+实现从企业微信群聊拖拽图片和文字到侧边栏,自动上传并启动AI分析。
+
+## 📊 企业微信拖拽特性
+
+### 支持的拖拽内容
+
+| 内容类型 | dataTransfer属性 | 说明 |
+|---------|-----------------|------|
+| **图片文件** | `files` | File对象数组,包含图片二进制数据 |
+| **纯文本** | `getData('text/plain')` | 消息文本内容 |
+| **HTML** | `getData('text/html')` | 富文本消息(可能包含图片标签) |
+| **URL** | `getData('text/uri-list')` | 图片或文件的URL链接 |
+
+### 拖拽事件序列
+
+```
+1. dragenter  → 鼠标进入目标区域
+2. dragover   → 鼠标在目标区域移动(持续触发)
+3. dragleave  → 鼠标离开目标区域
+4. drop       → 释放拖拽内容
+```
+
+## 🔧 实现方案
+
+### 方案1: 增强现有拖拽区域
+
+**位置**: `space-requirements-management.component.html`
+
+```html
+<!-- 增强的拖拽上传区域 -->
+<div class="drag-drop-zone drag-drop-zone-enhanced"
+     (dragenter)="onDragEnter($event, space.id)"
+     (dragover)="onDragOver($event, space.id)"
+     (dragleave)="onDragLeave($event)"
+     (drop)="onDrop($event, space.id)"
+     [class.drag-over]="isDragOver && dragOverSpaceId === space.id"
+     [class.wechat-mode]="isWeChatEnv">
+  
+  <div class="drag-drop-content">
+    <!-- 企业微信模式提示 -->
+    @if (isWeChatEnv) {
+      <div class="wechat-hint">
+        <div class="wechat-icon">💬</div>
+        <h4>从群聊拖拽到这里</h4>
+        <p>支持图片、文字、混合内容</p>
+        <p class="ai-hint">🤖 AI将自动分析并智能归类</p>
+      </div>
+    } @else {
+      <!-- 普通模式提示 -->
+      <div class="drag-drop-icon">
+        <ion-icon name="cloud-upload-outline"></ion-icon>
+      </div>
+      <h4>拖拽参考图片到此</h4>
+      <p>或点击下方按钮上传</p>
+      <p class="drag-hint">AI将自动分析图片并智能分类</p>
+    }
+    
+    <!-- 拖拽中的视觉反馈 -->
+    @if (isDragOver && dragOverSpaceId === space.id) {
+      <div class="drag-feedback">
+        <div class="feedback-icon">📥</div>
+        <p>松开鼠标即可上传</p>
+      </div>
+    }
+  </div>
+</div>
+```
+
+### 方案2: 拖拽内容解析
+
+**位置**: `stage-requirements.component.ts`
+
+```typescript
+/**
+ * 解析企业微信拖拽数据
+ */
+private parseWeChatDragData(event: DragEvent): {
+  images: File[];
+  text: string;
+  html: string;
+  urls: string[];
+  hasContent: boolean;
+  autoAnalyze: boolean;
+} {
+  const dataTransfer = event.dataTransfer;
+  if (!dataTransfer) {
+    return {
+      images: [],
+      text: '',
+      html: '',
+      urls: [],
+      hasContent: false,
+      autoAnalyze: false
+    };
+  }
+
+  console.log('🔍 [企业微信拖拽] dataTransfer.types:', dataTransfer.types);
+  console.log('🔍 [企业微信拖拽] files.length:', dataTransfer.files.length);
+
+  // 1. 提取图片文件
+  const images: File[] = [];
+  if (dataTransfer.files && dataTransfer.files.length > 0) {
+    for (let i = 0; i < dataTransfer.files.length; i++) {
+      const file = dataTransfer.files[i];
+      if (file.type.startsWith('image/')) {
+        images.push(file);
+        console.log(`📸 [图片文件] ${file.name} (${(file.size/1024).toFixed(2)}KB)`);
+      }
+    }
+  }
+
+  // 2. 提取文本内容
+  const text = dataTransfer.getData('text/plain') || '';
+  if (text) {
+    console.log(`📝 [文本内容] ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`);
+  }
+
+  // 3. 提取HTML内容(可能包含图片标签)
+  const html = dataTransfer.getData('text/html') || '';
+  if (html) {
+    console.log(`🌐 [HTML内容] 长度: ${html.length}`);
+    // 可选:从HTML中提取图片URL
+    const imgUrls = this.extractImageUrlsFromHtml(html);
+    if (imgUrls.length > 0) {
+      console.log(`🖼️ [HTML图片] ${imgUrls.length}个`, imgUrls);
+    }
+  }
+
+  // 4. 提取URL列表
+  const urlList = dataTransfer.getData('text/uri-list') || '';
+  const urls = urlList.split('\n').filter(url => url.trim() && !url.startsWith('#'));
+  if (urls.length > 0) {
+    console.log(`🔗 [URL列表] ${urls.length}个`, urls);
+  }
+
+  // 5. 判断是否有内容
+  const hasContent = images.length > 0 || text.length > 0 || urls.length > 0;
+
+  // 6. 判断是否自动启动AI分析(有图片或有丰富文本)
+  const autoAnalyze = images.length > 0 || text.length > 50;
+
+  return {
+    images,
+    text,
+    html,
+    urls,
+    hasContent,
+    autoAnalyze
+  };
+}
+
+/**
+ * 从HTML中提取图片URL
+ */
+private extractImageUrlsFromHtml(html: string): string[] {
+  const imgRegex = /<img[^>]+src="([^"]+)"/gi;
+  const urls: string[] = [];
+  let match;
+  
+  while ((match = imgRegex.exec(html)) !== null) {
+    urls.push(match[1]);
+  }
+  
+  return urls;
+}
+
+/**
+ * 检测是否为企业微信环境
+ */
+private isWeChatWorkEnv(): boolean {
+  const ua = window.navigator.userAgent.toLowerCase();
+  return ua.includes('wxwork') || ua.includes('qywechat');
+}
+```
+
+### 方案3: 增强的拖拽处理
+
+**位置**: `stage-requirements.component.ts`
+
+```typescript
+/**
+ * 增强的拖拽处理(支持企业微信)
+ */
+async onDrop(event: DragEvent, spaceId: string): Promise<void> {
+  event.preventDefault();
+  event.stopPropagation();
+  this.isDragOver = false;
+  this.dragOverSpaceId = '';
+
+  console.log('📥 [拖拽放下] 空间ID:', spaceId);
+  console.log('📥 [拖拽放下] 环境:', this.isWeChatWorkEnv() ? '企业微信' : '普通浏览器');
+
+  // 1. 解析拖拽数据
+  const dragData = this.parseWeChatDragData(event);
+  
+  if (!dragData.hasContent) {
+    console.warn('⚠️ [拖拽放下] 未检测到有效内容');
+    return;
+  }
+
+  console.log('✅ [拖拽放下] 解析结果:', {
+    图片数量: dragData.images.length,
+    文字长度: dragData.text.length,
+    URL数量: dragData.urls.length,
+    自动分析: dragData.autoAnalyze
+  });
+
+  // 2. 处理图片文件
+  if (dragData.images.length > 0) {
+    console.log(`📤 [开始上传] ${dragData.images.length}个图片文件`);
+    await this.uploadAndAnalyzeImages(dragData.images, spaceId);
+  }
+
+  // 3. 处理URL列表(下载并上传)
+  if (dragData.urls.length > 0) {
+    console.log(`🔗 [处理URL] ${dragData.urls.length}个图片链接`);
+    await this.downloadAndUploadFromUrls(dragData.urls, spaceId);
+  }
+
+  // 4. 处理文本内容(保存到特殊需求)
+  if (dragData.text && dragData.text.length > 0) {
+    console.log(`💾 [保存文本] 到特殊需求 (${dragData.text.length}字)`);
+    const existingReq = this.getSpaceSpecialRequirements(spaceId) || '';
+    const newReq = existingReq 
+      ? `${existingReq}\n\n--- 从企业微信拖拽 ---\n${dragData.text}`
+      : dragData.text;
+    this.setSpaceSpecialRequirements(spaceId, newReq);
+  }
+
+  // 5. 可选:自动启动AI设计分析
+  if (dragData.autoAnalyze && dragData.images.length > 0) {
+    console.log('🤖 [自动分析] 启动AI设计分析');
+    setTimeout(() => {
+      this.openAIDesignDialogWithFiles(spaceId, dragData.images, dragData.text);
+    }, 1000); // 等待上传完成
+  }
+
+  this.cdr.markForCheck();
+}
+
+/**
+ * 从URL下载并上传图片
+ */
+private async downloadAndUploadFromUrls(urls: string[], spaceId: string): Promise<void> {
+  for (const url of urls) {
+    try {
+      // 检查是否为图片URL
+      if (!this.isImageUrl(url)) {
+        console.warn(`⚠️ [非图片URL] ${url}`);
+        continue;
+      }
+
+      console.log(`📥 [下载图片] ${url}`);
+      
+      // 使用fetch下载图片
+      const response = await fetch(url);
+      if (!response.ok) {
+        throw new Error(`下载失败: ${response.statusText}`);
+      }
+
+      const blob = await response.blob();
+      const fileName = this.extractFileNameFromUrl(url) || `image_${Date.now()}.jpg`;
+      const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
+
+      console.log(`✅ [下载完成] ${fileName} (${(blob.size/1024).toFixed(2)}KB)`);
+
+      // 上传图片
+      await this.uploadAndAnalyzeImages([file], spaceId);
+
+    } catch (error) {
+      console.error(`❌ [下载失败] ${url}`, error);
+    }
+  }
+}
+
+/**
+ * 判断是否为图片URL
+ */
+private isImageUrl(url: string): boolean {
+  const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
+  const lowerUrl = url.toLowerCase();
+  return imageExts.some(ext => lowerUrl.includes(ext)) || 
+         lowerUrl.includes('image') ||
+         lowerUrl.includes('photo');
+}
+
+/**
+ * 从URL提取文件名
+ */
+private extractFileNameFromUrl(url: string): string | null {
+  try {
+    const urlObj = new URL(url);
+    const pathname = urlObj.pathname;
+    const segments = pathname.split('/');
+    return segments[segments.length - 1] || null;
+  } catch {
+    return null;
+  }
+}
+
+/**
+ * 打开AI设计分析弹窗并预填内容
+ */
+private openAIDesignDialogWithFiles(spaceId: string, files: File[], text: string): void {
+  // 1. 打开弹窗
+  const space = this.projectProducts.find(p => p.id === spaceId);
+  if (!space) return;
+
+  this.openAIDesignDialog(space);
+
+  // 2. 等待组件初始化后,预填内容
+  setTimeout(() => {
+    // 假设有AI分析组件引用
+    if (this.aiDesignAnalysisComponent) {
+      this.aiDesignAnalysisComponent.aiDesignTextDescription = text;
+      this.aiDesignAnalysisComponent.processAIFiles(files);
+      // 可选:自动启动分析
+      // this.aiDesignAnalysisComponent.startAIDesignAnalysis();
+    }
+  }, 500);
+}
+```
+
+## 🎨 UI优化
+
+### 企业微信侧边栏适配
+
+**位置**: `space-requirements-management.component.scss`
+
+```scss
+// 企业微信环境检测
+.wechat-mode {
+  // 侧边栏宽度:280-400px
+  .drag-drop-zone {
+    min-height: 100px; // 减小高度
+    padding: 16px;
+    
+    .wechat-hint {
+      text-align: center;
+      
+      .wechat-icon {
+        font-size: 32px;
+        margin-bottom: 8px;
+        animation: pulse 2s infinite;
+      }
+      
+      h4 {
+        font-size: 14px;
+        margin: 8px 0;
+        color: #1a202c;
+      }
+      
+      p {
+        font-size: 12px;
+        color: #718096;
+        margin: 4px 0;
+      }
+      
+      .ai-hint {
+        color: #667eea;
+        font-weight: 600;
+        margin-top: 8px;
+      }
+    }
+  }
+  
+  // 拖拽反馈
+  &.drag-over {
+    .drag-feedback {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      
+      .feedback-icon {
+        font-size: 48px;
+        animation: bounce 0.6s infinite;
+      }
+      
+      p {
+        margin-top: 8px;
+        font-size: 14px;
+        font-weight: 600;
+        color: #667eea;
+      }
+    }
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+  50% {
+    opacity: 0.8;
+    transform: scale(1.1);
+  }
+}
+
+@keyframes bounce {
+  0%, 100% {
+    transform: translateY(0);
+  }
+  50% {
+    transform: translateY(-10px);
+  }
+}
+
+// 移动端和企业微信侧边栏
+@media (max-width: 480px) {
+  .space-requirements-card {
+    padding: 12px;
+    
+    .drag-drop-zone {
+      min-height: 80px;
+      padding: 12px;
+      
+      h4 {
+        font-size: 13px;
+      }
+      
+      p {
+        font-size: 11px;
+      }
+    }
+    
+    // 图片类型标签:2x2网格
+    .image-type-tabs-scroll {
+      grid-template-columns: repeat(2, 1fr);
+      gap: 6px;
+      
+      .tab-button {
+        padding: 8px 12px;
+        font-size: 12px;
+        min-height: 40px;
+      }
+    }
+    
+    // 图片展示:2列网格
+    .images-grid {
+      grid-template-columns: repeat(2, 1fr);
+      gap: 8px;
+    }
+  }
+}
+```
+
+## 📱 进度提示优化
+
+### 上传进度Toast
+
+**位置**: `stage-requirements.component.ts`
+
+```typescript
+/**
+ * 显示上传摘要(企业微信友好)
+ */
+private showUploadSummary(results: { success: string[], failed: Array<{name: string, error: string}> }): void {
+  const successCount = results.success.length;
+  const failedCount = results.failed.length;
+  
+  if (failedCount === 0) {
+    // 全部成功 - 使用Toast
+    if (window?.fmode?.toast?.success) {
+      window.fmode.toast.success(`✅ 成功上传 ${successCount} 个文件\n🤖 AI正在分析中...`);
+    } else {
+      console.log(`✅ 成功上传 ${successCount} 个文件,AI正在分析中...`);
+    }
+  } else if (successCount === 0) {
+    // 全部失败 - 使用Alert
+    const message = `上传失败\n\n${results.failed.map(f => `• ${f.name}\n  ${f.error}`).join('\n\n')}`;
+    window?.fmode?.alert?.(message);
+  } else {
+    // 部分成功 - 使用详细Alert
+    const message = [
+      `上传完成`,
+      ``,
+      `✅ 成功: ${successCount} 个`,
+      `❌ 失败: ${failedCount} 个`,
+      ``,
+      `失败文件:`,
+      ...results.failed.map(f => `• ${f.name}\n  ${f.error}`)
+    ].join('\n');
+    
+    window?.fmode?.alert?.(message);
+  }
+}
+```
+
+## 🧪 测试场景
+
+### 测试1: 拖拽单张图片
+```
+操作:从企业微信群聊拖拽1张图片到侧边栏
+预期:
+  ✅ 图片成功上传
+  ✅ AI自动分析并归类
+  ✅ 显示在对应类型标签页
+  ✅ Toast提示"成功上传1个文件"
+```
+
+### 测试2: 拖拽多张图片
+```
+操作:从企业微信群聊拖拽3张图片到侧边栏
+预期:
+  ✅ 3张图片全部上传
+  ✅ AI批量分析
+  ✅ 按类型自动归类
+  ✅ Toast提示"成功上传3个文件"
+```
+
+### 测试3: 拖拽图片+文字
+```
+操作:从企业微信群聊拖拽图片和文字到侧边栏
+预期:
+  ✅ 图片上传成功
+  ✅ 文字保存到特殊需求
+  ✅ 自动打开AI分析弹窗
+  ✅ 文字预填到描述框
+```
+
+### 测试4: 拖拽纯文字
+```
+操作:从企业微信群聊拖拽文字到侧边栏
+预期:
+  ✅ 文字保存到特殊需求
+  ⚠️ 不启动AI分析(无图片)
+  ✅ Toast提示"已保存到特殊需求"
+```
+
+### 测试5: 拖拽失败场景
+```
+操作:拖拽超大文件(>10MB)
+预期:
+  ❌ 上传被拒绝
+  ✅ Alert提示"文件过大"
+  ✅ 显示失败文件列表
+```
+
+## 🔍 调试技巧
+
+### 控制台日志
+```typescript
+// 在onDrop方法中添加详细日志
+console.log('🔍 [拖拽调试] dataTransfer:', {
+  types: Array.from(event.dataTransfer?.types || []),
+  files: event.dataTransfer?.files,
+  filesCount: event.dataTransfer?.files?.length || 0,
+  items: event.dataTransfer?.items,
+  itemsCount: event.dataTransfer?.items?.length || 0
+});
+
+// 遍历DataTransferItem
+if (event.dataTransfer?.items) {
+  for (let i = 0; i < event.dataTransfer.items.length; i++) {
+    const item = event.dataTransfer.items[i];
+    console.log(`🔍 [Item ${i}]`, {
+      kind: item.kind,
+      type: item.type
+    });
+  }
+}
+```
+
+### 网络面板检查
+1. 打开开发者工具 → Network
+2. 筛选:XHR/Fetch
+3. 查找上传请求:
+   - URL包含`upload`或`file`
+   - Method: POST/PUT
+   - Status: 200(成功)或 631(失败)
+4. 查看Request Payload:文件数据
+5. 查看Response:返回的URL
+
+### 企业微信调试
+```typescript
+// 检测企业微信环境
+console.log('📱 [环境检测]', {
+  userAgent: navigator.userAgent,
+  isWeChatWork: /wxwork|qywechat/i.test(navigator.userAgent),
+  platform: navigator.platform,
+  viewport: {
+    width: window.innerWidth,
+    height: window.innerHeight
+  }
+});
+```
+
+## 📝 注意事项
+
+### 1. 企业微信限制
+- **文件大小**: 建议 < 10MB
+- **文件格式**: JPG、PNG(避免HEIC、WebP)
+- **并发上传**: 建议 <= 3个文件
+- **URL访问**: 需要公网可访问(AI分析需要)
+
+### 2. 安全考虑
+- **文件名**: 自动清理中文和特殊字符
+- **URL验证**: 检查是否为HTTP/HTTPS
+- **大小检查**: 限制单文件10MB
+- **类型检查**: 只允许图片和CAD文件
+
+### 3. 性能优化
+- **异步上传**: 不阻塞UI
+- **进度反馈**: 实时显示上传进度
+- **错误重试**: 自动重试3次
+- **批量优化**: 限制并发数量
+
+### 4. 用户体验
+- **拖拽提示**: 明确告知用户可拖拽
+- **视觉反馈**: 拖拽中高亮目标区域
+- **结果提示**: Toast/Alert显示上传结果
+- **AI提示**: 告知AI正在分析
+
+---
+
+**文档版本**: 1.0.0  
+**创建时间**: 2025-12-03  
+**适用环境**: 企业微信侧边栏  
+**兼容性**: 支持Chrome、Safari、企业微信内置浏览器

+ 201 - 0
docs/wework-drag-drop-guide.md

@@ -0,0 +1,201 @@
+# 企业微信拖拽图片功能使用指南
+
+## 功能说明
+
+在"确认需求"阶段的AI设计分析区域,现已支持从企业微信群聊中直接拖拽图片到上传区域。
+
+## 使用方法
+
+### 1. 拖拽上传
+1. 打开项目详情 → 进入"确认需求"阶段
+2. 找到"AI设计分析"卡片
+3. 从企业微信群聊中选择图片
+4. 直接拖拽到"上传参考文件"区域
+5. 系统会自动处理并显示预览
+
+### 2. 支持的格式
+- **图片格式**: JPG, JPEG, PNG, GIF, WebP, BMP, TIFF
+- **单张图片大小**: 最大 50MB(超过5MB会自动压缩)
+- **数量限制**: 最多同时上传 20 张图片
+
+### 3. 多张图片上传
+- 可以一次拖拽多张图片
+- 可以多次拖拽,逐步添加图片
+- 每张图片都会显示文件名和大小
+
+## 数据结构日志
+
+当您拖拽图片时,浏览器控制台会自动打印详细的数据结构信息:
+
+### 打开控制台
+1. 按 `F12` 或 `Ctrl + Shift + I` 打开开发者工具
+2. 切换到"Console"(控制台)标签页
+3. 执行拖拽操作
+
+### 日志示例
+
+```
+🎯 ========== 拖拽事件触发 ==========
+📋 事件类型: drop
+📋 DataTransfer对象: DataTransfer {...}
+
+📦 DataTransfer详细信息:
+  - dropEffect: none
+  - effectAllowed: all
+  - files数量: 2
+  - items数量: 2
+  - types: ['Files']
+
+📄 Items详细信息:
+  Item 1: {kind: 'file', type: 'image/jpeg', webkitGetAsEntry: '支持'}
+  Item 2: {kind: 'file', type: 'image/png', webkitGetAsEntry: '支持'}
+
+📁 Files详细信息:
+  File 1: {
+    name: '客厅效果图.jpg',
+    type: 'image/jpeg',
+    size: '1024.50 KB',
+    lastModified: '2025/12/2 23:45:30'
+  }
+  File 2: {
+    name: '卧室效果图.png',
+    type: 'image/png',
+    size: '2048.75 KB',
+    lastModified: '2025/12/2 23:45:35'
+  }
+
+✅ 通过标准files API获取到文件,共 2 个
+📤 准备处理文件: 客厅效果图.jpg, 大小: 1.00MB
+🔄 将图片转换为base64格式...
+✅ 图片已转换为base64,大小: 1365.33KB
+💾 已保存图片: 客厅效果图.jpg
+📤 准备处理文件: 卧室效果图.png, 大小: 2.00MB
+🔄 将图片转换为base64格式...
+✅ 图片已转换为base64,大小: 2731.67KB
+💾 已保存图片: 卧室效果图.png
+✅ 已处理2个文件
+🎯 所有图片已转为base64,可直接进行AI分析
+```
+
+## 数据结构说明
+
+### DataTransfer 对象
+```typescript
+{
+  dropEffect: string,        // 拖放效果 (none/copy/move/link)
+  effectAllowed: string,     // 允许的效果 (all/copy/move/link)
+  files: FileList,          // 文件列表
+  items: DataTransferItemList, // 数据项列表
+  types: string[]           // 数据类型数组
+}
+```
+
+### DataTransferItem 对象
+```typescript
+{
+  kind: 'file' | 'string',  // 数据类型
+  type: string,             // MIME类型 (如 'image/jpeg')
+  webkitGetAsEntry: boolean // 是否支持目录拖拽
+}
+```
+
+### File 对象
+```typescript
+{
+  name: string,             // 文件名
+  type: string,             // MIME类型
+  size: number,             // 文件大小(字节)
+  lastModified: number      // 最后修改时间戳
+}
+```
+
+### 上传后的文件记录
+```typescript
+{
+  url: string,              // base64数据URL
+  name: string,             // 文件名
+  type: string,             // MIME类型
+  size: number,             // 原始文件大小
+  extension: string,        // 文件扩展名 (jpg/png/...)
+  isBase64: true            // 标记为base64格式
+}
+```
+
+## 企业微信特殊处理
+
+系统会尝试两种方式获取文件:
+
+### 方式1: 标准 files API
+```javascript
+const files = event.dataTransfer.files;
+```
+
+### 方式2: items API(企业微信备用方案)
+```javascript
+const items = event.dataTransfer.items;
+for (let i = 0; i < items.length; i++) {
+  if (items[i].kind === 'file') {
+    const file = items[i].getAsFile();
+  }
+}
+```
+
+## 常见问题
+
+### Q: 拖拽后没有反应?
+**A**: 请检查控制台日志:
+- 如果看到"未能从拖拽事件中提取到文件",请尝试点击上传按钮
+- 确认拖拽的是图片文件而非文件夹
+
+### Q: 为什么有些图片拖拽失败?
+**A**: 可能的原因:
+- 文件格式不支持(只支持图片格式)
+- 文件超过50MB限制
+- 网络连接问题
+
+### Q: 如何查看详细的错误信息?
+**A**: 
+1. 打开浏览器控制台(F12)
+2. 查看红色错误信息
+3. 截图控制台日志,联系技术支持
+
+## 技术实现
+
+### 文件处理流程
+```
+拖拽事件触发
+  ↓
+提取文件对象(files/items API)
+  ↓
+验证文件格式和大小
+  ↓
+大文件自动压缩(>5MB)
+  ↓
+转换为base64格式
+  ↓
+保存到内存数组
+  ↓
+显示预览
+  ↓
+可进行AI分析
+```
+
+### 压缩策略
+- **5MB以下**: 直接使用
+- **5MB-50MB**: 自动压缩(质量0.8,最大宽度3000px)
+- **50MB以上**: 拒绝上传,提示用户使用专业工具压缩
+
+## 相关文件
+
+- **组件**: `stage-requirements.component.ts`
+- **模板**: `stage-requirements.component.html`
+- **服务**: `design-analysis-ai.service.ts`
+
+## 更新日志
+
+**2025-12-02**
+- ✅ 支持企业微信拖拽图片
+- ✅ 添加详细的数据结构日志
+- ✅ 支持多张图片同时拖拽
+- ✅ 自动压缩大文件
+- ✅ 实时显示上传进度

+ 335 - 0
src/modules/project/pages/project-detail/stages/components/space-requirements-management/space-requirements-management.component.html

@@ -0,0 +1,335 @@
+<!-- 空间需求管理 - 新布局 -->
+<div class="card space-requirements-card">
+  <div class="card-header">
+    <h3 class="card-title">
+      <span class="icon">🏠</span>
+      空间需求管理
+    </h3>
+    <p class="card-subtitle">按空间管理参考图片、CAD文件和特殊需求</p>
+  </div>
+  <div class="card-content">
+    <div class="spaces-container">
+      @for (space of projectProducts; track space.id) {
+        <div class="space-item" [class.expanded]="isSpaceExpanded(space.id)">
+          <!-- 空间头部 - 折叠时显示 -->
+          <div class="space-header" (click)="toggleSpaceExpansion(space.id)">
+            <div class="space-name-section">
+              {{ getSpaceDisplayName(space) }}
+              <span class="reference-count-badge">
+                参考 {{ getTotalSpaceFileCount(space.id) }}
+              </span>
+            </div>
+            
+            <!-- 特殊需求显示 -->
+            @if (getSpaceSpecialRequirements(space.id)) {
+              <div class="special-requirements-box">
+                <span class="requirements-label">特殊要求:</span>
+                <span class="requirements-text">{{ getSpaceSpecialRequirements(space.id) | slice:0:30 }}{{ getSpaceSpecialRequirements(space.id).length > 30 ? '...' : '' }}</span>
+              </div>
+            }
+            
+            <!--操作按钮 -->
+            <div class="header-actions">
+              @if (canEdit) {
+                <button class="btn-icon-small btn-ai" title="AI设计分析" (click)="openAIDesignDialog(space, $event)">
+                  <span class="icon-text">🤖</span>
+                </button>
+                <button class="btn-icon-small btn-edit" title="编辑特殊要求" (click)="toggleSpaceExpansion(space.id); $event.stopPropagation()">
+                  <span class="icon-text">✏️</span>
+                </button>
+              }
+            </div>
+            
+            <!-- 展开/收起图标 -->
+            <div class="expand-icon" [class.expanded]="isSpaceExpanded(space.id)">
+              <svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
+                <path d="M7 10l5 5 5-5z"/>
+              </svg>
+            </div>
+          </div>
+
+          <!-- 空间内容 - 展开时显示 -->
+          @if (isSpaceExpanded(space.id)) {
+            <div class="space-content">
+              <!-- 拖拽上传区域 -->
+              <div class="drag-drop-zone"
+                   (dragover)="onDragOver($event, space.id)"
+                   (dragleave)="onDragLeave($event)"
+                   (drop)="onDrop($event, space.id)"
+                   [class.drag-over]="isDragOver && dragOverSpaceId === space.id">
+                <div class="drag-drop-content">
+                  <div class="drag-drop-icon">
+                    <ion-icon name="cloud-upload-outline"></ion-icon>
+                  </div>
+                  <h4>拖拽参考图片到此</h4>
+                  <p>或点击下方按钮上传</p>
+                  <p class="drag-hint">AI将自动分析图片并智能分类</p>
+                </div>
+              </div>
+
+              <!-- 图片类型导航标签 - 横向滚动布局 -->
+              <div class="image-type-tabs-wrapper">
+                <div class="image-type-tabs-scroll">
+                  @for (type of imageTypes; track type.id) {
+                    <button
+                      class="tab-button"
+                      [class.active]="activeImageTab[space.id] === type.id"
+                      (click)="selectImageTab(space.id, type.id)">
+                      <span class="tab-label">{{ type.name }}</span>
+                      @if (getImageCountByType(space.id, type.id) > 0) {
+                        <span class="tab-badge">{{ getImageCountByType(space.id, type.id) }}</span>
+                      }
+                    </button>
+                  }
+                </div>
+              </div>
+
+              <!-- 图片展示区域 -->
+              <div class="images-section">
+                @if (activeImageTab[space.id] === 'all') {
+                  <!-- 全部图片和CAD文件 -->
+                  <div class="section-header">
+                    <h5>所有参考文件</h5>
+                    @if (canEdit) {
+                      <input
+                        type="file"
+                        accept="image/*"
+                        multiple
+                        (change)="onFileUpload($event, space.id)"
+                        [disabled]="uploading"
+                        hidden
+                        [id]="'spaceImageInput_' + space.id" />
+                      <button
+                        class="btn btn-sm btn-outline"
+                        (click)="triggerFileClick('spaceImageInput_' + space.id)"
+                        [disabled]="uploading">
+                        <ion-icon name="add"></ion-icon>
+                        上传参考图
+                      </button>
+                    }
+                  </div>
+                  <div class="section-content">
+                    @if (getSpaceReferenceImages(space.id).length > 0) {
+                      <div class="images-grid">
+                        @for (image of getSpaceReferenceImages(space.id); track image.id) {
+                          <div class="image-item">
+                            <img [src]="image.url" [alt]="image.name" (click)="onImageView(image.id)" />
+                            <div class="image-overlay">
+                              <div class="overlay-top">
+                                <span class="badge" [class]="getImageTypeBadgeClass(image.type)">
+                                  {{ getImageTypeLabel(image.type) }}
+                                </span>
+                                @if (hasImageAnalysis(image.id)) {
+                                  <span class="badge badge-success">
+                                    <ion-icon name="sparkles"></ion-icon>
+                                  </span>
+                                }
+                              </div>
+                              <div class="overlay-actions">
+                                <button
+                                  class="btn-icon btn-primary"
+                                  (click)="onImageView(image.id, $event)"
+                                  title="查看色彩分析">
+                                  <ion-icon name="color-palette"></ion-icon>
+                                </button>
+                                @if (canEdit) {
+                                  <button
+                                    class="btn-icon btn-danger"
+                                    (click)="onImageDelete(image.id, $event)">
+                                    <ion-icon name="trash"></ion-icon>
+                                  </button>
+                                }
+                              </div>
+                            </div>
+                          </div>
+                        }
+                      </div>
+                    } @else {
+                      <div class="empty-state">
+                        <ion-icon name="image-outline"></ion-icon>
+                        <p>暂无参考图片</p>
+                      </div>
+                    }
+                  </div>
+
+                  <!-- CAD文件显示 -->
+                  @if (getSpaceCADFiles(space.id).length > 0) {
+                    <div class="section-header">
+                      <h5>CAD文件</h5>
+                    </div>
+                    <div class="section-content">
+                      <div class="cad-files-list">
+                        @for (cadFile of getSpaceCADFiles(space.id); track cadFile.id) {
+                          <div class="cad-file-item">
+                            <div class="cad-icon">
+                              <ion-icon name="document"></ion-icon>
+                            </div>
+                            <div class="cad-info">
+                              <div class="cad-name">{{ cadFile.name }}</div>
+                              <div class="cad-meta">
+                                @if (hasImageAnalysis(cadFile.id)) {
+                                  <span class="badge badge-success">
+                                    <ion-icon name="sparkles"></ion-icon>
+                                    已分析
+                                  </span>
+                                }
+                              </div>
+                            </div>
+                            @if (canEdit) {
+                              <button
+                                class="btn-icon btn-danger"
+                                (click)="onCADDelete(cadFile.id, $event)">
+                                <ion-icon name="trash"></ion-icon>
+                              </button>
+                            }
+                          </div>
+                        }
+                      </div>
+                    </div>
+                  }
+
+                  <!-- 用户需求备注 -->
+                  <div class="section-divider"></div>
+                  <div class="user-notes-section">
+                    <div class="section-header">
+                      <h5>用户需求备注</h5>
+                    </div>
+                    <div class="section-content">
+                      <textarea
+                        class="form-textarea"
+                        [(ngModel)]="spaceSpecialRequirements[space.id]"
+                        (ngModelChange)="onSpecialRequirementsChange(space.id, $event)"
+                        [disabled]="!canEdit"
+                        rows="4"
+                        [placeholder]="'描述' + getSpaceDisplayName(space) + '的特殊要求和注意事项'"></textarea>
+                    </div>
+                  </div>
+                } @else if (activeImageTab[space.id] === 'cad') {
+                  <!-- CAD文件 -->
+                  <div class="section-header">
+                    <h5>CAD文件</h5>
+                    @if (canEdit) {
+                      <input
+                        type="file"
+                        accept=".dwg,.dxf,.pdf"
+                        multiple
+                        (change)="onFileUpload($event, space.id, 'cad')"
+                        [disabled]="uploading"
+                        hidden
+                        [id]="'spaceCADInput_' + space.id" />
+                      <button
+                        class="btn btn-sm btn-outline"
+                        (click)="triggerFileClick('spaceCADInput_' + space.id)"
+                        [disabled]="uploading">
+                        <ion-icon name="add"></ion-icon>
+                        上传CAD
+                      </button>
+                    }
+                  </div>
+                  <div class="section-content">
+                    @if (getSpaceCADFiles(space.id).length > 0) {
+                      <div class="file-list">
+                        @for (file of getSpaceCADFiles(space.id); track file.id) {
+                          <div class="file-item">
+                            <ion-icon name="document-text" class="file-icon"></ion-icon>
+                            <div class="file-info">
+                              <h6>{{ file.name }}</h6>
+                              <p>{{ formatFileSize(file.size) }} · {{ file.uploadTime | date:'MM-dd HH:mm' }}</p>
+                              @if (hasImageAnalysis(file.id)) {
+                                <span class="badge badge-success">
+                                  <ion-icon name="sparkles"></ion-icon>
+                                  已分析
+                                </span>
+                              }
+                            </div>
+                            @if (canEdit) {
+                              <button
+                                class="btn-icon btn-danger"
+                                (click)="onCADDelete(file.id)">
+                                <ion-icon name="trash"></ion-icon>
+                              </button>
+                            }
+                          </div>
+                        }
+                      </div>
+                    } @else {
+                      <div class="empty-state">
+                        <ion-icon name="document-outline"></ion-icon>
+                        <p>暂无CAD文件</p>
+                      </div>
+                    }
+                  </div>
+                } @else {
+                  <!-- 按类型过滤的图片 -->
+                  <div class="section-header">
+                    <h5>{{ getImageTypeLabel(activeImageTab[space.id]) }}图片</h5>
+                    @if (canEdit) {
+                      <input
+                        type="file"
+                        accept="image/*"
+                        multiple
+                        (change)="onFileUpload($event, space.id, activeImageTab[space.id])"
+                        [disabled]="uploading"
+                        hidden
+                        [id]="'spaceImageInput_' + space.id + '_' + activeImageTab[space.id]" />
+                      <button
+                        class="btn btn-sm btn-outline"
+                        (click)="triggerFileClick('spaceImageInput_' + space.id + '_' + activeImageTab[space.id])"
+                        [disabled]="uploading">
+                        <ion-icon name="add"></ion-icon>
+                        上传{{ getImageTypeLabel(activeImageTab[space.id]) }}图
+                      </button>
+                    }
+                  </div>
+                  <div class="section-content">
+                    @if (getImagesByType(space.id, activeImageTab[space.id]).length > 0) {
+                      <div class="images-grid">
+                        @for (image of getImagesByType(space.id, activeImageTab[space.id]); track image.id) {
+                          <div class="image-item">
+                            <img [src]="image.url" [alt]="image.name" (click)="onImageView(image.id)" />
+                            <div class="image-overlay">
+                              <div class="overlay-top">
+                                <span class="badge" [class]="getImageTypeBadgeClass(image.type)">
+                                  {{ getImageTypeLabel(image.type) }}
+                                </span>
+                                @if (hasImageAnalysis(image.id)) {
+                                  <span class="badge badge-success">
+                                    <ion-icon name="sparkles"></ion-icon>
+                                  </span>
+                                }
+                              </div>
+                              <div class="overlay-actions">
+                                <button
+                                  class="btn-icon btn-primary"
+                                  (click)="onImageView(image.id, $event)"
+                                  title="查看色彩分析">
+                                  <ion-icon name="color-palette"></ion-icon>
+                                </button>
+                                @if (canEdit) {
+                                  <button
+                                    class="btn-icon btn-danger"
+                                    (click)="onImageDelete(image.id, $event)">
+                                    <ion-icon name="trash"></ion-icon>
+                                  </button>
+                                }
+                              </div>
+                            </div>
+                          </div>
+                        }
+                      </div>
+                    } @else {
+                      <div class="empty-state">
+                        <ion-icon name="image-outline"></ion-icon>
+                        <p>暂无{{ getImageTypeLabel(activeImageTab[space.id]) }}图片</p>
+                      </div>
+                    }
+                  </div>
+                }
+              </div>
+            </div>
+          }
+        </div>
+      }
+    </div>
+  </div>
+</div>

+ 779 - 0
src/modules/project/pages/project-detail/stages/components/space-requirements-management/space-requirements-management.component.scss

@@ -0,0 +1,779 @@
+// 空间需求管理卡片样式
+.space-requirements-card {
+  .card-header {
+    border-bottom: 2px solid var(--light-shade);
+    
+    .card-title {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin: 0 0 8px;
+      font-size: 18px;
+      font-weight: 600;
+      color: var(--dark-color);
+
+      .icon {
+        font-size: 24px;
+      }
+    }
+
+    .card-subtitle {
+      margin: 0;
+      font-size: 13px;
+      color: var(--medium-color);
+    }
+  }
+
+  .card-content {
+    padding: 20px;
+  }
+
+  .spaces-container {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .space-item {
+      background: white;
+      border-radius: 12px;
+      border: 2px solid var(--light-shade);
+      overflow: hidden;
+      transition: all 0.3s ease;
+
+      &:hover {
+        border-color: var(--primary-color);
+        box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.15);
+      }
+
+      &.expanded {
+        box-shadow: 0 8px 24px rgba(var(--primary-rgb), 0.2);
+      }
+
+      // 空间头部
+      .space-header {
+        display: grid;
+        grid-template-columns: auto 1fr auto auto;
+        align-items: center;
+        gap: 16px;
+        padding: 16px 20px;
+        background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+        border-bottom: 1px solid var(--light-shade);
+        cursor: pointer;
+        transition: all 0.3s ease;
+        min-height: 60px;
+
+        .space-name-section {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          font-size: 16px;
+          font-weight: 600;
+          color: var(--dark-color);
+          white-space: nowrap;
+
+          .reference-count-badge {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            padding: 4px 12px;
+            background: linear-gradient(135deg, #e0e7ff 0%, #ddd6fe 100%);
+            color: var(--primary-color);
+            font-size: 12px;
+            font-weight: 600;
+            border-radius: 12px;
+            box-shadow: 0 2px 6px rgba(var(--primary-rgb), 0.15);
+            flex-shrink: 0;
+          }
+        }
+
+        .special-requirements-box {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          padding: 6px 10px;
+          background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(245, 158, 11, 0.08));
+          border-left: 3px solid var(--warning-color);
+          border-radius: 4px;
+          font-size: 12px;
+          overflow: hidden;
+          min-width: 0;
+          flex: 1;
+          max-width: 180px;
+
+          .requirements-label {
+            font-weight: 600;
+            color: var(--warning-color);
+            white-space: nowrap;
+            flex-shrink: 0;
+            font-size: 11px;
+          }
+
+          .requirements-text {
+            color: var(--medium-color);
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            flex: 1;
+            min-width: 0;
+          }
+        }
+
+        .header-actions {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          flex-shrink: 0;
+
+          .btn-icon-small {
+            width: 30px;
+            height: 30px;
+            padding: 4px;
+            background: rgba(var(--primary-rgb), 0.1);
+            color: var(--primary-color);
+            border: 1px solid rgba(var(--primary-rgb), 0.2);
+            border-radius: 6px;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            flex-shrink: 0;
+
+            .icon-text {
+              font-size: 15px;
+              line-height: 1;
+              display: block;
+            }
+
+            ion-icon {
+              font-size: 15px;
+            }
+
+            &:hover {
+              background: rgba(var(--primary-rgb), 0.2);
+              border-color: var(--primary-color);
+              transform: scale(1.05);
+            }
+
+            &.btn-edit:hover {
+              background: var(--primary-color);
+              color: white;
+            }
+
+            &.btn-ai:hover {
+              background: var(--tertiary-color);
+              color: white;
+              border-color: var(--tertiary-color);
+            }
+          }
+        }
+
+        .expand-icon {
+          flex-shrink: 0;
+          width: 28px;
+          height: 28px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+          border-radius: 50%;
+          transition: all 0.3s ease;
+          color: var(--medium-color);
+
+          svg {
+            width: 20px;
+            height: 20px;
+            transition: transform 0.3s ease;
+          }
+
+          &.expanded svg {
+            transform: rotate(180deg);
+          }
+        }
+
+        &:hover {
+          background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
+
+          .expand-icon {
+            background: linear-gradient(135deg, var(--primary-color), #764ba2);
+            color: white;
+          }
+        }
+      }
+
+      // 空间内容
+      .space-content {
+        padding: 20px;
+        animation: slideDown 0.3s ease-out;
+
+        // 拖拽上传区域
+        .drag-drop-zone {
+          margin-bottom: 24px;
+          padding: 32px 20px;
+          border: 2px dashed var(--light-shade);
+          border-radius: 12px;
+          background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.02), rgba(12, 209, 232, 0.02));
+          transition: all 0.3s ease;
+          cursor: pointer;
+
+          &.drag-over {
+            border-color: var(--primary-color);
+            background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(12, 209, 232, 0.1));
+            box-shadow: 0 8px 24px rgba(var(--primary-rgb), 0.15);
+
+            .drag-drop-content {
+              transform: scale(1.05);
+            }
+          }
+
+          .drag-drop-content {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            text-align: center;
+            transition: transform 0.3s ease;
+
+            .drag-drop-icon {
+              font-size: 48px;
+              color: var(--primary-color);
+              margin-bottom: 12px;
+              opacity: 0.8;
+            }
+
+            h4 {
+              margin: 0 0 8px;
+              font-size: 16px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
+
+            p {
+              margin: 0;
+              font-size: 13px;
+              color: var(--medium-color);
+
+              &.drag-hint {
+                margin-top: 8px;
+                font-size: 12px;
+                color: var(--primary-color);
+                font-weight: 500;
+              }
+            }
+          }
+        }
+
+        // 图片类型标签导航 - 响应式网格布局
+        .image-type-tabs-wrapper {
+          position: relative;
+          margin-bottom: 20px;
+          padding-bottom: 12px;
+          border-bottom: 2px solid var(--light-shade);
+        }
+
+        .image-type-tabs-scroll {
+          display: grid;
+          grid-template-columns: repeat(2, 1fr); // 默认2列(移动端)
+          gap: 8px;
+          padding: 2px; // 防止阴影被裁剪
+
+          // 桌面端:4列一行
+          @media (min-width: 768px) {
+            grid-template-columns: repeat(4, 1fr);
+          }
+
+          // 平板端:保持2列
+          @media (min-width: 480px) and (max-width: 767px) {
+            grid-template-columns: repeat(2, 1fr);
+          }
+
+          .tab-button {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 6px;
+            padding: 10px 16px; // 稍微减小padding以适应小屏幕
+            background: white;
+            border: 2px solid #e2e8f0;
+            border-radius: 12px;
+            font-size: 13px; // 稍微减小字体
+            font-weight: 600;
+            color: var(--medium-color);
+            cursor: pointer;
+            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+            white-space: nowrap;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+            min-height: 44px; // 确保触摸目标足够大
+
+            .tab-label {
+              flex-shrink: 0;
+            }
+
+            .tab-badge {
+              display: inline-flex;
+              align-items: center;
+              justify-content: center;
+              min-width: 20px;
+              height: 20px;
+              padding: 0 6px;
+              background: #e2e8f0;
+              border-radius: 10px;
+              font-size: 10px;
+              font-weight: 700;
+              color: #64748b;
+            }
+
+            &:hover:not(.active) {
+              border-color: #cbd5e1;
+              background: #f8fafc;
+              transform: translateY(-2px);
+              box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+            }
+
+            &.active {
+              background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+              border-color: transparent;
+              color: white;
+              box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
+              transform: translateY(-2px);
+
+              .tab-badge {
+                background: rgba(255, 255, 255, 0.25);
+                color: white;
+              }
+            }
+
+            &:active {
+              transform: translateY(0);
+            }
+
+            // 移动端优化
+            @media (max-width: 479px) {
+              padding: 8px 12px;
+              font-size: 12px;
+              min-height: 40px;
+
+              .tab-badge {
+                min-width: 18px;
+                height: 18px;
+                font-size: 9px;
+              }
+            }
+          }
+        }
+
+        // 图片展示区域
+        .images-section {
+          .section-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 16px;
+            padding-bottom: 12px;
+            border-bottom: 1px solid var(--light-shade);
+
+            h5 {
+              margin: 0;
+              font-size: 15px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
+
+            .btn {
+              padding: 8px 16px;
+              font-size: 12px;
+            }
+          }
+
+          .section-content {
+            margin-bottom: 16px;
+          }
+
+          .images-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+            gap: 12px;
+
+            .image-item {
+              position: relative;
+              aspect-ratio: 1;
+              border-radius: 8px;
+              overflow: hidden;
+              cursor: pointer;
+              box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+              transition: all 0.3s ease;
+              border: 2px solid transparent;
+
+              &:hover {
+                transform: translateY(-4px);
+                box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
+                border-color: var(--primary-color);
+
+                img {
+                  transform: scale(1.05);
+                }
+
+                .image-overlay {
+                  opacity: 1;
+                }
+              }
+
+              img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                transition: transform 0.3s ease;
+              }
+
+              .image-overlay {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.7));
+                display: flex;
+                flex-direction: column;
+                justify-content: space-between;
+                padding: 8px;
+                opacity: 0;
+                transition: opacity 0.3s ease;
+
+                .overlay-top {
+                  display: flex;
+                  flex-wrap: wrap;
+                  gap: 4px;
+                  align-items: flex-start;
+
+                  .badge {
+                    height: fit-content;
+                    font-size: 10px;
+                    padding: 4px 8px;
+                    background: rgba(0, 0, 0, 0.6);
+                    color: white;
+                    border-radius: 4px;
+
+                    &.badge-primary {
+                      background: rgba(16, 185, 129, 0.9);
+                    }
+
+                    &.badge-secondary {
+                      background: rgba(59, 130, 246, 0.9);
+                    }
+
+                    &.badge-success {
+                      background: rgba(34, 197, 94, 0.9);
+                    }
+                  }
+                }
+
+                .overlay-actions {
+                  display: flex;
+                  gap: 6px;
+                  justify-content: flex-end;
+
+                  .btn-icon {
+                    width: 32px;
+                    height: 32px;
+                    padding: 4px;
+                    background: rgba(var(--primary-rgb), 0.9);
+                    color: white;
+                    border: none;
+                    border-radius: 6px;
+                    cursor: pointer;
+                    transition: all 0.2s ease;
+
+                    &:hover {
+                      background: var(--primary-color);
+                      transform: scale(1.1);
+                    }
+
+                    &.btn-danger {
+                      background: rgba(239, 68, 68, 0.9);
+
+                      &:hover {
+                        background: #ef4444;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+
+          .file-list {
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+
+            .file-item {
+              display: flex;
+              align-items: center;
+              gap: 12px;
+              padding: 12px;
+              background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+              border: 1px solid var(--light-shade);
+              border-radius: 8px;
+              transition: all 0.3s ease;
+
+              &:hover {
+                border-color: var(--primary-color);
+                background: rgba(var(--primary-rgb), 0.03);
+              }
+
+              .file-icon {
+                font-size: 32px;
+                color: var(--primary-color);
+                flex-shrink: 0;
+              }
+
+              .file-info {
+                flex: 1;
+                min-width: 0;
+
+                h6 {
+                  margin: 0 0 4px;
+                  font-size: 14px;
+                  font-weight: 600;
+                  color: var(--dark-color);
+                  overflow: hidden;
+                  text-overflow: ellipsis;
+                  white-space: nowrap;
+                }
+
+                p {
+                  margin: 0;
+                  font-size: 12px;
+                  color: var(--medium-color);
+                }
+
+                .badge {
+                  display: inline-flex;
+                  align-items: center;
+                  gap: 4px;
+                  margin-top: 4px;
+                  padding: 4px 8px;
+                  background: var(--success-color);
+                  color: white;
+                  font-size: 11px;
+                  border-radius: 4px;
+                }
+              }
+
+              .btn-icon {
+                flex-shrink: 0;
+                width: 32px;
+                height: 32px;
+                padding: 4px;
+                background: rgba(239, 68, 68, 0.1);
+                color: var(--danger-color);
+                border: none;
+                border-radius: 6px;
+                cursor: pointer;
+                transition: all 0.2s ease;
+
+                &:hover {
+                  background: rgba(239, 68, 68, 0.2);
+                  transform: scale(1.1);
+                }
+              }
+            }
+          }
+
+          .empty-state {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            padding: 40px 20px;
+            text-align: center;
+
+            ion-icon {
+              font-size: 48px;
+              color: var(--medium-color);
+              margin-bottom: 12px;
+              opacity: 0.5;
+            }
+
+            p {
+              margin: 0;
+              color: var(--medium-color);
+              font-size: 13px;
+            }
+          }
+
+          .section-divider {
+            height: 1px;
+            background: var(--light-shade);
+            margin: 20px 0;
+          }
+
+          // CAD文件列表
+          .cad-files-list {
+            display: flex;
+            flex-direction: column;
+            gap: 12px;
+
+            .cad-file-item {
+              display: flex;
+              align-items: center;
+              gap: 12px;
+              padding: 12px;
+              background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+              border: 1px solid var(--light-shade);
+              border-radius: 8px;
+              transition: all 0.2s ease;
+
+              &:hover {
+                border-color: var(--primary-color);
+                background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+              }
+
+              .cad-icon {
+                flex-shrink: 0;
+                width: 40px;
+                height: 40px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: linear-gradient(135deg, #e0e7ff 0%, #ddd6fe 100%);
+                border-radius: 6px;
+                color: var(--primary-color);
+                font-size: 20px;
+              }
+
+              .cad-info {
+                flex: 1;
+                min-width: 0;
+
+                .cad-name {
+                  font-size: 13px;
+                  font-weight: 600;
+                  color: var(--dark-color);
+                  overflow: hidden;
+                  text-overflow: ellipsis;
+                  white-space: nowrap;
+                }
+
+                .cad-meta {
+                  display: flex;
+                  align-items: center;
+                  gap: 8px;
+                  margin-top: 4px;
+
+                  .badge {
+                    display: inline-flex;
+                    align-items: center;
+                    gap: 4px;
+                    padding: 2px 8px;
+                    font-size: 11px;
+                    font-weight: 600;
+                    border-radius: 4px;
+                    background: #d4edda;
+                    color: #155724;
+
+                    ion-icon {
+                      font-size: 12px;
+                    }
+                  }
+                }
+              }
+
+              .btn-icon {
+                flex-shrink: 0;
+                width: 32px;
+                height: 32px;
+                padding: 4px;
+                background: transparent;
+                border: 1px solid var(--light-shade);
+                border-radius: 6px;
+                cursor: pointer;
+                transition: all 0.2s ease;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+
+                &.btn-danger {
+                  color: #dc3545;
+
+                  &:hover {
+                    background: #dc3545;
+                    color: white;
+                    border-color: #dc3545;
+                  }
+                }
+              }
+            }
+          }
+
+          // 用户需求备注
+          .user-notes-section {
+            .form-textarea {
+              width: 100%;
+              padding: 12px;
+              border: 2px solid var(--light-shade);
+              border-radius: 8px;
+              font-size: 13px;
+              line-height: 1.6;
+              resize: vertical;
+              transition: all 0.3s ease;
+              font-family: inherit;
+
+              &:focus {
+                outline: none;
+                border-color: var(--primary-color);
+                box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
+              }
+
+              &:disabled {
+                background: #f5f5f5;
+                cursor: not-allowed;
+              }
+
+              &::placeholder {
+                color: var(--medium-color);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 动画
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .space-requirements-card {
+    .space-header {
+      padding: 16px 20px;
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+
+      .special-requirements-box {
+        max-width: 100%;
+      }
+
+      .header-actions {
+        width: 100%;
+        justify-content: flex-end;
+      }
+    }
+
+    .images-grid {
+      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important;
+    }
+  }
+}

+ 277 - 0
src/modules/project/pages/project-detail/stages/components/space-requirements-management/space-requirements-management.component.ts

@@ -0,0 +1,277 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonIcon } from '@ionic/angular/standalone';
+
+/**
+ * 空间需求管理组件
+ * 用于管理项目空间的参考图片、CAD文件和特殊需求
+ */
+@Component({
+  selector: 'app-space-requirements-management',
+  standalone: true,
+  imports: [CommonModule, FormsModule, IonIcon],
+  templateUrl: './space-requirements-management.component.html',
+  styleUrl: './space-requirements-management.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SpaceRequirementsManagementComponent {
+  @Input() projectProducts: any[] = []; // 项目空间列表
+  @Input() canEdit: boolean = true; // 是否可编辑
+  @Input() uploading: boolean = false; // 上传状态
+  @Input() spaceSpecialRequirements: { [spaceId: string]: string } = {}; // 空间特殊需求
+  @Input() expandedSpaces: Set<string> = new Set(); // 展开的空间ID集合
+  @Input() activeImageTab: { [spaceId: string]: string } = {}; // 当前激活的图片标签
+  @Input() isDragOver: boolean = false; // 拖拽状态
+  @Input() dragOverSpaceId: string | null = null; // 拖拽覆盖的空间ID
+  
+  // 输出事件
+  @Output() spaceExpanded = new EventEmitter<string>(); // 空间展开/收起
+  @Output() imageTabChanged = new EventEmitter<{spaceId: string, tabId: string}>(); // 图片标签切换
+  @Output() fileUpload = new EventEmitter<{event: Event, spaceId: string, type?: string}>(); // 文件上传
+  @Output() imageDelete = new EventEmitter<string>(); // 删除图片
+  @Output() cadDelete = new EventEmitter<string>(); // 删除CAD
+  @Output() imageView = new EventEmitter<string>(); // 查看图片
+  @Output() specialRequirementsChange = new EventEmitter<{spaceId: string, value: string}>(); // 特殊需求变更
+  @Output() aiDesignOpen = new EventEmitter<any>(); // 打开AI设计分析
+  @Output() dragOverChange = new EventEmitter<{event: DragEvent, spaceId: string}>(); // 拖拽悬停
+  @Output() dragLeaveChange = new EventEmitter<DragEvent>(); // 拖拽离开
+  @Output() dropChange = new EventEmitter<{event: DragEvent, spaceId: string}>(); // 拖拽放下
+
+  // 图片类型定义
+  imageTypes = [
+    { id: 'all', name: '全部' },
+    { id: 'soft_decor', name: '软装' },
+    { id: 'hard_decor', name: '硬装' },
+    { id: 'cad', name: 'CAD' }
+  ];
+
+  constructor(private cdr: ChangeDetectorRef) {}
+
+  /**
+   * 获取空间显示名称
+   */
+  getSpaceDisplayName(space: any): string {
+    return space.name || this.getSpaceTypeName(space.type);
+  }
+
+  /**
+   * 获取空间类型名称
+   */
+  getSpaceTypeName(type: string): string {
+    const nameMap: Record<string, string> = {
+      'living_room': '客厅',
+      'bedroom': '卧室',
+      'kitchen': '厨房',
+      'bathroom': '卫生间',
+      'dining_room': '餐厅',
+      'study': '书房',
+      'balcony': '阳台',
+      'hallway': '走廊',
+      'other': '其他'
+    };
+    return nameMap[type] || type;
+  }
+
+  /**
+   * 检查空间是否展开
+   */
+  isSpaceExpanded(spaceId: string): boolean {
+    return this.expandedSpaces.has(spaceId);
+  }
+
+  /**
+   * 切换空间展开状态
+   */
+  toggleSpaceExpansion(spaceId: string): void {
+    this.spaceExpanded.emit(spaceId);
+  }
+
+  /**
+   * 选择图片标签
+   */
+  selectImageTab(spaceId: string, tabId: string): void {
+    this.imageTabChanged.emit({ spaceId, tabId });
+  }
+
+  /**
+   * 触发文件选择
+   */
+  triggerFileClick(inputId: string): void {
+    const element = document.getElementById(inputId) as HTMLInputElement;
+    if (element) {
+      element.click();
+    }
+  }
+
+  /**
+   * 处理文件上传
+   */
+  onFileUpload(event: Event, spaceId: string, type?: string): void {
+    this.fileUpload.emit({ event, spaceId, type });
+  }
+
+  /**
+   * 删除图片
+   */
+  onImageDelete(imageId: string, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.imageDelete.emit(imageId);
+  }
+
+  /**
+   * 删除CAD文件
+   */
+  onCADDelete(cadId: string, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.cadDelete.emit(cadId);
+  }
+
+  /**
+   * 查看图片分析
+   */
+  onImageView(imageId: string, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.imageView.emit(imageId);
+  }
+
+  /**
+   * 特殊需求变更
+   */
+  onSpecialRequirementsChange(spaceId: string, value: string): void {
+    this.specialRequirementsChange.emit({ spaceId, value });
+  }
+
+  /**
+   * 打开AI设计分析
+   */
+  openAIDesignDialog(space: any, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.aiDesignOpen.emit(space);
+  }
+
+  /**
+   * 拖拽悬停
+   */
+  onDragOver(event: DragEvent, spaceId: string): void {
+    this.dragOverChange.emit({ event, spaceId });
+  }
+
+  /**
+   * 拖拽离开
+   */
+  onDragLeave(event: DragEvent): void {
+    this.dragLeaveChange.emit(event);
+  }
+
+  /**
+   * 拖拽放下
+   */
+  onDrop(event: DragEvent, spaceId: string): void {
+    this.dropChange.emit({ event, spaceId });
+  }
+
+  /**
+   * 格式化文件大小
+   */
+  formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 B';
+    const k = 1024;
+    const sizes = ['B', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+  }
+
+  /**
+   * 获取图片类型标签
+   */
+  getImageTypeLabel(type: string): string {
+    const typeMap: { [key: string]: string } = {
+      'all': '全部',
+      'soft_decor': '软装',
+      'hard_decor': '硬装',
+      'rendering': '渲染图',
+      'white_model': '白模',
+      'cad': 'CAD'
+    };
+    return typeMap[type] || type;
+  }
+
+  /**
+   * 获取图片类型徽章样式
+   */
+  getImageTypeBadgeClass(type: string): string {
+    const classMap: { [key: string]: string } = {
+      'soft_decor': 'badge-primary',
+      'hard_decor': 'badge-secondary',
+      'rendering': 'badge-success',
+      'white_model': 'badge-warning',
+      'cad': 'badge-info'
+    };
+    return classMap[type] || 'badge-default';
+  }
+
+  // 以下方法需要父组件通过@Input传入数据或通过服务注入
+
+  /**
+   * 获取空间文件总数
+   * 需要父组件提供
+   */
+  @Input() getTotalSpaceFileCount!: (spaceId: string) => number;
+
+  /**
+   * 获取空间特殊需求
+   * 需要父组件提供
+   */
+  @Input() getSpaceSpecialRequirements!: (spaceId: string) => string;
+
+  /**
+   * 获取空间参考图片
+   * 需要父组件提供
+   */
+  @Input() getSpaceReferenceImages!: (spaceId: string) => any[];
+
+  /**
+   * 获取空间CAD文件
+   * 需要父组件提供
+   */
+  @Input() getSpaceCADFiles!: (spaceId: string) => any[];
+
+  /**
+   * 获取图片数量(按类型)
+   * 需要父组件提供
+   */
+  @Input() getImageCountByType!: (spaceId: string, type: string) => number;
+
+  /**
+   * 获取图片(按类型)
+   * 需要父组件提供
+   */
+  @Input() getImagesByType!: (spaceId: string, type: string) => any[];
+
+  /**
+   * 检查是否有图片分析
+   * 需要父组件提供
+   */
+  @Input() hasImageAnalysis!: (imageId: string) => boolean;
+
+  /**
+   * 获取图片分析结果
+   * 需要父组件提供
+   */
+  @Input() getImageAnalysisResults!: (spaceId: string) => any[];
+
+  /**
+   * 获取图片URL
+   * 需要父组件提供
+   */
+  @Input() getImageUrl!: (imageId: string) => string;
+}

+ 52 - 682
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -82,9 +82,13 @@
           <!-- 步骤1: 上传图片和描述 -->
           @if (!aiDesignAnalysisResult) {
             <div class="upload-section">
-              <!-- 已上传的文件 -->
+              <!-- 已上传的文件 - 支持拖拽 -->
               @if (aiDesignUploadedFiles.length > 0) {
-                <div class="uploaded-files">
+                <div class="uploaded-files"
+                     (drop)="onAIFileDrop($event)"
+                     (dragover)="onAIFileDragOver($event)"
+                     (dragleave)="onAIFileDragLeave($event)"
+                     [class.drag-over]="aiDesignDragOver">
                   @for (file of aiDesignUploadedFiles; track file.url; let i = $index) {
                     <div class="file-item" [class.is-image]="file.extension && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.extension)">
                       @if (file.extension && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.extension)) {
@@ -116,6 +120,18 @@
                     <div class="add-more" (click)="triggerAIDialogFileInput()">
                       <div class="add-icon">+</div>
                       <div class="add-text">继续添加</div>
+                      <div class="add-hint">或拖拽文件到此</div>
+                    </div>
+                  }
+                  
+                  <!-- 拖拽提示遮罩层 -->
+                  @if (aiDesignDragOver) {
+                    <div class="drag-overlay">
+                      <div class="drag-hint-content">
+                        <div class="drag-icon">📥</div>
+                        <p>松开鼠标即可添加</p>
+                        <p class="drag-support">支持:图片、文字、URL</p>
+                      </div>
                     </div>
                   }
                 </div>
@@ -560,588 +576,39 @@
 
     <!-- 全局需求 (始终显示) -->
     <div class="global-requirements">
-        <!-- 空间需求管理 - 新布局 -->
-        <div class="card space-requirements-card">
-          <div class="card-header">
-            <h3 class="card-title">
-              <span class="icon">🏠</span>
-              空间需求管理
-            </h3>
-            <p class="card-subtitle">按空间管理参考图片、CAD文件和特殊需求</p>
-          </div>
-          <div class="card-content">
-            <div class="spaces-container">
-              @for (space of projectProducts; track space.id) {
-                <div class="space-item" [class.expanded]="isSpaceExpanded(space.id)">
-                  <!-- 空间头部 - 折叠时显示 -->
-                  <div class="space-header" (click)="toggleSpaceExpansion(space.id)">
-                    <div class="space-name-section">
-                      {{ getSpaceDisplayName(space) }}
-                      <span class="reference-count-badge">
-                        参考 {{ getTotalSpaceFileCount(space.id) }}
-                      </span>
-                    </div>
-                    
-                    <!-- 特殊需求显示 -->
-                    @if (getSpaceSpecialRequirements(space.id)) {
-                      <div class="special-requirements-box">
-                        <span class="requirements-label">特殊要求:</span>
-                        <span class="requirements-text">{{ getSpaceSpecialRequirements(space.id) | slice:0:30 }}{{ getSpaceSpecialRequirements(space.id).length > 30 ? '...' : '' }}</span>
-                      </div>
-                    }
-                    
-                    <!-- 操作按钮 -->
-                    <div class="header-actions">
-                      @if (canEdit) {
-                        <button class="btn-icon-small btn-ai" title="AI设计分析" (click)="openAIDesignDialog(space); $event.stopPropagation()">
-                          <span class="icon-text">🤖</span>
-                        </button>
-                        <button class="btn-icon-small btn-edit" title="编辑特殊要求" (click)="toggleSpaceExpansion(space.id); $event.stopPropagation()">
-                          <span class="icon-text">✏️</span>
-                        </button>
-                      }
-                    </div>
-                    
-                    <!-- 展开/收起图标 -->
-                    <div class="expand-icon" [class.expanded]="isSpaceExpanded(space.id)">
-                      <svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
-                        <path d="M7 10l5 5 5-5z"/>
-                      </svg>
-                    </div>
-                  </div>
-
-                  <!-- 空间内容 - 展开时显示 -->
-                  @if (isSpaceExpanded(space.id)) {
-                    <div class="space-content">
-                      <!-- 拖拽上传区域 -->
-                      <div class="drag-drop-zone"
-                           (dragover)="onDragOver($event, space.id)"
-                           (dragleave)="onDragLeave($event)"
-                           (drop)="onDrop($event, space.id)"
-                           [class.drag-over]="isDragOver && dragOverSpaceId === space.id">
-                        <div class="drag-drop-content">
-                          <div class="drag-drop-icon">
-                            <ion-icon name="cloud-upload-outline"></ion-icon>
-                          </div>
-                          <h4>拖拽参考图片到此</h4>
-                          <p>或点击下方按钮上传</p>
-                          <p class="drag-hint">AI将自动分析图片并智能分类</p>
-                        </div>
-                      </div>
-
-                      <!-- 图片类型导航标签 -->
-                      <div class="image-type-tabs">
-                        @for (type of imageTypes; track type.id) {
-                          <button
-                            class="tab-button"
-                            [class.active]="activeImageTab[space.id] === type.id"
-                            (click)="selectImageTab(space.id, type.id)">
-                            <span class="tab-label">{{ type.name }}</span>
-                            @if (getImageCountByType(space.id, type.id) > 0) {
-                              <span class="tab-badge">{{ getImageCountByType(space.id, type.id) }}</span>
-                            }
-                          </button>
-                        }
-                      </div>
-
-                      <!-- 图片展示区域 -->
-                      <div class="images-section">
-                        @if (activeImageTab[space.id] === 'all') {
-                          <!-- 全部图片和CAD文件 -->
-                          <div class="section-header">
-                            <h5>所有参考文件</h5>
-                            @if (canEdit) {
-                              <input
-                                type="file"
-                                accept="image/*"
-                                multiple
-                                (change)="uploadReferenceImage($event, space.id)"
-                                [disabled]="uploading"
-                                hidden
-                                [id]="'spaceImageInput_' + space.id" />
-                              <button
-                                class="btn btn-sm btn-outline"
-                                (click)="triggerFileClick('spaceImageInput_' + space.id)"
-                                [disabled]="uploading">
-                                <ion-icon name="add"></ion-icon>
-                                上传参考图
-                              </button>
-                            }
-                          </div>
-                          <div class="section-content">
-                            @if (getSpaceReferenceImages(space.id).length > 0) {
-                              <div class="images-grid">
-                                @for (image of getSpaceReferenceImages(space.id); track image.id) {
-                                  <div class="image-item">
-                                    <img [src]="image.url" [alt]="image.name" (click)="viewImageColorAnalysis(image.id)" />
-                                    <div class="image-overlay">
-                                      <div class="overlay-top">
-                                        <span class="badge" [class]="getImageTypeBadgeClass(image.type)">
-                                          {{ getImageTypeLabel(image.type) }}
-                                        </span>
-                                        @if (hasImageAnalysis(image.id)) {
-                                          <span class="badge badge-success">
-                                            <ion-icon name="sparkles"></ion-icon>
-                                          </span>
-                                        }
-                                      </div>
-                                      <div class="overlay-actions">
-                                        <button
-                                          class="btn-icon btn-primary"
-                                          (click)="viewImageColorAnalysis(image.id); $event.stopPropagation()"
-                                          title="查看色彩分析">
-                                          <ion-icon name="color-palette"></ion-icon>
-                                        </button>
-                                        @if (canEdit) {
-                                          <button
-                                            class="btn-icon btn-danger"
-                                            (click)="deleteReferenceImage(image.id); $event.stopPropagation()">
-                                            <ion-icon name="trash"></ion-icon>
-                                          </button>
-                                        }
-                                      </div>
-                                    </div>
-                                  </div>
-                                }
-                              </div>
-                            } @else {
-                              <div class="empty-state">
-                                <ion-icon name="image-outline"></ion-icon>
-                                <p>暂无参考图片</p>
-                              </div>
-                            }
-                          </div>
-
-                          <!-- CAD文件显示 -->
-                          @if (getSpaceCADFiles(space.id).length > 0) {
-                            <div class="section-header">
-                              <h5>CAD文件</h5>
-                            </div>
-                            <div class="section-content">
-                              <div class="cad-files-list">
-                                @for (cadFile of getSpaceCADFiles(space.id); track cadFile.id) {
-                                  <div class="cad-file-item">
-                                    <div class="cad-icon">
-                                      <ion-icon name="document"></ion-icon>
-                                    </div>
-                                    <div class="cad-info">
-                                      <div class="cad-name">{{ cadFile.name }}</div>
-                                      <div class="cad-meta">
-                                        @if (hasImageAnalysis(cadFile.id)) {
-                                          <span class="badge badge-success">
-                                            <ion-icon name="sparkles"></ion-icon>
-                                            已分析
-                                          </span>
-                                        }
-                                      </div>
-                                    </div>
-                                    @if (canEdit) {
-                                      <button
-                                        class="btn-icon btn-danger"
-                                        (click)="deleteCADFile(cadFile.id); $event.stopPropagation()">
-                                        <ion-icon name="trash"></ion-icon>
-                                      </button>
-                                    }
-                                  </div>
-                                }
-                              </div>
-                            </div>
-                          }
-
-                          <!-- AI分析结果展示 - 新布局 -->
-                          @if (getImageAnalysisResults(space.id).length > 0) {
-                            <div class="section-divider"></div>
-                            <div class="ai-analysis-section">
-                              <div class="section-header">
-                                <h5>
-                                  <ion-icon name="sparkles"></ion-icon>
-                                  AI分析结果
-                                </h5>
-                              </div>
-                              <div class="analysis-results-list">
-                                @for (analysis of getImageAnalysisResults(space.id); track analysis.imageId) {
-                                  <div class="analysis-item-card">
-                                    <!-- 左侧:图片 -->
-                                    <div class="analysis-image-section">
-                                      <div class="analysis-image">
-                                        <img [src]="getImageUrl(analysis.imageId)" [alt]="'Analysis ' + analysis.imageId" />
-                                      </div>
-                                    </div>
-
-                                    <!-- 右侧:分析结果 -->
-                                    <div class="analysis-results-section">
-                                      <!-- 参考类型和用户要求(按图1格式) -->
-                                      <div class="analysis-header-box">
-                                        <div class="header-info">
-                                          <div class="info-item">
-                                            <span class="info-label">参考类型:</span>
-                                            <span class="info-value">{{ getImageTypeLabel(analysis.imageType) }}</span>
-                                          </div>
-                                          <div class="info-divider">|</div>
-                                          <div class="info-item">
-                                            <span class="info-label">备注用户要求</span>
-                                          </div>
-                                        </div>
-                                      </div>
-
-                                      <!-- AI分析结果内容 -->
-                                      <div class="analysis-details-content">
-                                        <!-- 原有分析维度 -->
-                                        @if (analysis.originalAnalysis) {
-                                          <div class="analysis-group">
-                                            <h6>原有分析维度</h6>
-                                            <div class="analysis-grid">
-                                              @if (analysis.originalAnalysis.quality) {
-                                                <div class="analysis-row">
-                                                  <span class="label">质量评分:</span>
-                                                  <span class="value">{{ analysis.originalAnalysis.quality.score }}/100 ({{ analysis.originalAnalysis.quality.level }})</span>
-                                                </div>
-                                              }
-                                              @if (analysis.originalAnalysis.content) {
-                                                <div class="analysis-row">
-                                                  <span class="label">内容分类:</span>
-                                                  <span class="value">{{ analysis.originalAnalysis.content.category }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.originalAnalysis.technical) {
-                                                <div class="analysis-row">
-                                                  <span class="label">像素:</span>
-                                                  <span class="value">{{ analysis.originalAnalysis.technical.megapixels }}MP</span>
-                                                </div>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 新增分析维度 -->
-                                        @if (analysis.enhancedAnalysis) {
-                                          <div class="analysis-group">
-                                            <h6>设计分析维度</h6>
-                                            <div class="analysis-grid">
-                                              @if (analysis.enhancedAnalysis.style) {
-                                                <div class="analysis-row">
-                                                  <span class="label">风格:</span>
-                                                  <span class="value">{{ analysis.enhancedAnalysis.style }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.enhancedAnalysis.atmosphere) {
-                                                <div class="analysis-row">
-                                                  <span class="label">氛围:</span>
-                                                  <span class="value">{{ analysis.enhancedAnalysis.atmosphere }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.enhancedAnalysis.material) {
-                                                <div class="analysis-row">
-                                                  <span class="label">材质:</span>
-                                                  <span class="value">{{ analysis.enhancedAnalysis.material }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.enhancedAnalysis.texture) {
-                                                <div class="analysis-row">
-                                                  <span class="label">纹理:</span>
-                                                  <span class="value">{{ analysis.enhancedAnalysis.texture }}</span>
-                                                </div>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 风格元素分析(新增) -->
-                                        @if (analysis.styleElements) {
-                                          <div class="analysis-group">
-                                            <h6>风格元素</h6>
-                                            <div class="analysis-tags">
-                                              @for (keyword of analysis.styleElements.styleKeywords; track keyword) {
-                                                <span class="tag">{{ keyword }}</span>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 色彩搭配分析(新增) -->
-                                        @if (analysis.colorScheme) {
-                                          <div class="analysis-group">
-                                            <h6>色彩搭配</h6>
-                                            <div class="color-scheme-display">
-                                              @if (analysis.colorScheme.primaryColors && analysis.colorScheme.primaryColors.length > 0) {
-                                                <div class="color-row">
-                                                  <span class="color-label">主色调:</span>
-                                                  <div class="color-samples">
-                                                    @for (color of analysis.colorScheme.primaryColors; track color) {
-                                                      <div class="color-sample" [style.backgroundColor]="color" [title]="color"></div>
-                                                    }
-                                                  </div>
-                                                </div>
-                                              }
-                                              @if (analysis.colorScheme.secondaryColors && analysis.colorScheme.secondaryColors.length > 0) {
-                                                <div class="color-row">
-                                                  <span class="color-label">辅助色:</span>
-                                                  <div class="color-samples">
-                                                    @for (color of analysis.colorScheme.secondaryColors; track color) {
-                                                      <div class="color-sample" [style.backgroundColor]="color" [title]="color"></div>
-                                                    }
-                                                  </div>
-                                                </div>
-                                              }
-                                              @if (analysis.colorScheme.accentColors && analysis.colorScheme.accentColors.length > 0) {
-                                                <div class="color-row">
-                                                  <span class="color-label">点缀色:</span>
-                                                  <div class="color-samples">
-                                                    @for (color of analysis.colorScheme.accentColors; track color) {
-                                                      <div class="color-sample" [style.backgroundColor]="color" [title]="color"></div>
-                                                    }
-                                                  </div>
-                                                </div>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 材质分析(新增) -->
-                                        @if (analysis.materialAnalysis) {
-                                          <div class="analysis-group">
-                                            <h6>材质分析</h6>
-                                            <div class="analysis-tags">
-                                              @for (material of analysis.materialAnalysis.materials; track material) {
-                                                <span class="tag tag-material">{{ material }}</span>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 布局特征分析(新增) -->
-                                        @if (analysis.layoutFeatures) {
-                                          <div class="analysis-group">
-                                            <h6>布局特征</h6>
-                                            <div class="analysis-tags">
-                                              @for (feature of analysis.layoutFeatures.features; track feature) {
-                                                <span class="tag tag-layout">{{ feature }}</span>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 空间氛围分析(新增) -->
-                                        @if (analysis.atmosphereAnalysis) {
-                                          <div class="analysis-group">
-                                            <h6>空间氛围</h6>
-                                            <div class="analysis-row">
-                                              <span class="value">{{ analysis.atmosphereAnalysis.atmosphere }}</span>
-                                            </div>
-                                          </div>
-                                        }
-
-                                        <!-- 色彩解析报告 -->
-                                        @if (analysis.colorAnalysis) {
-                                          <div class="analysis-group">
-                                            <h6>色彩解析报告</h6>
-                                            <div class="color-analysis-content">
-                                              @if (analysis.colorAnalysis.brightness) {
-                                                <div class="color-item">
-                                                  <span class="color-label">明度:</span>
-                                                  <span class="color-value">{{ analysis.colorAnalysis.brightness }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.colorAnalysis.hue) {
-                                                <div class="color-item">
-                                                  <span class="color-label">色相:</span>
-                                                  <span class="color-value">{{ analysis.colorAnalysis.hue }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.colorAnalysis.saturation) {
-                                                <div class="color-item">
-                                                  <span class="color-label">饱和度:</span>
-                                                  <span class="color-value">{{ analysis.colorAnalysis.saturation }}</span>
-                                                </div>
-                                              }
-                                              @if (analysis.colorAnalysis.openness) {
-                                                <div class="color-item">
-                                                  <span class="color-label">色彩开放度:</span>
-                                                  <span class="color-value">{{ analysis.colorAnalysis.openness }}</span>
-                                                </div>
-                                              }
-
-                                              <!-- 颜色样本 -->
-                                              @if (analysis.colorAnalysis.extracted && analysis.colorAnalysis.extracted.length > 0) {
-                                                <div class="color-swatches-group">
-                                                  <span class="color-label">提取颜色:</span>
-                                                  <div class="color-swatches">
-                                                    @for (color of analysis.colorAnalysis.extracted; track color) {
-                                                      <div class="color-swatch" [style.backgroundColor]="color" [title]="color"></div>
-                                                    }
-                                                  </div>
-                                                </div>
-                                              }
-
-                                              @if (analysis.colorAnalysis.organized && analysis.colorAnalysis.organized.length > 0) {
-                                                <div class="color-org-group">
-                                                  <span class="color-label">组织颜色 (主次):</span>
-                                                  <div class="color-organization">
-                                                    @for (org of analysis.colorAnalysis.organized; track org.color) {
-                                                      <div class="color-org-item">
-                                                        <div class="color-org-swatch" [style.backgroundColor]="org.color"></div>
-                                                        <span class="color-org-label">{{ org.role }}: {{ org.percentage }}%</span>
-                                                      </div>
-                                                    }
-                                                  </div>
-                                                </div>
-                                              }
-                                            </div>
-                                          </div>
-                                        }
-                                      </div>
-                                    </div>
-                                  </div>
-                                }
-                              </div>
-                            </div>
-                          }
-                          
-                          <!-- 用户需求备注 -->
-                          <div class="section-divider"></div>
-                          <div class="user-notes-section">
-                            <div class="section-header">
-                              <h5>用户需求备注</h5>
-                            </div>
-                            <div class="section-content">
-                              <textarea
-                                class="form-textarea"
-                                [(ngModel)]="spaceSpecialRequirements[space.id]"
-                                (ngModelChange)="setSpaceSpecialRequirements(space.id, $event)"
-                                [disabled]="!canEdit"
-                                rows="4"
-                                [placeholder]="'描述' + getSpaceDisplayName(space) + '的特殊要求和注意事项'"></textarea>
-                            </div>
-                          </div>
-                        } @else if (activeImageTab[space.id] === 'cad') {
-                          <!-- CAD文件 -->
-                          <div class="section-header">
-                            <h5>CAD文件</h5>
-                            @if (canEdit) {
-                              <input
-                                type="file"
-                                accept=".dwg,.dxf,.pdf"
-                                multiple
-                                (change)="uploadCAD($event, space.id)"
-                                [disabled]="uploading"
-                                hidden
-                                [id]="'spaceCADInput_' + space.id" />
-                              <button
-                                class="btn btn-sm btn-outline"
-                                (click)="triggerFileClick('spaceCADInput_' + space.id)"
-                                [disabled]="uploading">
-                                <ion-icon name="add"></ion-icon>
-                                上传CAD
-                              </button>
-                            }
-                          </div>
-                          <div class="section-content">
-                            @if (getSpaceCADFiles(space.id).length > 0) {
-                              <div class="file-list">
-                                @for (file of getSpaceCADFiles(space.id); track file.id) {
-                                  <div class="file-item">
-                                    <ion-icon name="document-text" class="file-icon"></ion-icon>
-                                    <div class="file-info">
-                                      <h6>{{ file.name }}</h6>
-                                      <p>{{ formatFileSize(file.size) }} · {{ file.uploadTime | date:'MM-dd HH:mm' }}</p>
-                                      @if (hasImageAnalysis(file.id)) {
-                                        <span class="badge badge-success">
-                                          <ion-icon name="sparkles"></ion-icon>
-                                          已分析
-                                        </span>
-                                      }
-                                    </div>
-                                    @if (canEdit) {
-                                      <button
-                                        class="btn-icon btn-danger"
-                                        (click)="deleteCAD(file.id)">
-                                        <ion-icon name="trash"></ion-icon>
-                                      </button>
-                                    }
-                                  </div>
-                                }
-                              </div>
-                            } @else {
-                              <div class="empty-state">
-                                <ion-icon name="document-outline"></ion-icon>
-                                <p>暂无CAD文件</p>
-                              </div>
-                            }
-                          </div>
-                        } @else {
-                          <!-- 按类型过滤的图片 -->
-                          <div class="section-header">
-                            <h5>{{ getImageTypeLabel(activeImageTab[space.id]) }}图片</h5>
-                            @if (canEdit) {
-                              <input
-                                type="file"
-                                accept="image/*"
-                                multiple
-                                (change)="uploadReferenceImageWithType($event, space.id, activeImageTab[space.id])"
-                                [disabled]="uploading"
-                                hidden
-                                [id]="'spaceImageInput_' + space.id + '_' + activeImageTab[space.id]" />
-                              <button
-                                class="btn btn-sm btn-outline"
-                                (click)="triggerFileClick('spaceImageInput_' + space.id + '_' + activeImageTab[space.id])"
-                                [disabled]="uploading">
-                                <ion-icon name="add"></ion-icon>
-                                上传{{ getImageTypeLabel(activeImageTab[space.id]) }}图
-                              </button>
-                            }
-                          </div>
-                          <div class="section-content">
-                            @if (getImagesByType(space.id, activeImageTab[space.id]).length > 0) {
-                              <div class="images-grid">
-                                @for (image of getImagesByType(space.id, activeImageTab[space.id]); track image.id) {
-                                  <div class="image-item">
-                                    <img [src]="image.url" [alt]="image.name" (click)="viewImageColorAnalysis(image.id)" />
-                                    <div class="image-overlay">
-                                      <div class="overlay-top">
-                                        <span class="badge" [class]="getImageTypeBadgeClass(image.type)">
-                                          {{ getImageTypeLabel(image.type) }}
-                                        </span>
-                                        @if (hasImageAnalysis(image.id)) {
-                                          <span class="badge badge-success">
-                                            <ion-icon name="sparkles"></ion-icon>
-                                          </span>
-                                        }
-                                      </div>
-                                      <div class="overlay-actions">
-                                        <button
-                                          class="btn-icon btn-primary"
-                                          (click)="viewImageColorAnalysis(image.id); $event.stopPropagation()"
-                                          title="查看色彩分析">
-                                          <ion-icon name="color-palette"></ion-icon>
-                                        </button>
-                                        @if (canEdit) {
-                                          <button
-                                            class="btn-icon btn-danger"
-                                            (click)="deleteReferenceImage(image.id); $event.stopPropagation()">
-                                            <ion-icon name="trash"></ion-icon>
-                                          </button>
-                                        }
-                                      </div>
-                                    </div>
-                                  </div>
-                                }
-                              </div>
-                            } @else {
-                              <div class="empty-state">
-                                <ion-icon name="image-outline"></ion-icon>
-                                <p>暂无{{ getImageTypeLabel(activeImageTab[space.id]) }}图片</p>
-                              </div>
-                            }
-                          </div>
-                        }
-                      </div>
-                    </div>
-                  }
-                </div>
-              }
-            </div>
-          </div>
-        </div>
+        <!-- 空间需求管理 - 已提取为独立组件 -->
+        <app-space-requirements-management
+          [projectProducts]="projectProducts"
+          [canEdit]="canEdit"
+          [uploading]="uploading"
+          [spaceSpecialRequirements]="spaceSpecialRequirements"
+          [expandedSpaces]="expandedSpaces"
+          [activeImageTab]="activeImageTab"
+          [isDragOver]="isDragOver"
+          [dragOverSpaceId]="dragOverSpaceId"
+          
+          [getTotalSpaceFileCount]="getTotalSpaceFileCount.bind(this)"
+          [getSpaceSpecialRequirements]="getSpaceSpecialRequirements.bind(this)"
+          [getSpaceReferenceImages]="getSpaceReferenceImages.bind(this)"
+          [getSpaceCADFiles]="getSpaceCADFiles.bind(this)"
+          [getImageCountByType]="getImageCountByType.bind(this)"
+          [getImagesByType]="getImagesByType.bind(this)"
+          [hasImageAnalysis]="hasImageAnalysis.bind(this)"
+          [getImageAnalysisResults]="getImageAnalysisResults.bind(this)"
+          [getImageUrl]="getImageUrl.bind(this)"
+          
+          (spaceExpanded)="toggleSpaceExpansion($event)"
+          (imageTabChanged)="selectImageTab($event.spaceId, $event.tabId)"
+          (fileUpload)="handleComponentFileUpload($event)"
+          (imageDelete)="deleteReferenceImage($event)"
+          (cadDelete)="deleteCADFile($event)"
+          (imageView)="viewImageColorAnalysis($event)"
+          (specialRequirementsChange)="setSpaceSpecialRequirements($event.spaceId, $event.value)"
+          (aiDesignOpen)="openAIDesignDialog($event)"
+          (dragOverChange)="onDragOver($event.event, $event.spaceId)"
+          (dragLeaveChange)="onDragLeave($event)"
+          (dropChange)="onDrop($event.event, $event.spaceId)">
+        </app-space-requirements-management>
 
         <!-- 风格偏好 - 已隐藏 -->
         <!-- 
@@ -1153,104 +620,7 @@
             </h3>
           </div>
           <div class="card-content">
-            <div class="form-group">
-              <label class="form-label">风格偏好 <span class="required">*</span></label>
-              <input
-                type="text"
-                class="form-input"
-                [(ngModel)]="globalRequirements.stylePreference"
-                [disabled]="!canEdit"
-                placeholder="如:现代简约、北欧、轻奢等" />
-            </div>
-
-            <div class="color-scheme">
-              <div class="form-group">
-                <label class="form-label">色彩氛围</label>
-                <select
-                  class="form-select"
-                  [(ngModel)]="globalRequirements.colorScheme.atmosphere"
-                  [disabled]="!canEdit">
-                  <option value="">请选择</option>
-                  <option value="温馨">温馨</option>
-                  <option value="高级">高级</option>
-                  <option value="简约">简约</option>
-                  <option value="时尚">时尚</option>
-                  <option value="北欧">北欧</option>
-                  <option value="中式">中式</option>
-                  <option value="欧式">欧式</option>
-                </select>
-              </div>
-
-              <div class="color-inputs">
-                <div class="form-group">
-                  <label class="form-label">主色调</label>
-                  <input
-                    type="color"
-                    class="form-color-input"
-                    [(ngModel)]="globalRequirements.colorScheme.primary"
-                    [disabled]="!canEdit" />
-                </div>
-                <div class="form-group">
-                  <label class="form-label">副色调</label>
-                  <input
-                    type="color"
-                    class="form-color-input"
-                    [(ngModel)]="globalRequirements.colorScheme.secondary"
-                    [disabled]="!canEdit" />
-                </div>
-                <div class="form-group">
-                  <label class="form-label">点缀色</label>
-                  <input
-                    type="color"
-                    class="form-color-input"
-                    [(ngModel)]="globalRequirements.colorScheme.accent"
-                    [disabled]="!canEdit" />
-                </div>
-              </div>
-            </div>
-
-            <div class="quality-level">
-              <label class="form-label">质量等级</label>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input
-                    type="radio"
-                    name="qualityLevel"
-                    value="standard"
-                    [(ngModel)]="globalRequirements.qualityLevel"
-                    [disabled]="!canEdit" />
-                  <span class="radio-label">{{ getQualityLevelName('standard') }}</span>
-                </label>
-                <label class="radio-item">
-                  <input
-                    type="radio"
-                    name="qualityLevel"
-                    value="premium"
-                    [(ngModel)]="globalRequirements.qualityLevel"
-                    [disabled]="!canEdit" />
-                  <span class="radio-label">{{ getQualityLevelName('premium') }}</span>
-                </label>
-                <label class="radio-item">
-                  <input
-                    type="radio"
-                    name="qualityLevel"
-                    value="luxury"
-                    [(ngModel)]="globalRequirements.qualityLevel"
-                    [disabled]="!canEdit" />
-                  <span class="radio-label">{{ getQualityLevelName('luxury') }}</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="form-group">
-              <label class="form-label">特殊需求</label>
-              <textarea
-                class="form-textarea"
-                [(ngModel)]="globalRequirements.specialRequirements"
-                [disabled]="!canEdit"
-                rows="3"
-                placeholder="描述任何特殊需求或注意事项"></textarea>
-            </div>
+            ...
           </div>
         </div>
         -->

+ 68 - 1
src/modules/project/pages/project-detail/stages/stage-requirements.component.scss

@@ -3564,12 +3564,28 @@
       }
     }
 
-    // 已上传文件
+    // 已上传文件 - 支持拖拽
     .uploaded-files {
+      position: relative;
       display: grid;
       grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
       gap: 16px;
       margin-bottom: 24px;
+      transition: all 0.3s ease;
+      
+      // 拖拽悬停状态
+      &.drag-over {
+        background: #f0f7ff;
+        border: 2px dashed #667eea;
+        border-radius: 12px;
+        padding: 8px;
+        
+        .file-item,
+        .add-more {
+          opacity: 0.5;
+          pointer-events: none;
+        }
+      }
 
       .file-item {
         position: relative;
@@ -3711,7 +3727,58 @@
           font-size: 13px;
           color: #718096;
         }
+        
+        .add-hint {
+          font-size: 11px;
+          color: #a0aec0;
+          margin-top: 4px;
+        }
       }
+      
+      // 🔥 拖拽提示遮罩层
+      .drag-overlay {
+        position: absolute;
+        inset: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: rgba(102, 126, 234, 0.05);
+        border-radius: 12px;
+        z-index: 100;
+        animation: fadeIn 0.2s ease;
+        
+        .drag-hint-content {
+          text-align: center;
+          padding: 24px;
+          background: white;
+          border-radius: 12px;
+          box-shadow: 0 8px 32px rgba(102, 126, 234, 0.2);
+          
+          .drag-icon {
+            font-size: 48px;
+            margin-bottom: 12px;
+            animation: bounce 0.6s infinite;
+          }
+          
+          p {
+            margin: 4px 0;
+            font-size: 16px;
+            font-weight: 600;
+            color: #667eea;
+            
+            &.drag-support {
+              font-size: 14px;
+              font-weight: 400;
+              color: #718096;
+            }
+          }
+        }
+      }
+    }
+    
+    @keyframes fadeIn {
+      from { opacity: 0; }
+      to { opacity: 1; }
     }
 
     // AI对话容器(优化布局,减少遮挡)

+ 624 - 42
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -8,7 +8,9 @@ import { MatDialog } from '@angular/material/dialog';
 import { ProductSpaceService, Project } from '../../../services/product-space.service';
 import { ProjectFileService } from '../../../services/project-file.service';
 import { DesignAnalysisAIService } from '../../../services/design-analysis-ai.service';
+import { ImageAnalysisService } from '../../../services/image-analysis.service';
 import { ColorGetDialogComponent } from '../../../components/color-get/color-get-dialog.component';
+import { SpaceRequirementsManagementComponent } from './components/space-requirements-management/space-requirements-management.component';
 import { completionJSON } from 'fmode-ng/core';
 import { addIcons } from 'ionicons';
 import { add, chevronDown, colorPalette, send, sparkles, trash } from 'ionicons/icons';
@@ -27,7 +29,7 @@ addIcons({
 @Component({
   selector: 'app-stage-requirements',
   standalone: true,
-  imports: [CommonModule, FormsModule, ReactiveFormsModule, IonIcon],
+  imports: [CommonModule, FormsModule, ReactiveFormsModule, IonIcon, SpaceRequirementsManagementComponent],
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
   providers: [],
   templateUrl: './stage-requirements.component.html',
@@ -256,6 +258,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     private productSpaceService: ProductSpaceService,
     private projectFileService: ProjectFileService,
     private designAnalysisAIService: DesignAnalysisAIService,
+    private imageAnalysisService: ImageAnalysisService,
     private dialog: MatDialog
   ) {}
 
@@ -625,7 +628,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   }
 
   /**
-   * 拖拽放下处理
+   * 拖拽放下处理(增强版 - 支持企业微信)
    */
   async onDrop(event: DragEvent, spaceId: string): Promise<void> {
     event.preventDefault();
@@ -633,15 +636,29 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     this.isDragOver = false;
     this.dragOverSpaceId = '';
 
-    const files = event.dataTransfer?.files;
-    if (!files || files.length === 0) return;
+    console.log('📥 [拖拽放下] 空间ID:', spaceId);
+    console.log('📥 [拖拽放下] 环境:', this.isWeChatWorkEnv() ? '企业微信' : '普通浏览器');
+
+    // 1. 解析拖拽数据(支持企业微信)
+    const dragData = this.parseWeChatDragData(event);
+    
+    if (!dragData.hasContent) {
+      console.warn('⚠️ [拖拽放下] 未检测到有效内容');
+      return;
+    }
 
-    // 分类处理拖拽的文件
+    console.log('✅ [拖拽放下] 解析结果:', {
+      图片数量: dragData.images.length,
+      文字长度: dragData.text.length,
+      URL数量: dragData.urls.length,
+      自动分析: dragData.autoAnalyze
+    });
+
+    // 2. 分类处理图片文件
     const imageFiles: File[] = [];
     const cadFiles: File[] = [];
     
-    for (let i = 0; i < files.length; i++) {
-      const file = files[i];
+    for (const file of dragData.images) {
       const fileName = file.name.toLowerCase();
       
       // 识别图片文件
@@ -654,16 +671,35 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
       }
     }
 
-    // 上传图片并进行AI分析
+    // 3. 上传图片并进行AI分析
     if (imageFiles.length > 0) {
+      console.log(`📤 [开始上传] ${imageFiles.length}个图片文件`);
       await this.uploadAndAnalyzeImages(imageFiles, spaceId);
     }
 
-    // 上传CAD文件
+    // 4. 处理URL列表(下载并上传)
+    if (dragData.urls.length > 0) {
+      console.log(`🔗 [处理URL] ${dragData.urls.length}个图片链接`);
+      await this.downloadAndUploadFromUrls(dragData.urls, spaceId);
+    }
+
+    // 5. 上传CAD文件
     if (cadFiles.length > 0) {
+      console.log(`📤 [上传CAD] ${cadFiles.length}个CAD文件`);
       await this.uploadCADFiles(cadFiles, spaceId);
     }
 
+    // 6. 处理文本内容(保存到特殊需求)
+    if (dragData.text && dragData.text.length > 0) {
+      console.log(`💾 [保存文本] 到特殊需求 (${dragData.text.length}字)`);
+      const existingReq = this.getSpaceSpecialRequirements(spaceId) || '';
+      const timestamp = new Date().toLocaleString('zh-CN');
+      const newReq = existingReq 
+        ? `${existingReq}\n\n--- 从企业微信拖拽 (${timestamp}) ---\n${dragData.text}`
+        : dragData.text;
+      this.setSpaceSpecialRequirements(spaceId, newReq);
+    }
+
     this.cdr.markForCheck();
   }
 
@@ -1523,7 +1559,586 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     }
   }
 
+  /**
+   * 处理来自空间需求管理组件的文件上传事件
+   */
+  async handleComponentFileUpload(data: {event: Event, spaceId: string, type?: string}): Promise<void> {
+    if (data.type === 'cad') {
+      await this.uploadCAD(data.event, data.spaceId);
+    } else if (data.type) {
+      await this.uploadReferenceImageWithType(data.event, data.spaceId, data.type);
+    } else {
+      await this.uploadReferenceImage(data.event, data.spaceId);
+    }
+  }
+
   analysisImageMap:any = {}
+  
+  // ==================== 企业微信拖拽支持 ====================
+  
+  /**
+   * 解析企业微信拖拽数据
+   */
+  private parseWeChatDragData(event: DragEvent): {
+    images: File[];
+    text: string;
+    html: string;
+    urls: string[];
+    hasContent: boolean;
+    autoAnalyze: boolean;
+  } {
+    const dataTransfer = event.dataTransfer;
+    if (!dataTransfer) {
+      return {
+        images: [],
+        text: '',
+        html: '',
+        urls: [],
+        hasContent: false,
+        autoAnalyze: false
+      };
+    }
+
+    console.log('🔍 [企业微信拖拽] dataTransfer.types:', dataTransfer.types);
+    console.log('🔍 [企业微信拖拽] files.length:', dataTransfer.files.length);
+
+    // 1. 提取图片文件
+    const images: File[] = [];
+    if (dataTransfer.files && dataTransfer.files.length > 0) {
+      for (let i = 0; i < dataTransfer.files.length; i++) {
+        const file = dataTransfer.files[i];
+        if (file.type.startsWith('image/')) {
+          images.push(file);
+          console.log(`📸 [图片文件] ${file.name} (${(file.size/1024).toFixed(2)}KB)`);
+        }
+      }
+    }
+
+    // 2. 提取文本内容
+    const text = dataTransfer.getData('text/plain') || '';
+    if (text) {
+      console.log(`📝 [文本内容] ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`);
+    }
+
+    // 3. 提取HTML内容(可能包含图片标签)
+    const html = dataTransfer.getData('text/html') || '';
+    if (html) {
+      console.log(`🌐 [HTML内容] 长度: ${html.length}`);
+      // 可选:从HTML中提取图片URL
+      const imgUrls = this.extractImageUrlsFromHtml(html);
+      if (imgUrls.length > 0) {
+        console.log(`🖼️ [HTML图片] ${imgUrls.length}个`, imgUrls);
+      }
+    }
+
+    // 4. 提取URL列表
+    const urlList = dataTransfer.getData('text/uri-list') || '';
+    const urls = urlList.split('\n').filter(url => url.trim() && !url.startsWith('#'));
+    if (urls.length > 0) {
+      console.log(`🔗 [URL列表] ${urls.length}个`, urls);
+    }
+
+    // 5. 判断是否有内容
+    const hasContent = images.length > 0 || text.length > 0 || urls.length > 0;
+
+    // 6. 判断是否自动启动AI分析(有图片或有丰富文本)
+    const autoAnalyze = images.length > 0 || text.length > 50;
+
+    return {
+      images,
+      text,
+      html,
+      urls,
+      hasContent,
+      autoAnalyze
+    };
+  }
+
+  /**
+   * 从HTML中提取图片URL
+   */
+  private extractImageUrlsFromHtml(html: string): string[] {
+    const imgRegex = /<img[^>]+src="([^"]+)"/gi;
+    const urls: string[] = [];
+    let match;
+    
+    while ((match = imgRegex.exec(html)) !== null) {
+      urls.push(match[1]);
+    }
+    
+    return urls;
+  }
+
+  /**
+   * 检测是否为企业微信环境
+   */
+  isWeChatWorkEnv(): boolean {
+    const ua = window.navigator.userAgent.toLowerCase();
+    return ua.includes('wxwork') || ua.includes('qywechat');
+  }
+
+  /**
+   * 从URL下载并上传图片
+   */
+  private async downloadAndUploadFromUrls(urls: string[], spaceId: string): Promise<void> {
+    for (const url of urls) {
+      try {
+        // 检查是否为图片URL
+        if (!this.isImageUrl(url)) {
+          console.warn(`⚠️ [非图片URL] ${url}`);
+          continue;
+        }
+
+        console.log(`📥 [下载图片] ${url}`);
+        
+        // 使用fetch下载图片
+        const response = await fetch(url);
+        if (!response.ok) {
+          throw new Error(`下载失败: ${response.statusText}`);
+        }
+
+        const blob = await response.blob();
+        const fileName = this.extractFileNameFromUrl(url) || `image_${Date.now()}.jpg`;
+        const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
+
+        console.log(`✅ [下载完成] ${fileName} (${(blob.size/1024).toFixed(2)}KB)`);
+
+        // 上传图片
+        await this.uploadAndAnalyzeImages([file], spaceId);
+
+      } catch (error) {
+        console.error(`❌ [下载失败] ${url}`, error);
+      }
+    }
+  }
+
+  /**
+   * 判断是否为图片URL
+   */
+  private isImageUrl(url: string): boolean {
+    const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
+    const lowerUrl = url.toLowerCase();
+    return imageExts.some(ext => lowerUrl.includes(ext)) || 
+           lowerUrl.includes('image') ||
+           lowerUrl.includes('photo');
+  }
+
+  /**
+   * 从URL提取文件名
+   */
+  private extractFileNameFromUrl(url: string): string | null {
+    try {
+      const urlObj = new URL(url);
+      const pathname = urlObj.pathname;
+      const segments = pathname.split('/');
+      return segments[segments.length - 1] || null;
+    } catch {
+      return null;
+    }
+  }
+
+  /**
+   * 清理文件名:移除特殊字符,保留扩展名
+   */
+  private sanitizeFileName(fileName: string): string {
+    const lastDotIndex = fileName.lastIndexOf('.');
+    const name = lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
+    const ext = lastDotIndex > 0 ? fileName.substring(lastDotIndex) : '';
+    
+    // 使用时间戳+随机数(推荐)
+    const timestamp = Date.now();
+    const random = Math.random().toString(36).substring(2, 8);
+    const cleanName = `img_${timestamp}_${random}${ext}`;
+    
+    return cleanName;
+  }
+
+  /**
+   * 带重试的文件上传
+   */
+  private async uploadFileWithRetry(
+    file: File, 
+    projectId: string, 
+    spaceId: string, 
+    fileName: string,
+    maxRetries: number = 3
+  ): Promise<any> {
+    let lastError: any = null;
+    
+    for (let attempt = 1; attempt <= maxRetries; attempt++) {
+      try {
+        console.log(`📤 上传尝试 ${attempt}/${maxRetries}: ${fileName}`);
+        
+        const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
+          file,
+          projectId,
+          'reference_image',
+          spaceId,
+          'requirements',
+          {
+            uploadedFor: 'requirements_analysis',
+            spaceId: spaceId,
+            uploadStage: 'requirements',
+            fileName: fileName
+          },
+          (progress) => {
+            console.log(`📊 上传进度: ${progress}% [${fileName}]`);
+          }
+        );
+
+        console.log(`✅ 上传成功 (尝试 ${attempt}): ${fileName}`);
+        return projectFile;
+
+      } catch (error: any) {
+        lastError = error;
+        console.error(`❌ 上传失败 (尝试 ${attempt}/${maxRetries}):`, error);
+
+        // 如果是631错误且还有重试次数,等待后重试
+        if (attempt < maxRetries) {
+          const waitTime = attempt * 1000; // 递增等待时间
+          console.log(`⏳ 等待 ${waitTime}ms 后重试...`);
+          await this.delay(waitTime);
+        }
+      }
+    }
+
+    // 所有重试都失败
+    throw new Error(`上传失败(已重试${maxRetries}次): ${lastError?.message || '未知错误'}`);
+  }
+
+  /**
+   * 延迟函数
+   */
+  private delay(ms: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+
+  /**
+   * 显示上传结果摘要(企业微信友好)
+   */
+  private showUploadSummary(results: { success: string[], failed: Array<{name: string, error: string}> }): void {
+    const successCount = results.success.length;
+    const failedCount = results.failed.length;
+    
+    if (failedCount === 0) {
+      // 全部成功 - 使用Toast
+      if (window?.fmode?.toast?.success) {
+        window.fmode.toast.success(`✅ 成功上传 ${successCount} 个文件\n🤖 AI正在分析中...`);
+      } else {
+        console.log(`✅ 成功上传 ${successCount} 个文件,AI正在分析中...`);
+      }
+    } else if (successCount === 0) {
+      // 全部失败 - 使用Alert
+      const message = `上传失败\n\n${results.failed.map(f => `• ${f.name}\n  ${f.error}`).join('\n\n')}`;
+      window?.fmode?.alert?.(message);
+    } else {
+      // 部分成功 - 使用详细Alert
+      const message = [
+        `上传完成`,
+        ``,
+        `✅ 成功: ${successCount} 个`,
+        `❌ 失败: ${failedCount} 个`,
+        ``,
+        `失败文件:`,
+        ...results.failed.map(f => `• ${f.name}\n  ${f.error}`)
+      ].join('\n');
+      
+      window?.fmode?.alert?.(message);
+    }
+  }
+  
+  // ==================== End: 企业微信拖拽支持 ====================
+  
+  // ==================== AI对话拖拽支持 ====================
+  
+  /**
+   * 详细打印拖拽数据结构(用于调试)
+   */
+  private logDragDataStructure(event: DragEvent, context: string): void {
+    console.log(`\n========== [${context}] 拖拽数据结构分析 ==========`);
+    
+    const dt = event.dataTransfer;
+    if (!dt) {
+      console.log('❌ dataTransfer 为空');
+      return;
+    }
+
+    // 1. 基本信息
+    console.log('\n📋 基本信息:');
+    console.log('  dropEffect:', dt.dropEffect);
+    console.log('  effectAllowed:', dt.effectAllowed);
+    console.log('  types:', Array.from(dt.types));
+    
+    // 2. Files
+    console.log('\n📁 Files 对象:');
+    console.log('  files.length:', dt.files?.length || 0);
+    if (dt.files && dt.files.length > 0) {
+      for (let i = 0; i < dt.files.length; i++) {
+        const file = dt.files[i];
+        console.log(`  [${i}] File对象:`, {
+          name: file.name,
+          size: file.size,
+          type: file.type,
+          lastModified: new Date(file.lastModified).toLocaleString()
+        });
+      }
+    }
+    
+    // 3. Items
+    console.log('\n📦 Items 对象:');
+    console.log('  items.length:', dt.items?.length || 0);
+    if (dt.items && dt.items.length > 0) {
+      for (let i = 0; i < dt.items.length; i++) {
+        const item = dt.items[i];
+        console.log(`  [${i}] DataTransferItem:`, {
+          kind: item.kind,
+          type: item.type
+        });
+        
+        // 尝试获取item的内容
+        if (item.kind === 'string') {
+          item.getAsString((str) => {
+            console.log(`    → 字符串内容 (${item.type}):`, str.substring(0, 200));
+          });
+        }
+      }
+    }
+    
+    // 4. getData测试
+    console.log('\n📝 getData() 测试:');
+    const commonTypes = ['text/plain', 'text/html', 'text/uri-list'];
+    
+    for (const type of commonTypes) {
+      try {
+        const data = dt.getData(type);
+        if (data) {
+          const preview = data.length > 200 ? data.substring(0, 200) + '...' : data;
+          console.log(`  ${type}:`, preview);
+        }
+      } catch (e) {
+        // 某些类型可能不可访问
+      }
+    }
+    
+    console.log('\n========== 数据结构分析结束 ==========\n');
+  }
+
+  /**
+   * 提取拖拽内容
+   */
+  private extractDragContent(event: DragEvent): {
+    files: File[];
+    images: File[];
+    text: string;
+    html: string;
+    urls: string[];
+    hasContent: boolean;
+  } {
+    const dt = event.dataTransfer;
+    if (!dt) return { files: [], images: [], text: '', html: '', urls: [], hasContent: false };
+    
+    // 提取文件
+    const files: File[] = dt.files ? Array.from(dt.files) : [];
+    const images = files.filter(f => f.type.startsWith('image/'));
+    
+    // 提取文字(过滤掉[图片]占位符)
+    let text = dt.getData('text/plain') || '';
+    text = text.replace(/\[图片\]/g, '').trim();
+    
+    // 提取HTML
+    const html = dt.getData('text/html') || '';
+    
+    // 提取URL
+    const uriList = dt.getData('text/uri-list') || '';
+    const urls = uriList.split('\n')
+      .map(url => url.trim())
+      .filter(url => url && !url.startsWith('#'));
+    
+    const hasContent = files.length > 0 || text.length > 0 || urls.length > 0;
+    
+    return { files, images, text, html, urls, hasContent };
+  }
+
+  /**
+   * AI文件拖拽悬停
+   */
+  onAIFileDragOver(event: DragEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+    this.aiDesignDragOver = true;
+  }
+
+  /**
+   * AI文件拖拽离开
+   */
+  onAIFileDragLeave(event: DragEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+    this.aiDesignDragOver = false;
+  }
+
+  /**
+   * AI文件拖拽放下(增强版 - 支持企业微信)
+   */
+  async onAIFileDrop(event: DragEvent): Promise<void> {
+    event.preventDefault();
+    event.stopPropagation();
+    this.aiDesignDragOver = false;
+    
+    console.log('📥 [AI对话拖拽] 放下');
+    console.log('📥 [AI对话拖拽] 环境:', this.isWeChatWorkEnv() ? '企业微信' : '普通浏览器');
+    
+    // 🔍 详细打印数据结构(调试用)
+    this.logDragDataStructure(event, 'AI对话区域');
+    
+    // 📊 提取内容
+    const content = this.extractDragContent(event);
+    console.log('📊 [AI对话拖拽] 提取的内容:', content);
+    
+    if (!content.hasContent) {
+      console.warn('⚠️ [AI对话拖拽] 未检测到有效内容');
+      return;
+    }
+    
+    try {
+      this.aiDesignUploading = true;
+      
+      // 1. 处理图片文件
+      if (content.images.length > 0) {
+        console.log(`📸 [AI对话拖拽] 处理${content.images.length}张图片`);
+        await this.processAIFiles(content.images);
+      }
+      
+      // 2. 处理其他文件(PDF/CAD)
+      const otherFiles = content.files.filter(f => !f.type.startsWith('image/'));
+      if (otherFiles.length > 0) {
+        console.log(`📄 [AI对话拖拽] 处理${otherFiles.length}个文件`);
+        await this.processAIFiles(otherFiles);
+      }
+      
+      // 3. 处理图片URL(下载后添加)
+      if (content.urls.length > 0) {
+        console.log(`🔗 [AI对话拖拽] 处理${content.urls.length}个URL`);
+        await this.processImageUrls(content.urls);
+      }
+      
+      // 4. 处理文字内容(添加到AI对话输入框)
+      if (content.text) {
+        console.log(`📝 [AI对话拖拽] 处理文字内容: ${content.text.substring(0, 50)}...`);
+        this.appendTextToAIInput(content.text);
+      }
+      
+      this.cdr.markForCheck();
+      
+    } catch (error) {
+      console.error('❌ [AI对话拖拽] 处理失败:', error);
+      window?.fmode?.alert?.('处理拖拽内容失败,请重试');
+    } finally {
+      this.aiDesignUploading = false;
+    }
+  }
+
+  /**
+   * 处理AI文件(图片和文档)
+   */
+  private async processAIFiles(files: FileList | File[]): Promise<void> {
+    if (this.aiDesignUploadedFiles.length + files.length > 3) {
+      window?.fmode?.alert('最多只能上传3个参考文件');
+      return;
+    }
+    
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+      const extension = file.name.split('.').pop()?.toLowerCase() || '';
+      const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension);
+      
+      let url = '';
+      if (isImage) {
+        // 读取为DataURL用于预览
+        url = await this.readFileAsDataURL(file);
+      }
+      
+      this.aiDesignUploadedFiles.push({
+        file: file,
+        name: file.name,
+        size: file.size,
+        extension: extension,
+        url: url
+      });
+      
+      if (isImage) {
+        this.aiDesignUploadedImages.push(url);
+      }
+      
+      console.log(`✅ [AI对话拖拽] 文件已添加: ${file.name}`);
+    }
+  }
+
+  /**
+   * 读取文件为DataURL
+   */
+  private readFileAsDataURL(file: File): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = (e: any) => resolve(e.target.result);
+      reader.onerror = (e) => reject(e);
+      reader.readAsDataURL(file);
+    });
+  }
+
+  /**
+   * 处理图片URL(下载并添加)
+   */
+  private async processImageUrls(urls: string[]): Promise<void> {
+    for (const url of urls) {
+      try {
+        // 检查是否为图片URL
+        if (!this.isImageUrl(url)) {
+          console.warn(`⚠️ [AI对话拖拽] 非图片URL: ${url}`);
+          continue;
+        }
+        
+        console.log(`📥 [AI对话拖拽] 下载图片: ${url}`);
+        
+        // 使用fetch下载图片
+        const response = await fetch(url);
+        if (!response.ok) {
+          throw new Error(`下载失败: ${response.statusText}`);
+        }
+        
+        const blob = await response.blob();
+        const fileName = this.extractFileNameFromUrl(url) || `image_${Date.now()}.jpg`;
+        const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
+        
+        console.log(`✅ [AI对话拖拽] 下载完成: ${fileName}`);
+        
+        // 添加到文件列表
+        await this.processAIFiles([file]);
+        
+      } catch (error) {
+        console.error(`❌ [AI对话拖拽] 下载失败: ${url}`, error);
+      }
+    }
+  }
+
+  /**
+   * 将文字内容添加到AI输入框
+   */
+  private appendTextToAIInput(text: string): void {
+    // 如果有AI对话输入框,添加文字
+    if (this.aiChatInput) {
+      this.aiChatInput = this.aiChatInput 
+        ? `${this.aiChatInput}\n${text}` 
+        : text;
+    } else {
+      this.aiChatInput = text;
+    }
+    
+    console.log(`✅ [AI对话拖拽] 文字已添加到输入框: ${text.substring(0, 50)}...`);
+  }
+  
+  // ==================== End: AI对话拖拽支持 ====================
+  
   /**
    * 删除参考图片 - 同时删除服务器文件
    */
@@ -3217,39 +3832,6 @@ ${context}
     this.cdr.markForCheck();
   }
 
-  /**
-   * 处理文件拖拽进入
-   */
-  onAIFileDragOver(event: DragEvent): void {
-    event.preventDefault();
-    event.stopPropagation();
-    this.aiDesignDragOver = true;
-  }
-
-  /**
-   * 处理文件拖拽离开
-   */
-  onAIFileDragLeave(event: DragEvent): void {
-    event.preventDefault();
-    event.stopPropagation();
-    this.aiDesignDragOver = false;
-  }
-
-  /**
-   * 处理文件拖放
-   */
-  async onAIFileDrop(event: DragEvent): Promise<void> {
-    event.preventDefault();
-    event.stopPropagation();
-    this.aiDesignDragOver = false;
-
-    const files = event.dataTransfer?.files;
-    if (!files || files.length === 0) {
-      return;
-    }
-
-    await this.handleAIFileUpload(Array.from(files));
-  }
 
   /**
    * 处理AI文件上传(统一处理点击和拖拽)