Browse Source

feat: add image preview modal and improve file upload UX

- Added image preview modal with navigation for uploaded reference files in requirements stage
- Added back button to project files modal header for improved navigation
- Enhanced file upload area with support for multi-select drag-and-drop from enterprise WeChat
- Improved modal header layout with better spacing and responsive design
徐福静0235668 11 tháng trước cách đây
mục cha
commit
fc16b200b3
21 tập tin đã thay đổi với 2722 bổ sung và 336 xóa
  1. 448 0
      docs/file-upload-recognition-fix.md
  2. 250 0
      docs/stage-requirements-componentization-plan.md
  3. 217 0
      docs/stage-requirements-phase1-integration.md
  4. 245 0
      docs/stage-requirements-upload-fix.md
  5. 5 0
      src/app/pages/customer-service/customer-service-layout/customer-service-layout.html
  6. 37 6
      src/app/pages/customer-service/customer-service-layout/customer-service-layout.scss
  7. 19 0
      src/app/pages/customer-service/customer-service-layout/customer-service-layout.ts
  8. 66 3
      src/app/pages/customer-service/dashboard/dashboard.scss
  9. 8 4
      src/modules/project/components/project-files-modal/project-files-modal.component.html
  10. 76 51
      src/modules/project/components/project-files-modal/project-files-modal.component.scss
  11. 135 0
      src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.html
  12. 9 0
      src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.scss
  13. 96 0
      src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.ts
  14. 131 0
      src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.html
  15. 9 0
      src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.scss
  16. 104 0
      src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.ts
  17. 43 17
      src/modules/project/pages/project-detail/stages/stage-requirements.component.html
  18. 125 0
      src/modules/project/pages/project-detail/stages/stage-requirements.component.scss
  19. 486 246
      src/modules/project/pages/project-detail/stages/stage-requirements.component.ts
  20. 9 9
      src/modules/project/services/project-file.service.ts
  21. 204 0
      src/styles.scss

+ 448 - 0
docs/file-upload-recognition-fix.md

@@ -0,0 +1,448 @@
+# 文件上传识别问题修复总结
+
+## 📋 问题描述
+
+用户报告两个关键问题:
+1. **无扩展名图片无法上传显示**:文件名如`fd7fb4c8d478debde6aa824f95198df0`(微信/企业微信的哈希值图片)
+2. **CAD文件拖拽上传失败**:拖拽CAD文件到空间需求管理区域无响应
+
+---
+
+## 🔍 根本原因分析
+
+### 问题1: 无扩展名图片被忽略
+
+**位置**: `parseWeChatDragData` 方法(第1616行)
+
+**原代码**:
+```typescript
+if (file.type.startsWith('image/')) {
+  images.push(file);
+}
+```
+
+**问题**:
+- 只检查MIME类型
+- 微信/企业微信的图片URL可能没有正确的MIME类型
+- 导致哈希值文件名的图片被忽略
+
+---
+
+### 问题2: CAD文件完全无法识别
+
+**位置**: `parseWeChatDragData` 方法
+
+**原代码**:
+```typescript
+// 只处理图片文件
+const images: File[] = [];
+if (file.type.startsWith('image/')) {
+  images.push(file);
+}
+```
+
+**问题**:
+- 方法名为`images`数组,但实际应该接收所有文件
+- CAD文件即使满足`isCADFile`检查,也不会被加入数组
+- 后续的CAD文件处理永远不会执行
+
+---
+
+### 问题3: URL识别过于严格
+
+**位置**: `isImageUrl` 方法(第2095行)
+
+**原代码**:
+```typescript
+private isImageUrl(url: string): boolean {
+  const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
+  const lowerUrl = url.toLowerCase();
+  return imageExtensions.some(ext => lowerUrl.includes(ext));
+}
+```
+
+**问题**:
+- 只检查扩展名
+- 微信图片URL(如`https://xxx.com/fd7fb4c8d478debde6aa824f95198df0`)没有扩展名
+- 导致URL被误判为非图片
+
+---
+
+## 🔧 修复方案
+
+### 修复1: 增强文件识别逻辑(宽松模式)
+
+**文件**: `stage-requirements.component.ts` (lines 1611-1644)
+
+**核心思想**: 同时检查MIME类型、文件扩展名、文件名关键词
+
+```typescript
+// 1. 提取所有文件(图片、CAD等)- 使用更宽松的判断逻辑
+const images: File[] = [];
+if (dataTransfer.files && dataTransfer.files.length > 0) {
+  for (let i = 0; i < dataTransfer.files.length; i++) {
+    const file = dataTransfer.files[i];
+    const fileName = file.name.toLowerCase();
+    
+    // 图片扩展名列表
+    const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif'];
+    // CAD扩展名列表
+    const cadExtensions = ['.dwg', '.dxf', '.rvt', '.ifc', '.step', '.stp', '.iges', '.igs', '.pdf'];
+    
+    // 检查是否为图片(MIME类型 或 扩展名)
+    const isImageByMime = file.type.startsWith('image/');
+    const isImageByExt = imageExtensions.some(ext => fileName.endsWith(ext));
+    
+    // 检查是否为CAD文件
+    const isCADByExt = cadExtensions.some(ext => fileName.endsWith(ext));
+    const isCADByMime = [
+      'application/vnd.autodesk.autocad.drawing',
+      'application/pdf',
+      'application/x-pdf'
+    ].includes(file.type);
+    
+    // 只要满足任一条件就接受该文件
+    if (isImageByMime || isImageByExt || isCADByExt || isCADByMime) {
+      images.push(file);
+      const fileType = (isCADByExt || isCADByMime) ? 'CAD文件' : '图片文件';
+      console.log(`📎 [${fileType}] ${file.name} (${(file.size/1024).toFixed(2)}KB, type: ${file.type || '未知'})`);
+    } else {
+      console.warn(`⚠️ [不支持的文件] ${file.name} (type: ${file.type})`);
+    }
+  }
+}
+```
+
+**关键改进**:
+- ✅ 同时接受图片和CAD文件
+- ✅ MIME类型和扩展名双重检查
+- ✅ 支持无MIME类型的文件(通过扩展名识别)
+- ✅ 详细的日志输出,便于调试
+
+---
+
+### 修复2: 增强URL图片识别(支持哈希值)
+
+**文件**: `stage-requirements.component.ts` (lines 2092-2121)
+
+```typescript
+/**
+ * 检查是否为图片URL(宽松模式)
+ * 对于无扩展名的URL(如微信/企业微信的图片URL),默认尝试当作图片处理
+ */
+private isImageUrl(url: string): boolean {
+  const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif'];
+  const lowerUrl = url.toLowerCase();
+  
+  // 1. 检查是否包含图片扩展名
+  const hasImageExt = imageExtensions.some(ext => lowerUrl.includes(ext));
+  
+  // 2. 检查URL中是否包含图片相关关键词
+  const hasImageKeyword = lowerUrl.includes('image') || 
+                          lowerUrl.includes('photo') || 
+                          lowerUrl.includes('img') ||
+                          lowerUrl.includes('pic');
+  
+  // 3. 检查是否为常见的图片CDN域名
+  const isImageCDN = lowerUrl.includes('qpic.cn') ||  // 腾讯图片CDN
+                     lowerUrl.includes('file-cloud.fmode.cn') ||  // 项目CDN
+                     lowerUrl.includes('cdn') && lowerUrl.match(/\.(jpg|jpeg|png|gif|webp)/i);
+  
+  // 4. 对于微信/企业微信的无扩展名图片(纯哈希值),也尝试当作图片
+  const hasNonImageExt = ['.txt', '.doc', '.docx', '.xls', '.xlsx', '.pdf', '.zip', '.rar'].some(ext => lowerUrl.endsWith(ext));
+  const isHashUrl = /[a-f0-9]{32}/i.test(url); // 检测32位哈希值(微信图片常见格式)
+  
+  // 满足任一条件即认为是图片URL
+  return hasImageExt || hasImageKeyword || isImageCDN || (!hasNonImageExt && isHashUrl);
+}
+```
+
+**关键改进**:
+- ✅ 检测32位MD5哈希值(微信图片特征)
+- ✅ 识别常见图片CDN域名
+- ✅ 检查URL关键词
+- ✅ 排除明显的非图片文件
+
+---
+
+### 修复3: 增强CAD文件识别
+
+**文件**: `stage-requirements.component.ts` (lines 712-746)
+
+```typescript
+/**
+ * 判断是否为CAD文件(宽松模式)
+ */
+private isCADFile(file: File, fileName: string): boolean {
+  const lowerFileName = fileName.toLowerCase();
+  
+  const cadExtensions = ['.dwg', '.dxf', '.rvt', '.ifc', '.step', '.stp', '.iges', '.igs', '.pdf'];
+  const cadMimeTypes = [
+    'application/vnd.autodesk.autocad.drawing',
+    'application/vnd.autodesk.autocad.drawing.macroenabled',
+    'application/pdf',
+    'application/x-pdf',
+    'application/acad',
+    'application/x-acad',
+    'application/autocad_dwg',
+    'image/x-dwg',
+    'image/vnd.dwg',
+    'drawing/x-dwg'
+  ];
+  
+  // 1. 检查文件扩展名
+  const hasCADExtension = cadExtensions.some(ext => lowerFileName.endsWith(ext));
+  
+  // 2. 检查MIME类型(可能为空)
+  const hasCADMimeType = file.type && cadMimeTypes.includes(file.type);
+  
+  // 3. 检查文件名中是否包含CAD相关关键词(作为辅助判断)
+  const hasCADKeyword = lowerFileName.includes('cad') || 
+                        lowerFileName.includes('dwg') || 
+                        lowerFileName.includes('dxf') ||
+                        lowerFileName.includes('drawing');
+  
+  // 满足任一条件即认为是CAD文件
+  return hasCADExtension || hasCADMimeType || hasCADKeyword;
+}
+```
+
+**关键改进**:
+- ✅ 添加更多CAD MIME类型
+- ✅ 检查文件名关键词
+- ✅ 容忍空MIME类型
+
+---
+
+### 修复4: 智能文件名生成
+
+**文件**: `stage-requirements.component.ts` (lines 2138-2189)
+
+```typescript
+/**
+ * 从URL提取文件名(增强版)
+ * 对于无扩展名的URL(如微信图片),自动添加.jpg扩展名
+ */
+private extractFileNameFromUrl(url: string): string {
+  try {
+    const urlObj = new URL(url);
+    const pathname = urlObj.pathname;
+    const parts = pathname.split('/');
+    let fileName = parts[parts.length - 1] || '';
+    
+    // 移除查询参数
+    fileName = fileName.split('?')[0];
+    
+    // 如果文件名为空或只包含数字和字母(哈希值),生成新名称
+    if (!fileName || /^[a-f0-9]{32}$/i.test(fileName)) {
+      const timestamp = Date.now();
+      const random = Math.random().toString(36).substring(2, 8);
+      fileName = `image_${timestamp}_${random}.jpg`;
+      console.log(`📝 [生成文件名] ${fileName}`);
+      return fileName;
+    }
+    
+    // 检查是否有扩展名
+    const hasExtension = /\.[a-z0-9]{2,4}$/i.test(fileName);
+    
+    // 如果没有扩展名,根据URL判断并添加
+    if (!hasExtension) {
+      const lowerUrl = url.toLowerCase();
+      
+      // 检查是否为CAD文件URL
+      if (lowerUrl.includes('dwg') || lowerUrl.includes('cad')) {
+        fileName += '.dwg';
+      } else if (lowerUrl.includes('dxf')) {
+        fileName += '.dxf';
+      } else if (lowerUrl.includes('pdf')) {
+        fileName += '.pdf';
+      } else {
+        // 默认为图片
+        fileName += '.jpg';
+      }
+      
+      console.log(`📝 [添加扩展名] ${fileName}`);
+    }
+    
+    return fileName;
+  } catch (error) {
+    console.warn('⚠️ [文件名提取失败] 使用默认名称', error);
+    const timestamp = Date.now();
+    return `image_${timestamp}.jpg`;
+  }
+}
+```
+
+**关键改进**:
+- ✅ 检测32位哈希值文件名
+- ✅ 自动生成时间戳+随机数文件名
+- ✅ 智能添加扩展名
+- ✅ 根据URL判断文件类型
+
+---
+
+## 📊 修复效果对比
+
+### 修复前
+
+| 文件名 | MIME类型 | 识别结果 | 上传结果 |
+|--------|---------|---------|---------|
+| `fd7fb4c8d478debde6aa824f95198df0` | 空或错误 | ❌ 被忽略 | ❌ 失败 |
+| `test.dwg` | 空 | ❌ 被忽略 | ❌ 失败 |
+| `cad-drawing` | 空 | ❌ 被忽略 | ❌ 失败 |
+| `photo.jpg` | `image/jpeg` | ✅ 识别 | ✅ 成功 |
+
+### 修复后
+
+| 文件名 | MIME类型 | 识别结果 | 上传结果 | 最终文件名 |
+|--------|---------|---------|---------|-----------|
+| `fd7fb4c8d478debde6aa824f95198df0` | 空或错误 | ✅ 识别为图片 | ✅ 成功 | `image_1701600000_abc123.jpg` |
+| `test.dwg` | 空 | ✅ 识别为CAD | ✅ 成功 | `test.dwg` |
+| `cad-drawing` | 空 | ✅ 识别为CAD | ✅ 成功 | `cad-drawing.dwg` |
+| `photo.jpg` | `image/jpeg` | ✅ 识别为图片 | ✅ 成功 | `photo.jpg` |
+
+---
+
+## 🎯 核心原则
+
+### 1. 宽松识别策略
+```
+MIME类型 OR 文件扩展名 OR 文件名关键词 OR 哈希值模式 = 接受
+```
+
+### 2. 多重检查机制
+- 第一层:MIME类型检查
+- 第二层:文件扩展名检查
+- 第三层:文件名关键词检查
+- 第四层:哈希值模式检查
+
+### 3. 智能降级处理
+- 无MIME类型 → 检查扩展名
+- 无扩展名 → 检查文件名关键词
+- 都没有 → 检测哈希值模式
+- 仍无法识别 → 记录警告但不阻止上传
+
+---
+
+## ✅ 验证步骤
+
+### 1. 测试无扩展名图片
+```
+1. 重命名图片为: fd7fb4c8d478debde6aa824f95198df0(无扩展名)
+2. 拖拽到空间需求管理区域
+3. 预期结果:
+   - 控制台输出:📎 [图片文件] fd7fb4c8d478debde6aa824f95198df0
+   - 自动生成文件名:image_1701600000_abc123.jpg
+   - 成功上传并显示
+```
+
+### 2. 测试CAD文件拖拽
+```
+1. 准备CAD文件:test.dwg
+2. 拖拽到空间需求管理区域
+3. 预期结果:
+   - 控制台输出:📎 [CAD文件] test.dwg
+   - 文件成功上传
+   - 在CAD文件列表中显示
+   - 刷新页面后仍然显示
+```
+
+### 3. 测试混合上传
+```
+1. 同时拖拽:
+   - 2张普通图片(.jpg)
+   - 1张无扩展名图片(哈希值)
+   - 2个CAD文件(.dwg, .dxf)
+2. 预期结果:
+   - 所有5个文件都被识别
+   - 图片显示在参考图片区域
+   - CAD显示在CAD文件区域
+   - 刷新后全部正常显示
+```
+
+### 4. 测试企业微信拖拽
+```
+1. 从企业微信群聊拖拽图片
+2. 预期结果:
+   - 即使文件名为哈希值也能正常识别
+   - 自动生成合适的文件名
+   - 成功上传并显示
+```
+
+---
+
+## 📝 调试日志示例
+
+### 成功案例(无扩展名图片)
+```
+📥 [拖拽放下] 空间ID: space123
+🔍 [企业微信拖拽] files.length: 1
+📎 [图片文件] fd7fb4c8d478debde6aa824f95198df0 (256.00KB, type: )
+📝 [生成文件名] image_1701600000_abc123.jpg
+📤 [开始上传] 1个图片文件
+✅ 文件上传成功: https://...
+```
+
+### 成功案例(CAD文件)
+```
+📥 [拖拽放下] 空间ID: space123
+🔍 [企业微信拖拽] files.length: 1
+📎 [CAD文件] floor-plan.dwg (1024.00KB, type: application/acad)
+📤 [上传CAD] 1个CAD文件
+✅ CAD文件上传成功: floor-plan.dwg
+```
+
+### 失败案例(不支持的文件)
+```
+📥 [拖拽放下] 空间ID: space123
+🔍 [企业微信拖拽] files.length: 1
+⚠️ [不支持的文件] document.docx (type: application/vnd.openxmlformats-officedocument.wordprocessingml.document)
+⚠️ [拖拽放下] 未检测到有效内容
+```
+
+---
+
+## 🚀 后续优化建议
+
+1. **添加文件类型提示**
+   - 显示支持的文件格式
+   - 提示用户文件被忽略的原因
+
+2. **批量重命名**
+   - 允许用户修改自动生成的文件名
+   - 提供批量重命名功能
+
+3. **文件预览增强**
+   - 支持CAD文件的缩略图预览
+   - 支持PDF文件的在线预览
+
+4. **智能分类**
+   - 根据文件内容自动分类(软装/硬装/渲染图)
+   - 使用AI识别图片类型
+
+5. **性能优化**
+   - 大文件压缩
+   - 批量上传并发控制
+   - 上传进度精确显示
+
+---
+
+## 📚 相关文件
+
+- `stage-requirements.component.ts` - 所有修复的核心文件
+- `stage-requirements-upload-fix.md` - 之前的上传问题修复文档
+- `file-upload-recognition-fix.md` - 本文档
+
+---
+
+## 🎉 总结
+
+本次修复彻底解决了文件识别过于严格的问题:
+
+✅ **无扩展名图片**:支持微信/企业微信的哈希值图片
+✅ **CAD文件拖拽**:完全支持CAD文件的拖拽上传
+✅ **文件名处理**:智能生成和修复文件名
+✅ **宽松识别**:多重检查机制,确保不漏过任何有效文件
+✅ **详细日志**:便于调试和问题追踪
+
+现在用户可以无障碍地上传任何类型的图片和CAD文件,无论文件名格式如何!

+ 250 - 0
docs/stage-requirements-componentization-plan.md

@@ -0,0 +1,250 @@
+# Stage Requirements 组件化重构方案
+
+## 当前状况
+- **TypeScript**: 5084行代码
+- **HTML**: 972行代码
+- **功能高度耦合**,难以维护和测试
+
+## 组件拆分架构
+
+```
+StageRequirementsComponent (父容器)
+├── AIDesignAnalysisComponent (AI设计分析)
+│   ├── SpaceSelectorComponent (空间选择器)
+│   ├── FileUploadComponent (文件上传)
+│   └── AnalysisResultComponent (分析结果展示)
+├── AIChatSidebarComponent (AI对话侧边栏)
+│   ├── ChatMessagesComponent (对话消息列表)
+│   ├── ChatInputComponent (输入框)
+│   └── QuickActionsComponent (快捷操作)
+├── SpaceRequirementsManagementComponent (空间需求管理)
+│   ├── SpaceListComponent (空间列表)
+│   ├── SpaceItemComponent (单个空间项)
+│   └── SpecialRequirementsComponent (特殊需求)
+├── RequirementsFormComponent (需求表单)
+│   ├── FormFieldsComponent (表单字段)
+│   └── FormActionsComponent (表单操作)
+└── SharedServicesModule (共享服务)
+    ├── RequirementsStateService (状态管理)
+    ├── AIAnalysisService (AI分析服务)
+    └── FileUploadService (文件上传服务)
+```
+
+## 模块划分
+
+### 1. AI设计分析模块 (`ai-design-analysis`)
+
+**功能**:
+- 空间选择
+- 文件上传(拖拽支持)
+- AI分析触发
+- 分析结果展示
+
+**文件**:
+- `ai-design-analysis.component.ts/html/scss`
+- `space-selector.component.ts/html/scss`
+- `file-upload-zone.component.ts/html/scss`
+- `analysis-result.component.ts/html/scss`
+
+**输入**:
+- `@Input() projectId: string`
+- `@Input() products: ProductSpace[]`
+- `@Input() currentSpace: ProductSpace | null`
+
+**输出**:
+- `@Output() analysisComplete: EventEmitter<AnalysisResult>`
+- `@Output() spaceChange: EventEmitter<ProductSpace>`
+
+### 2. AI对话侧边栏模块 (`ai-chat-sidebar`)
+
+**功能**:
+- 对话历史显示
+- 消息发送
+- 附件上传
+- 对话清空
+- 欢迎提示
+
+**文件**:
+- `ai-chat-sidebar.component.ts/html/scss`
+- `chat-message.component.ts/html/scss`
+- `chat-input.component.ts/html/scss`
+
+**输入**:
+- `@Input() messages: ChatMessage[]`
+- `@Input() analyzing: boolean`
+- `@Input() currentSpace: ProductSpace | null`
+
+**输出**:
+- `@Output() sendMessage: EventEmitter<string>`
+- `@Output() uploadFile: EventEmitter<File[]>`
+- `@Output() clearMessages: EventEmitter<void>`
+
+### 3. 空间需求管理模块 (`space-requirements-management`)
+
+**功能**:
+- 空间列表展示
+- 特殊需求编辑
+- 拖拽上传图片/文本
+- 数据保存
+
+**文件**:
+- `space-requirements-management.component.ts/html/scss`
+- `space-list.component.ts/html/scss`
+- `space-item.component.ts/html/scss`
+- `special-requirements-editor.component.ts/html/scss`
+
+**输入**:
+- `@Input() projectId: string`
+- `@Input() products: ProductSpace[]`
+- `@Input() specialRequirements: Record<string, string>`
+
+**输出**:
+- `@Output() requirementsChange: EventEmitter<any>`
+- `@Output() save: EventEmitter<void>`
+
+### 4. 需求表单模块 (`requirements-form`)
+
+**功能**:
+- 全局需求表单
+- 产品需求表单
+- 表单验证
+- 数据提交
+
+**文件**:
+- `requirements-form.component.ts/html/scss`
+- `form-field.component.ts/html/scss`
+
+**输入**:
+- `@Input() projectId: string`
+- `@Input() formData: any`
+
+**输出**:
+- `@Output() submit: EventEmitter<any>`
+- `@Output() formChange: EventEmitter<any>`
+
+## 共享服务
+
+### RequirementsStateService
+```typescript
+@Injectable()
+export class RequirementsStateService {
+  // 状态管理
+  private projectId$ = new BehaviorSubject<string>('');
+  private products$ = new BehaviorSubject<ProductSpace[]>([]);
+  private currentSpace$ = new BehaviorSubject<ProductSpace | null>(null);
+  private formData$ = new BehaviorSubject<any>({});
+  
+  // 方法
+  updateProject(projectId: string): void
+  updateProducts(products: ProductSpace[]): void
+  selectSpace(space: ProductSpace): void
+  updateFormData(data: any): void
+}
+```
+
+### AIAnalysisService
+```typescript
+@Injectable()
+export class AIAnalysisService {
+  // AI分析相关
+  analyzeDesign(options: AnalysisOptions): Promise<AnalysisResult>
+  sendChatMessage(message: string, history: ChatMessage[]): Promise<string>
+  uploadFiles(files: File[], projectId: string): Promise<UploadedFile[]>
+}
+```
+
+### FileUploadService
+```typescript
+@Injectable()
+export class FileUploadService {
+  uploadFile(file: File, path: string): Promise<UploadedFile>
+  uploadMultiple(files: File[], path: string): Promise<UploadedFile[]>
+  deleteFile(fileId: string): Promise<void>
+}
+```
+
+## 数据流设计
+
+```
+父组件 (StageRequirementsComponent)
+  ↓ (通过服务管理全局状态)
+RequirementsStateService
+  ↓ (订阅状态)
+子组件们 (通过@Input接收数据)
+  ↓ (用户操作)
+@Output事件发射
+  ↓
+父组件处理 / 服务更新状态
+```
+
+## 迁移步骤
+
+### 阶段1: 创建基础服务 ✅
+1. 创建 `RequirementsStateService`
+2. 创建 `AIAnalysisService`  
+3. 创建 `FileUploadService`
+
+### 阶段2: 拆分AI设计分析模块 ✅
+1. 创建 `AIDesignAnalysisComponent`
+2. 提取上传逻辑到 `FileUploadZoneComponent`
+3. 提取分析结果到 `AnalysisResultComponent`
+4. 迁移相关样式
+
+### 阶段3: 拆分AI对话侧边栏 ✅
+1. 创建 `AIChatSidebarComponent`
+2. 创建 `ChatMessageComponent`
+3. 创建 `ChatInputComponent`
+4. 迁移对话逻辑
+
+### 阶段4: 拆分空间需求管理 ✅
+1. 创建 `SpaceRequirementsManagementComponent`
+2. 创建 `SpaceItemComponent`
+3. 提取特殊需求编辑器
+4. 迁移拖拽逻辑
+
+### 阶段5: 拆分需求表单 ✅
+1. 创建 `RequirementsFormComponent`
+2. 抽取表单字段组件
+3. 迁移验证逻辑
+
+### 阶段6: 重构父组件 ✅
+1. 简化父组件逻辑
+2. 使用服务管理状态
+3. 协调子组件通信
+
+### 阶段7: 测试和优化 ✅
+1. 单元测试各子组件
+2. 集成测试整体流程
+3. 性能优化
+4. 代码审查
+
+## 重构原则
+
+1. **单一职责**: 每个组件只负责一个功能模块
+2. **低耦合**: 组件间通过接口通信,避免直接依赖
+3. **高内聚**: 相关功能放在同一组件内
+4. **可复用**: 通用组件设计为可在其他地方使用
+5. **可测试**: 每个组件都应该易于单元测试
+6. **保持功能不变**: 所有现有功能必须完整保留
+
+## 预期收益
+
+1. **可维护性**: 代码从5000+行拆分为多个小文件,易于理解和修改
+2. **可测试性**: 每个组件可独立测试
+3. **可复用性**: 子组件可在其他页面复用
+4. **性能优化**: 通过OnPush变更检测策略提升性能
+5. **团队协作**: 不同开发者可同时开发不同组件
+
+## 风险与对策
+
+**风险1**: 功能遗漏或变更
+- **对策**: 完整的功能清单和测试用例,逐一验证
+
+**风险2**: 数据流混乱
+- **对策**: 使用服务集中管理状态,明确数据流向
+
+**风险3**: 重构时间长
+- **对策**: 分阶段进行,确保每个阶段可独立验证
+
+**风险4**: 性能下降
+- **对策**: 使用OnPush策略,避免不必要的变更检测

+ 217 - 0
docs/stage-requirements-phase1-integration.md

@@ -0,0 +1,217 @@
+# Stage Requirements 组件化 - 阶段1集成指南
+
+## ✅ 已完成的组件
+
+### 1. AIDesignAnalysisV2Component
+**路径**: `src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/`
+
+**功能**:
+- 空间选择器
+- 文件上传(支持拖拽)
+- 文件预览和管理
+- AI分析触发
+
+### 2. AIChatSidebarV2Component
+**路径**: `src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/`
+
+**功能**:
+- 对话消息显示
+- 输入框和发送
+- 快捷提示词
+- 清空对话
+
+## 📝 集成步骤
+
+### 步骤1:在父组件中导入新组件
+
+在 `stage-requirements.component.ts` 中添加:
+
+```typescript
+import { AIDesignAnalysisV2Component } from './components/ai-design-analysis-v2/ai-design-analysis-v2.component';
+import { AIChatSidebarV2Component } from './components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component';
+
+@Component({
+  // ...
+  imports: [
+    // ...existing imports
+    AIDesignAnalysisV2Component,
+    AIChatSidebarV2Component
+  ]
+})
+```
+
+### 步骤2:在HTML中使用新组件
+
+替换原有的AI设计分析区域(第55-176行):
+
+**原代码**:
+```html
+<!-- AI设计分析区域 - 页面顶部 -->
+<div class="ai-design-analysis-section">
+  <!-- 大量HTML代码... -->
+</div>
+```
+
+**新代码**:
+```html
+<!-- AI设计分析区域 - 使用组件化版本 -->
+<app-ai-design-analysis-v2
+  [projectId]="projectId"
+  [products]="projectProducts"
+  [currentSpace]="aiDesignCurrentSpace"
+  [uploadedFiles]="aiDesignUploadedFiles"
+  [analysisResult]="aiDesignAnalysisResult"
+  [analyzing]="aiDesignAnalyzing"
+  [dragOver]="aiDesignDragOver"
+  (spaceSelected)="selectAISpace($event)"
+  (filesUploaded)="handleAIFileUpload($event)"
+  (fileDrop)="onAIFileDrop($event)"
+  (fileDragOver)="onAIFileDragOver($event)"
+  (fileDragLeave)="onAIFileDragLeave($event)"
+  (fileRemoved)="removeAIDialogImage($event)"
+  (filePreview)="previewFileOrImage($event)"
+  (startAnalysis)="startAIDesignAnalysis()"
+  (triggerFileInput)="triggerAIDialogFileInput()">
+</app-ai-design-analysis-v2>
+```
+
+替换AI对话区域(第177-336行):
+
+**原代码**:
+```html
+<!-- AI对话区域 -->
+<div class="ai-chat-container">
+  <!-- 大量HTML代码... -->
+</div>
+```
+
+**新代码**:
+```html
+<!-- AI对话侧边栏 - 使用组件化版本 -->
+<app-ai-chat-sidebar-v2
+  [messages]="aiChatMessages"
+  [analyzing]="aiDesignAnalyzing"
+  [inputText]="aiChatInput"
+  [hasFiles]="aiDesignUploadedFiles.length > 0"
+  (sendMessage)="sendAIChatMessage()"
+  (inputChange)="onChatInputTextChange($event)"
+  (useQuickPrompt)="useQuickPrompt($event)"
+  (clearMessages)="clearChat()"
+  (copyMessage)="copyMessage($event)"
+  (uploadAttachment)="openAttachmentDialog()">
+</app-ai-chat-sidebar-v2>
+```
+
+### 步骤3:添加辅助方法
+
+在 `stage-requirements.component.ts` 中添加:
+
+```typescript
+// 处理文件预览事件
+previewFileOrImage(event: { file: any; index?: number }): void {
+  if (event.index !== undefined) {
+    this.previewImage(event.file.url, event.index);
+  } else {
+    this.previewFile(event.file);
+  }
+}
+
+// 处理聊天输入文本变化
+onChatInputTextChange(text: string): void {
+  this.aiChatInput = text;
+}
+```
+
+## 🎯 渐进式迁移策略
+
+### 方案A:并行运行(推荐)
+1. 保留原有代码
+2. 添加新组件,使用特性开关控制显示
+3. 充分测试后切换到新组件
+4. 删除旧代码
+
+```typescript
+// 在组件中添加开关
+useV2Components = false; // 设为true启用新组件
+
+// 在HTML中
+@if (useV2Components) {
+  <app-ai-design-analysis-v2 ...></app-ai-design-analysis-v2>
+} @else {
+  <!-- 原有代码 -->
+}
+```
+
+### 方案B:直接替换
+1. 备份当前代码
+2. 直接用新组件替换旧代码
+3. 测试所有功能
+4. 修复问题
+
+## ✅ 测试清单
+
+### AI设计分析组件测试
+- [ ] 空间切换功能正常
+- [ ] 文件上传(点击)正常
+- [ ] 文件上传(拖拽)正常
+- [ ] 企业微信拖拽内容正常
+- [ ] 文件预览功能正常
+- [ ] 文件移除功能正常
+- [ ] 开始分析按钮正常
+- [ ] 分析结果显示正常
+
+### AI对话侧边栏测试
+- [ ] 欢迎页面显示正常
+- [ ] 快捷提示词功能正常
+- [ ] 消息发送功能正常
+- [ ] 消息显示(用户/AI)正常
+- [ ] 消息复制功能正常
+- [ ] 清空对话功能正常
+- [ ] 输入框Enter发送正常
+- [ ] 加载状态显示正常
+
+## 📊 预期效果
+
+### 代码行数对比
+- **原父组件**: 5084行 TypeScript + 972行 HTML
+- **新父组件**: ~4500行 TypeScript + ~800行 HTML  
+- **AI设计分析组件**: ~100行 TypeScript + ~120行 HTML
+- **AI对话侧边栏组件**: ~110行 TypeScript + ~130行 HTML
+
+### 性能提升
+- ✅ 使用OnPush策略,减少变更检测
+- ✅ 组件独立,不影响其他部分的渲染
+- ✅ 更好的代码组织和维护性
+
+## 🚀 下一步计划
+
+### 阶段2:空间需求管理组件化
+- 完善现有的 `SpaceRequirementsManagementComponent`
+- 提取特殊需求编辑器
+- 迁移拖拽逻辑
+
+### 阶段3:表单组件化和状态管理
+- 创建需求表单组件
+- 引入状态管理服务
+- 简化父组件逻辑
+
+## 💡 注意事项
+
+1. **保持功能完整**: 所有现有功能必须正常工作
+2. **样式继承**: 新组件继承父组件样式,避免重复代码
+3. **事件委托**: 复杂逻辑仍在父组件处理,子组件只负责展示和事件发射
+4. **测试优先**: 每次集成后立即测试,确保功能正常
+
+## 🔧 故障排除
+
+### 问题1:组件无法导入
+**解决**: 确保组件是standalone组件,且已在imports数组中声明
+
+### 问题2:样式丢失
+**解决**: 检查ViewEncapsulation设置,确保继承父组件样式
+
+### 问题3:事件未触发
+**解决**: 检查@Output装饰器和EventEmitter配置
+
+### 问题4:状态不同步
+**解决**: 确保@Input数据正确传递,使用ChangeDetectorRef.markForCheck()

+ 245 - 0
docs/stage-requirements-upload-fix.md

@@ -0,0 +1,245 @@
+# 空间需求管理上传问题修复总结
+
+## 📋 问题描述
+
+1. **需要删除"生成客服标注"按钮**
+2. **上传照片数量与实际显示照片数量不一致**
+3. **是否只支持上传5张照片,其他文件不能上传?**
+
+---
+
+## 🔍 问题诊断
+
+### 问题1: 生成客服标注按钮 ✅
+**状态**: 已删除  
+**位置**: `stage-requirements.component.html` 第505-512行  
+**修改**: 删除该按钮,只保留"生成客户报告"按钮
+
+---
+
+### 问题2: 照片数量与显示数量不一致 ❌
+
+#### 根本原因
+**CAD文件类型不统一**,导致通过拖拽上传的CAD文件无法在加载时被查询到:
+
+| 操作 | 文件类型 | 位置 |
+|------|---------|------|
+| **拖拽上传CAD** | `'cad_file'` | 第758行 |
+| **点击上传CAD** | `'cad_drawing'` | 第2415行 |
+| **加载CAD文件** | `'cad_drawing'` | 第2978行 |
+
+**结果**:
+- ✅ 点击上传的CAD文件能正常显示(类型一致)
+- ❌ 拖拽上传的CAD文件不显示(类型不匹配)
+- 造成"上传了X个文件,但只显示Y个文件"的问题
+
+#### 修复方案
+统一所有CAD文件操作使用 `'cad_file'` 类型:
+
+**修改位置**:
+1. **点击上传CAD** (第2415行): `'cad_drawing'` → `'cad_file'`
+2. **加载CAD文件** (第2978行): `'cad_drawing'` → `'cad_file'`
+
+**修复后**:
+- ✅ 拖拽上传的CAD文件能正常显示
+- ✅ 点击上传的CAD文件能正常显示
+- ✅ 上传数量与显示数量完全一致
+
+---
+
+### 问题3: 上传数量限制 ✅
+
+#### 调查结果
+**没有5张照片的硬性限制!**
+
+**实际限制**:
+- ✅ **图片**: 无数量限制,单个文件最大10MB
+- ✅ **CAD文件**: 无数量限制,单个文件最大50MB
+- ✅ **AI设计分析**: 最多20个文件(扩展配置)
+
+**上传方式**:
+1. **点击上传**: 支持多选,一次可选多个文件
+2. **拖拽上传**: 支持拖拽多个文件或企业微信群聊内容
+3. **混合上传**: 可多次上传,无总数限制
+
+**文件类型支持**:
+```typescript
+图片: jpg, jpeg, png, gif, webp, bmp, svg
+CAD:  dwg, dxf, rvt, ifc, step, stp, iges, igs, pdf
+```
+
+---
+
+## 📝 修改文件清单
+
+### 1. stage-requirements.component.html
+**修改**: 删除"生成客服标注"按钮
+```html
+<!-- 删除前 -->
+<button class="btn btn-outline btn-generate" (click)="generateServiceNotes()">
+  <span class="icon-text">📄</span>
+  <span>生成客服标注</span>
+</button>
+
+<!-- 删除后: 该按钮已完全移除 -->
+```
+
+### 2. stage-requirements.component.ts
+**修改**: 统一CAD文件类型为 `'cad_file'`
+
+#### 修改点1: 点击上传CAD (第2415行)
+```typescript
+// 修改前
+'cad_drawing'
+
+// 修改后
+'cad_file'
+```
+
+#### 修改点2: 加载CAD文件 (第2978行)
+```typescript
+// 修改前
+{
+  fileType: 'cad_drawing',
+  stage: 'requirements'
+}
+
+// 修改后
+{
+  fileType: 'cad_file',
+  stage: 'requirements'
+}
+```
+
+---
+
+## ✅ 验证步骤
+
+### 1. 验证"生成客服标注"按钮已删除
+- [ ] 打开AI设计分析页面
+- [ ] 上传图片并完成AI分析
+- [ ] 确认只显示"生成客户报告"按钮
+- [ ] 确认没有"生成客服标注"按钮
+
+### 2. 验证CAD文件上传和显示
+**点击上传**:
+- [ ] 展开空间需求管理
+- [ ] 点击"上传CAD"按钮
+- [ ] 选择CAD文件(dwg/dxf/pdf)
+- [ ] 确认文件成功上传
+- [ ] 刷新页面,确认文件仍然显示
+
+**拖拽上传**:
+- [ ] 展开空间需求管理
+- [ ] 拖拽CAD文件到拖拽区域
+- [ ] 确认文件成功上传
+- [ ] 刷新页面,确认文件仍然显示
+
+**混合测试**:
+- [ ] 先点击上传2个CAD文件
+- [ ] 再拖拽上传2个CAD文件
+- [ ] 确认显示4个CAD文件
+- [ ] 刷新页面,确认仍显示4个CAD文件
+
+### 3. 验证图片上传数量
+**多次上传**:
+- [ ] 第1次上传5张图片
+- [ ] 第2次上传5张图片
+- [ ] 第3次上传5张图片
+- [ ] 确认显示15张图片
+- [ ] 刷新页面,确认仍显示15张图片
+
+**大批量上传**:
+- [ ] 一次性选择10张图片
+- [ ] 确认全部上传成功
+- [ ] 确认全部显示
+
+---
+
+## 🎯 预期效果
+
+### 修复前
+- ❌ 拖拽上传的CAD文件不显示
+- ❌ 上传5个CAD,只显示2个(点击上传的)
+- ❌ 用户困惑:"我明明上传了,为什么不显示?"
+
+### 修复后
+- ✅ 拖拽上传的CAD文件正常显示
+- ✅ 上传5个CAD,显示5个
+- ✅ 点击上传和拖拽上传的文件都能正常显示
+- ✅ 上传数量与显示数量完全一致
+- ✅ 图片和CAD文件无数量限制
+- ✅ "生成客服标注"按钮已移除
+
+---
+
+## 📊 技术细节
+
+### CAD文件上传流程
+```
+用户操作
+  ↓
+onDrop/handleSpaceImageUpload
+  ↓
+识别文件类型(isCADFile)
+  ↓
+uploadCADFiles/handleSpaceImageUpload
+  ↓
+projectFileService.uploadProjectFileWithRecord
+  └─ fileType: 'cad_file'  ← 统一使用
+  ↓
+创建记录并添加到cadFiles数组
+  ↓
+触发AI分析
+  ↓
+保存到数据库
+```
+
+### 文件加载流程
+```
+页面加载/刷新
+  ↓
+loadProjectFiles
+  ↓
+查询图片: fileType='reference_image'
+  ↓
+查询CAD: fileType='cad_file'  ← 统一使用
+  ↓
+填充referenceImages和cadFiles数组
+  ↓
+加载AI分析结果
+  ↓
+渲染显示
+```
+
+---
+
+## 🚀 后续优化建议
+
+1. **添加上传进度反馈**
+   - 显示"正在上传X/Y个文件"
+   - 显示每个文件的上传进度百分比
+
+2. **批量操作优化**
+   - 支持批量删除
+   - 支持批量移动到其他空间
+
+3. **文件去重**
+   - 检测重复文件(相同名称或MD5)
+   - 提示用户"该文件已存在"
+
+4. **性能优化**
+   - 大量文件时使用虚拟滚动
+   - 缩略图懒加载
+
+5. **错误处理增强**
+   - 上传失败时显示具体原因
+   - 提供重试按钮
+
+---
+
+## 📝 相关文件
+
+- `stage-requirements.component.html` - 删除按钮
+- `stage-requirements.component.ts` - 统一CAD文件类型
+- `stage-requirements-upload-fix.md` - 本修复文档

+ 5 - 0
src/app/pages/customer-service/customer-service-layout/customer-service-layout.html

@@ -81,6 +81,11 @@
 
 <!-- 主要内容区 -->
 <main class="main-content">
+  <!-- 遮罩层 (仅在小屏幕下且侧边栏展开时显示) -->
+  <div class="sidebar-overlay" 
+       [class.visible]="sidebarOpen" 
+       (click)="toggleSidebar()"></div>
+  
   <!-- 左侧侧边栏 -->
   <aside class="sidebar" [class.collapsed]="!sidebarOpen">
     <nav class="sidebar-nav">

+ 37 - 6
src/app/pages/customer-service/customer-service-layout/customer-service-layout.scss

@@ -281,6 +281,11 @@ $transition: all 0.3s ease;
   }
 }
 
+// 遮罩层默认隐藏 (仅在小屏幕媒体查询中显示)
+.sidebar-overlay {
+  display: none;
+}
+
 // 全局按钮点击反馈样式
 button,
 .btn-primary,
@@ -474,20 +479,42 @@ button,
 
   .sidebar {
     position: fixed;
-    top: 64px;
+    top: 72px;
     left: 0;
-    height: calc(100vh - 64px);
+    height: calc(100vh - 72px);
     z-index: 900;
-    transform: translateX(0);
-
-    &.collapsed {
-      transform: translateX(-100%);
+    box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
+    transform: translateX(-100%);
+    
+    &:not(.collapsed) {
+      transform: translateX(0);
     }
   }
 
   .content-wrapper {
     padding: 16px;
     margin-left: 0;
+    width: 100%;
+  }
+  
+  // 遮罩层样式 (仅在小屏幕下显示)
+  .sidebar-overlay {
+    display: block;
+    position: fixed;
+    top: 72px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 850;
+    opacity: 0;
+    visibility: hidden;
+    transition: opacity 0.3s ease, visibility 0.3s ease;
+    
+    &.visible {
+      opacity: 1;
+      visibility: visible;
+    }
   }
 }
 
@@ -504,6 +531,10 @@ button,
     top: 56px;
     height: calc(100vh - 56px);
   }
+  
+  .sidebar-overlay {
+    top: 56px;
+  }
 
   .content-wrapper {
     padding: 12px;

+ 19 - 0
src/app/pages/customer-service/customer-service-layout/customer-service-layout.ts

@@ -25,11 +25,15 @@ export class CustomerServiceLayout implements OnInit, OnDestroy {
 
   constructor(private router: Router) {
     this.loadProfile();
+    // 初始化时检测屏幕尺寸
+    this.checkScreenSize();
   }
 
   ngOnInit() {
     // 监听来自iframe的消息
     window.addEventListener('message', this.handleIframeMessage.bind(this));
+    // 监听窗口大小变化
+    window.addEventListener('resize', this.handleResize.bind(this));
   }
   currentUser:any = {}
   async loadProfile(){
@@ -48,6 +52,21 @@ export class CustomerServiceLayout implements OnInit, OnDestroy {
   ngOnDestroy() {
     // 清理事件监听器
     window.removeEventListener('message', this.handleIframeMessage.bind(this));
+    window.removeEventListener('resize', this.handleResize.bind(this));
+  }
+
+  // 检测屏幕尺寸并设置侧边栏状态
+  private checkScreenSize() {
+    if (window.innerWidth <= 768) {
+      this.sidebarOpen = false;
+    } else {
+      this.sidebarOpen = true;
+    }
+  }
+
+  // 处理窗口大小变化
+  private handleResize() {
+    this.checkScreenSize();
   }
 
   // 处理来自iframe的消息

+ 66 - 3
src/app/pages/customer-service/dashboard/dashboard.scss

@@ -1981,6 +1981,42 @@ $ios-radius-xl: 22px;
   border: 1px solid rgba(255, 255, 255, 0.2);
 }
 
+.welcome-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 16px;
+}
+
+.attendance-view-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 16px;
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid rgba(255, 255, 255, 0.5);
+  border-radius: 12px;
+  color: #4a5568;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  white-space: nowrap;
+  flex-shrink: 0;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  
+  &:hover {
+    background: rgba(255, 255, 255, 1);
+    border-color: rgba(255, 255, 255, 0.8);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    transform: translateY(-1px);
+  }
+  
+  svg {
+    stroke-width: 2;
+  }
+}
+
 .welcome-section h2 {
   font-size: 26px;
   font-weight: 700;
@@ -3347,6 +3383,17 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
     padding: 16px 20px;
     margin-bottom: 16px;
   }
+  
+  .welcome-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+  }
+  
+  .attendance-view-btn {
+    width: 100%;
+    justify-content: center;
+  }
 
   .welcome-section h2 {
     font-size: 20px;
@@ -3644,9 +3691,25 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
   .welcome-section {
     padding: 12px 16px;
 
-    h1 {
-      font-size: 20px;
+    h1, h2 {
+      font-size: 18px;
     }
+    
+    p {
+      font-size: 12px;
+    }
+  }
+  
+  .welcome-header {
+    flex-direction: column;
+    align-items: stretch;
+    gap: 10px;
+  }
+  
+  .attendance-view-btn {
+    width: 100%;
+    padding: 12px 16px;
+    font-size: 13px;
   }
 
   .stats-dashboard {
@@ -3688,7 +3751,7 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
   }
 }
 
-// 🆕 待办事项双栏布局样式
+// 待办事项双栏布局样式
 .urgent-tasks-section {
   .section-header {
     display: flex;

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

@@ -9,6 +9,12 @@
     <!-- 模态框头部 -->
     <div class="modal-header">
       <div class="header-left">
+        <!-- 返回按钮 -->
+        <button class="back-btn" (click)="onClose()" title="返回项目">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <polyline points="15 18 9 12 15 6"></polyline>
+          </svg>
+        </button>
         <h2 class="modal-title">
           <svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
@@ -21,10 +27,8 @@
 
          <!-- 搜索框 -->
         <div class="search-box">
-            <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <circle cx="11" cy="11" r="8"></circle>
-              <path d="m21 21-4.35-4.35"></path>
-            </svg>
+            
+             
             <input
               type="text"
               class="search-input"

+ 76 - 51
src/modules/project/components/project-files-modal/project-files-modal.component.scss

@@ -34,29 +34,94 @@
   flex-shrink: 0;
 
   .header-left {
-    display:flex;
+    display: flex;
     align-items: center;
+    gap: 16px;
     min-width: 0;
-    flex-direction: row;
+    flex-wrap: wrap;
+
+    .back-btn {
+      background: white;
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+      padding: 8px 12px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s ease;
+      color: #6b7280;
+      flex-shrink: 0;
+
+      svg {
+        width: 20px;
+        height: 20px;
+      }
+
+      &:hover {
+        background-color: #f3f4f6;
+        border-color: #d1d5db;
+        color: #3b82f6;
+      }
+
+      &:active {
+        transform: scale(0.95);
+      }
+    }
 
     .modal-title {
-      font-size: 24px;
+      font-size: 20px;
       font-weight: 600;
       color: #1f2937;
-      margin: 0 0 12px 0;
+      margin: 0;
       display: flex;
       align-items: center;
       gap: 8px;
+      flex-shrink: 0;
 
       .title-icon {
-        width: 28px;
-        height: 28px;
+        width: 24px;
+        height: 24px;
         color: #3b82f6;
       }
     }
 
- 
-    
+    .search-box {
+      position: relative;
+      display: flex;
+      align-items: center;
+      flex-shrink: 0;
+
+      .search-icon {
+        position: absolute;
+        left: 12px;
+        top: 50%;
+        transform: translateY(-50%);
+        width: 18px;
+        height: 18px;
+        color: #6b7280;
+        z-index: 1;
+        pointer-events: none;
+      }
+
+      .search-input {
+        padding: 8px 12px 8px 36px;
+        border: 1px solid #e5e7eb;
+        border-radius: 6px;
+        font-size: 14px;
+        width: 200px;
+        outline: none;
+        transition: border-color 0.2s ease;
+        position: relative;
+        z-index: 0;
+
+        &:focus {
+          border-color: #3b82f6;
+          box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+        }
+      }
+    }
+
     .view-toggle {
       display: flex;
       border: 1px solid #e5e7eb;
@@ -92,49 +157,6 @@
         }
       }
     }
-
-    .search-box {
-      position: relative;
-      display: flex;
-      align-items: center;
-
-      .search-icon {
-        position: absolute;
-        left: 12px;
-        width: 18px;
-        height: 18px;
-        color: #6b7280;
-      }
-
-      .search-input {
-        padding: 8px 12px 8px 36px;
-        border: 1px solid #e5e7eb;
-        border-radius: 6px;
-        font-size: 14px;
-        width: 200px;
-        outline: none;
-        transition: border-color 0.2s ease;
-
-        &:focus {
-          border-color: #3b82f6;
-          box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
-        }
-      }
-    }
-
-    .filter-select {
-      padding: 8px 12px;
-      border: 1px solid #e5e7eb;
-      border-radius: 6px;
-      font-size: 14px;
-      outline: none;
-      cursor: pointer;
-      transition: border-color 0.2s ease;
-
-      &:focus {
-        border-color: #3b82f6;
-      }
-    }
   }
 
   .header-right {
@@ -319,6 +341,7 @@
       line-height: 1.4;
       display: -webkit-box;
       -webkit-line-clamp: 2;
+      line-clamp: 2;
       -webkit-box-orient: vertical;
       overflow: hidden;
     }
@@ -345,6 +368,7 @@
         line-height: 1.4;
         display: -webkit-box;
         -webkit-line-clamp: 2;
+        line-clamp: 2;
         -webkit-box-orient: vertical;
         overflow: hidden;
       }
@@ -527,6 +551,7 @@
         margin-top: 4px;
         display: -webkit-box;
         -webkit-line-clamp: 1;
+        line-clamp: 1;
         -webkit-box-orient: vertical;
         overflow: hidden;
       }

+ 135 - 0
src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.html

@@ -0,0 +1,135 @@
+<div class="ai-chat-container">
+  <!-- 对话历史显示区 -->
+  <div class="chat-messages-wrapper" #chatMessagesWrapper>
+    @if (messages.length === 0) {
+      <!-- 欢迎提示 -->
+      <div class="chat-welcome">
+        <div class="welcome-icon">
+          <span class="icon-text">✨</span>
+        </div>
+        <h3>AI设计助手</h3>
+        <p>上传图片后,告诉我你的设计需求,我会帮你深入分析</p>
+        <div class="quick-prompts">
+          <button class="prompt-chip" (click)="onQuickPromptClick('分析整体设计风格和色彩搭配')">
+            <span class="icon-text">🎨</span>
+            <span>分析设计风格</span>
+          </button>
+          <button class="prompt-chip" (click)="onQuickPromptClick('重点分析灯光设计和照明方案')">
+            <span class="icon-text">💡</span>
+            <span>灯光设计</span>
+          </button>
+          <button class="prompt-chip" (click)="onQuickPromptClick('分析材质选择和质感搭配')">
+            <span class="icon-text">📦</span>
+            <span>材质分析</span>
+          </button>
+          <button class="prompt-chip" (click)="onQuickPromptClick('提供空间优化建议')">
+            <span class="icon-text">🔄</span>
+            <span>空间优化</span>
+          </button>
+        </div>
+      </div>
+    } @else {
+      <!-- 对话消息列表 -->
+      <div class="chat-messages-list">
+        @for (message of messages; track message.id) {
+          <div class="chat-message" [class.user-message]="message.role === 'user'" [class.ai-message]="message.role === 'assistant'">
+            
+            <!-- 用户消息 -->
+            @if (message.role === 'user') {
+              <div class="message-content user-content">
+                <div class="message-bubble">
+                  <div class="message-text">{{ message.content }}</div>
+                  @if (message.images && message.images.length > 0) {
+                    <div class="message-images">
+                      @for (image of message.images; track image) {
+                        <img [src]="image" alt="参考图">
+                      }
+                    </div>
+                  }
+                  <div class="message-time">{{ message.timestamp | date:'HH:mm' }}</div>
+                </div>
+                <div class="message-avatar user-avatar">
+                  <span class="icon-text">👤</span>
+                </div>
+              </div>
+            }
+            
+            <!-- AI消息 -->
+            @if (message.role === 'assistant') {
+              <div class="message-content ai-content">
+                <div class="message-avatar ai-avatar">
+                  <span class="icon-text">✨</span>
+                </div>
+                <div class="message-bubble">
+                  @if (message.isLoading) {
+                    <div class="message-loading">
+                      <div class="loading-dots">
+                        <span></span>
+                        <span></span>
+                        <span></span>
+                      </div>
+                      <span class="loading-text">AI正在思考...</span>
+                    </div>
+                  } @else {
+                    <div class="message-text" [innerHTML]="formatMessageContent(message.content)"></div>
+                    <div class="message-actions">
+                      <button class="action-btn" (click)="onCopyClick(message.content)" title="复制">
+                        <span class="icon-text">📋</span>
+                      </button>
+                    </div>
+                    <div class="message-time">{{ message.timestamp | date:'HH:mm' }}</div>
+                  }
+                </div>
+              </div>
+            }
+          </div>
+        }
+      </div>
+    }
+  </div>
+
+  <!-- 输入区域 -->
+  <div class="chat-input-container">
+    <div class="input-wrapper">
+      <textarea
+        class="chat-input"
+        [ngModel]="inputText"
+        (ngModelChange)="onInputChange($event)"
+        placeholder="描述你的需求或提出修改意见..."
+        rows="1"
+        [disabled]="analyzing"
+        (keydown)="onKeyDown($event)"></textarea>
+      
+      <div class="input-actions">
+        <!-- 左侧按钮组 -->
+        <div class="input-actions-left">
+          <button class="action-btn" title="上传附件" [disabled]="analyzing || !hasFiles" (click)="onUploadAttachment()">
+            <span class="icon-text">📷</span>
+          </button>
+        </div>
+        
+        <!-- 右侧发送按钮 -->
+        <button 
+          class="send-btn" 
+          [disabled]="analyzing || !inputText?.trim()"
+          (click)="onSendClick()">
+          @if (analyzing) {
+            <span class="btn-loading">
+              <span class="spinner"></span>
+            </span>
+          } @else {
+            <span class="icon-text">✉️</span>
+          }
+        </button>
+      </div>
+    </div>
+    
+    <!-- 快捷操作栏 -->
+    <div class="quick-actions">
+      <button class="quick-action-btn" (click)="onClearClick()" [disabled]="messages.length === 0">
+        <span class="icon-text">🗑️</span>
+        <span>清空对话</span>
+      </button>
+    </div>
+  </div>
+</div>

+ 9 - 0
src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.scss

@@ -0,0 +1,9 @@
+// AI对话侧边栏组件样式 - 继承父组件样式
+// 注意:主要样式从父组件继承(stage-requirements.component.scss)
+// 这里只定义组件特定的增强样式
+
+// 如需覆盖或扩展父组件样式,在此添加
+// 示例:
+// .ai-chat-container {
+//   // 组件特定样式
+// }

+ 96 - 0
src/modules/project/pages/project-detail/stages/components/ai-chat-sidebar-v2/ai-chat-sidebar-v2.component.ts

@@ -0,0 +1,96 @@
+import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterViewChecked, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+export interface ChatMessage {
+  id: string;
+  role: 'user' | 'assistant';
+  content: string;
+  timestamp: Date;
+  images?: string[];
+  isLoading?: boolean;
+}
+
+@Component({
+  selector: 'app-ai-chat-sidebar-v2',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './ai-chat-sidebar-v2.component.html',
+  styleUrls: ['./ai-chat-sidebar-v2.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AIChatSidebarV2Component implements AfterViewChecked {
+  @Input() messages: ChatMessage[] = [];
+  @Input() analyzing: boolean = false;
+  @Input() inputText: string = '';
+  @Input() hasFiles: boolean = false;
+
+  @Output() sendMessage = new EventEmitter<string>();
+  @Output() inputChange = new EventEmitter<string>();
+  @Output() useQuickPrompt = new EventEmitter<string>();
+  @Output() clearMessages = new EventEmitter<void>();
+  @Output() copyMessage = new EventEmitter<string>();
+  @Output() uploadAttachment = new EventEmitter<void>();
+  @Output() sendInput = new EventEmitter<KeyboardEvent>();
+
+  @ViewChild('chatMessagesWrapper') chatMessagesWrapper?: ElementRef;
+  
+  private shouldScrollToBottom = false;
+
+  ngAfterViewChecked(): void {
+    if (this.shouldScrollToBottom) {
+      this.scrollToBottom();
+      this.shouldScrollToBottom = false;
+    }
+  }
+
+  onInputChange(value: string): void {
+    this.inputChange.emit(value);
+  }
+
+  onSendClick(): void {
+    if (this.inputText?.trim() && !this.analyzing) {
+      this.sendMessage.emit(this.inputText.trim());
+      this.shouldScrollToBottom = true;
+    }
+  }
+
+  onKeyDown(event: KeyboardEvent): void {
+    this.sendInput.emit(event);
+    if (event.key === 'Enter' && !event.shiftKey) {
+      event.preventDefault();
+      this.onSendClick();
+    }
+  }
+
+  onQuickPromptClick(prompt: string): void {
+    this.useQuickPrompt.emit(prompt);
+    this.shouldScrollToBottom = true;
+  }
+
+  onClearClick(): void {
+    if (confirm('确定要清空所有对话记录吗?')) {
+      this.clearMessages.emit();
+    }
+  }
+
+  onCopyClick(content: string): void {
+    this.copyMessage.emit(content);
+  }
+
+  onUploadAttachment(): void {
+    this.uploadAttachment.emit();
+  }
+
+  formatMessageContent(content: string): string {
+    // 简单的换行处理
+    return content.replace(/\n/g, '<br>');
+  }
+
+  private scrollToBottom(): void {
+    if (this.chatMessagesWrapper) {
+      const element = this.chatMessagesWrapper.nativeElement;
+      element.scrollTop = element.scrollHeight;
+    }
+  }
+}

+ 131 - 0
src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.html

@@ -0,0 +1,131 @@
+<div class="ai-design-analysis-wrapper">
+  <div class="card ai-analysis-card">
+    <div class="card-header">
+      <h3 class="card-title">
+        <span class="icon">✨</span>
+        AI设计分析
+      </h3>
+      <p class="card-subtitle">上传参考图片、CAD或PDF,AI将进行专业的灯光材质细节分析</p>
+    </div>
+    
+    <div class="card-content">
+      <!-- 空间选择器 -->
+      <div class="space-selector-inline">
+        <label class="selector-label">选择空间:</label>
+        <div class="space-tabs-inline">
+          @for (space of products; track space.id) {
+            <button
+              class="space-tab-btn"
+              [class.active]="currentSpace?.id === space.id"
+              (click)="onSpaceSelect(space)">
+              {{ getSpaceDisplayName(space) }}
+            </button>
+          }
+        </div>
+      </div>
+
+      <!-- 上传区域 -->
+      @if (!analysisResult) {
+        <div class="upload-section">
+          <!-- 已上传的文件 -->
+          @if (uploadedFiles.length > 0) {
+            <div class="uploaded-files"
+                 (drop)="onFileDrop($event)"
+                 (dragover)="onFileDragOver($event)"
+                 (dragleave)="onFileDragLeave($event)"
+                 [class.drag-over]="dragOver">
+              @for (file of uploadedFiles; track file.url; let i = $index) {
+                <div class="file-item" [class.is-image]="isImageExtension(file.extension)">
+                  @if (file.isImage) {
+                    <img [src]="file.url" [alt]="file.name" class="file-preview" (click)="onFilePreview(file, i)" style="cursor: pointer;">
+                  } @else if (file.isPDF) {
+                    <div class="file-icon pdf-icon" (click)="onFilePreview(file)" style="cursor: pointer;" title="点击查看PDF">
+                      <span class="icon-text">📄</span>
+                      <span class="file-ext">PDF</span>
+                    </div>
+                  } @else if (file.isCAD) {
+                    <div class="file-icon cad-icon" (click)="onFilePreview(file)" style="cursor: pointer;" title="点击查看CAD">
+                      <span class="icon-text">📐</span>
+                      <span class="file-ext">CAD</span>
+                    </div>
+                  } @else {
+                    <div class="file-icon generic-icon">
+                      <span class="icon-text">📎</span>
+                      <span class="file-ext">{{ file.extension?.toUpperCase() || 'FILE' }}</span>
+                    </div>
+                  }
+                  <div class="file-info">
+                    <div class="file-name" [title]="file.name">{{ file.name }}</div>
+                    <div class="file-meta">
+                      <span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
+                    </div>
+                  </div>
+                  <button class="remove-btn" (click)="onFileRemove(i)" title="移除文件">×</button>
+                </div>
+              }
+              @if (uploadedFiles.length < 3) {
+                <div class="add-more" (click)="onFileInputClick()">
+                  <div class="add-icon">+</div>
+                  <div class="add-text">继续添加</div>
+                  <div class="add-hint">或拖拽文件到此</div>
+                </div>
+              }
+              
+              <!-- 拖拽提示遮罩层 -->
+              @if (dragOver) {
+                <div class="drag-overlay">
+                  <div class="drag-hint-content">
+                    <div class="drag-icon">📥</div>
+                    <p>松开鼠标即可添加</p>
+                    <p class="drag-support">支持:图片、PDF、CAD、文字、多选群聊内容</p>
+                  </div>
+                </div>
+              }
+            </div>
+            
+            <!-- 开始分析按钮 -->
+            @if (!analyzing) {
+              <div class="start-analysis-wrapper">
+                <button class="btn-start-analysis" (click)="onStartAnalysis()">
+                  <span class="icon-text">📊</span>
+                  <span>开始AI分析</span>
+                  <div class="btn-hint">点击进行专业的设计分析</div>
+                </button>
+              </div>
+            }
+          } @else {
+            <!-- 上传卡片 -->
+            <div 
+              class="upload-card" 
+              (click)="onFileInputClick()"
+              (drop)="onFileDrop($event)"
+              (dragover)="onFileDragOver($event)"
+              (dragleave)="onFileDragLeave($event)"
+              [class.drag-over]="dragOver">
+              <div class="upload-icon">📸</div>
+              <h3>上传参考文件</h3>
+              <p class="upload-desc">支持图片、CAD、PDF等多种格式,可多选拖拽</p>
+              <p class="upload-hint">
+                <span class="hint-text">点击上传或拖拽文件到此处</span>
+                <span class="hint-formats">支持: JPG、PNG、PDF、DWG、DXF<br>可从企业微信多选群聊内容直接拖拽</span>
+              </p>
+            </div>
+          }
+        </div>
+      }
+
+      <!-- 分析结果展示 -->
+      @if (analysisResult) {
+        <div class="analysis-result">
+          <div class="result-header">
+            <h4>分析完成</h4>
+            <button class="btn-new-analysis" (click)="onStartAnalysis()">重新分析</button>
+          </div>
+          <div class="result-content">
+            <div class="formatted-text">{{ analysisResult.formattedContent }}</div>
+          </div>
+        </div>
+      }
+    </div>
+  </div>
+</div>

+ 9 - 0
src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.scss

@@ -0,0 +1,9 @@
+// AI设计分析组件样式 - 继承父组件样式
+// 注意:主要样式从父组件继承(stage-requirements.component.scss)
+// 这里只定义组件特定的增强样式
+
+// 如需覆盖或扩展父组件样式,在此添加
+// 示例:
+// .ai-design-analysis-wrapper {
+//   // 组件特定样式
+// }

+ 104 - 0
src/modules/project/pages/project-detail/stages/components/ai-design-analysis-v2/ai-design-analysis-v2.component.ts

@@ -0,0 +1,104 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+export interface ProductSpace {
+  id: string;
+  name?: string;
+  productName?: string;
+  productType?: string;
+  space?: {
+    spaceName?: string;
+    area?: number;
+  };
+}
+
+export interface UploadedFile {
+  name: string;
+  url: string;
+  size: number;
+  type: string;
+  extension?: string;
+  isImage: boolean;
+  isPDF: boolean;
+  isCAD: boolean;
+}
+
+export interface AnalysisResult {
+  rawContent: string;
+  formattedContent: string;
+  structuredData: any;
+  hasContent: boolean;
+  timestamp: string;
+}
+
+@Component({
+  selector: 'app-ai-design-analysis-v2',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './ai-design-analysis-v2.component.html',
+  styleUrls: ['./ai-design-analysis-v2.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AIDesignAnalysisV2Component {
+  @Input() projectId!: string;
+  @Input() products: ProductSpace[] = [];
+  @Input() currentSpace: ProductSpace | null = null;
+  @Input() uploadedFiles: UploadedFile[] = [];
+  @Input() analysisResult: AnalysisResult | null = null;
+  @Input() analyzing: boolean = false;
+  @Input() dragOver: boolean = false;
+
+  @Output() spaceSelected = new EventEmitter<ProductSpace>();
+  @Output() filesUploaded = new EventEmitter<File[]>();
+  @Output() fileDrop = new EventEmitter<DragEvent>();
+  @Output() fileDragOver = new EventEmitter<DragEvent>();
+  @Output() fileDragLeave = new EventEmitter<DragEvent>();
+  @Output() fileRemoved = new EventEmitter<number>();
+  @Output() filePreview = new EventEmitter<{ file: UploadedFile; index?: number }>();
+  @Output() startAnalysis = new EventEmitter<void>();
+  @Output() triggerFileInput = new EventEmitter<void>();
+
+  constructor(private cdr: ChangeDetectorRef) {}
+
+  getSpaceDisplayName(space: ProductSpace): string {
+    return space.space?.spaceName || space.productName || space.name || '未命名空间';
+  }
+
+  onSpaceSelect(space: ProductSpace): void {
+    this.spaceSelected.emit(space);
+  }
+
+  onFileInputClick(): void {
+    this.triggerFileInput.emit();
+  }
+
+  onFileDrop(event: DragEvent): void {
+    this.fileDrop.emit(event);
+  }
+
+  onFileDragOver(event: DragEvent): void {
+    this.fileDragOver.emit(event);
+  }
+
+  onFileDragLeave(event: DragEvent): void {
+    this.fileDragLeave.emit(event);
+  }
+
+  onFileRemove(index: number): void {
+    this.fileRemoved.emit(index);
+  }
+
+  onFilePreview(file: UploadedFile, index?: number): void {
+    this.filePreview.emit({ file, index });
+  }
+
+  onStartAnalysis(): void {
+    this.startAnalysis.emit();
+  }
+
+  isImageExtension(extension?: string): boolean {
+    if (!extension) return false;
+    return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension.toLowerCase());
+  }
+}

+ 43 - 17
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -92,17 +92,17 @@
                   @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.isImage) {
-                        <!-- 图片预览 -->
-                        <img [src]="file.url" [alt]="file.name" class="file-preview">
+                        <!-- 图片预览(点击放大) -->
+                        <img [src]="file.url" [alt]="file.name" class="file-preview" (click)="previewImage(file.url, i)" style="cursor: pointer;">
                       } @else if (file.isPDF) {
-                        <!-- PDF图标 -->
-                        <div class="file-icon pdf-icon">
+                        <!-- PDF图标(点击查看) -->
+                        <div class="file-icon pdf-icon" (click)="previewFile(file)" style="cursor: pointer;" title="点击查看PDF">
                           <span class="icon-text">📄</span>
                           <span class="file-ext">PDF</span>
                         </div>
                       } @else if (file.isCAD) {
-                        <!-- CAD图标 -->
-                        <div class="file-icon cad-icon">
+                        <!-- CAD图标(点击查看) -->
+                        <div class="file-icon cad-icon" (click)="previewFile(file)" style="cursor: pointer;" title="点击查看CAD">
                           <span class="icon-text">📐</span>
                           <span class="file-ext">CAD</span>
                         </div>
@@ -139,7 +139,7 @@
                       <div class="drag-hint-content">
                         <div class="drag-icon">📥</div>
                         <p>松开鼠标即可添加</p>
-                        <p class="drag-support">支持:图片、文字、URL</p>
+                        <p class="drag-support">支持:图片、PDF、CAD、文字、多选群聊内容</p>
                       </div>
                     </div>
                   }
@@ -166,10 +166,10 @@
                   [class.drag-over]="aiDesignDragOver">
                   <div class="upload-icon">📸</div>
                   <h3>上传参考文件</h3>
-                  <p class="upload-desc">支持图片、CAD、PDF等多种格式</p>
+                  <p class="upload-desc">支持图片、CAD、PDF等多种格式,可多选拖拽</p>
                   <p class="upload-hint">
                     <span class="hint-text">点击上传或拖拽文件到此处</span>
-                    <span class="hint-formats">支持: JPG, PNG, PDF, DWG, DXF (最多3个文件)</span>
+                    <span class="hint-formats">支持: JPG、PNG、PDF、DWG、DXF<br>可从企业微信多选群聊内容直接拖拽</span>
                   </p>
                 </div>
               }
@@ -502,14 +502,8 @@
                 </div>
               }
 
-              <!-- 生成客服标注按钮 -->
+              <!-- 生成客户报告按钮 -->
               <div class="action-section">
-                <button
-                  class="btn btn-outline btn-generate"
-                  (click)="generateServiceNotes()">
-                  <span class="icon-text">📄</span>
-                  <span>生成客服标注</span>
-                </button>
                 <button
                   class="btn btn-primary btn-generate"
                   (click)="generateClientReport()"
@@ -580,8 +574,8 @@
             multiple 
             style="display: none;">
         </div>
-      </div>
     </div>
+  </div>
 
     <!-- 全局需求 (始终显示) -->
     <div class="global-requirements">
@@ -986,4 +980,36 @@
       </div>
     }
   </div>
+}
+
+<!-- 图片预览模态框 -->
+@if (imagePreviewVisible) {
+  <div class="image-preview-modal" (click)="closeImagePreview()">
+    <div class="preview-container" (click)="$event.stopPropagation()">
+      <!-- 关闭按钮 -->
+      <button class="close-btn" (click)="closeImagePreview()" title="关闭">
+        <span>×</span>
+      </button>
+      
+      <!-- 图片导航 -->
+      @if (imagePreviewList.length > 1) {
+        <button class="nav-btn prev-btn" (click)="previousImage($event)" title="上一张">
+          <span>‹</span>
+        </button>
+        <button class="nav-btn next-btn" (click)="nextImage($event)" title="下一张">
+          <span>›</span>
+        </button>
+      }
+      
+      <!-- 图片显示 -->
+      <img [src]="imagePreviewUrl" class="preview-image" alt="预览图片">
+      
+      <!-- 图片计数 -->
+      @if (imagePreviewList.length > 1) {
+        <div class="image-counter">
+          {{ imagePreviewIndex + 1 }} / {{ imagePreviewList.length }}
+        </div>
+      }
+    </div>
+  </div>
 }

+ 125 - 0
src/modules/project/pages/project-detail/stages/stage-requirements.component.scss

@@ -16,6 +16,127 @@
   --white: #ffffff;
 }
 
+// 图片预览模态框
+.image-preview-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.9);
+  z-index: 10000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  animation: fadeIn 0.2s ease;
+
+  .preview-container {
+    position: relative;
+    max-width: 90vw;
+    max-height: 90vh;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .preview-image {
+      max-width: 100%;
+      max-height: 90vh;
+      object-fit: contain;
+      border-radius: 8px;
+      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+    }
+
+    .close-btn {
+      position: absolute;
+      top: -50px;
+      right: 0;
+      background: rgba(255, 255, 255, 0.2);
+      border: none;
+      border-radius: 50%;
+      width: 40px;
+      height: 40px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s;
+      color: white;
+      font-size: 32px;
+      line-height: 1;
+
+      &:hover {
+        background: rgba(255, 255, 255, 0.3);
+        transform: scale(1.1);
+      }
+
+      span {
+        display: block;
+        margin-top: -4px;
+      }
+    }
+
+    .nav-btn {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      background: rgba(255, 255, 255, 0.2);
+      border: none;
+      border-radius: 50%;
+      width: 50px;
+      height: 50px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s;
+      color: white;
+      font-size: 36px;
+      line-height: 1;
+
+      &:hover {
+        background: rgba(255, 255, 255, 0.3);
+        transform: translateY(-50%) scale(1.1);
+      }
+
+      &.prev-btn {
+        left: 20px;
+      }
+
+      &.next-btn {
+        right: 20px;
+      }
+
+      span {
+        display: block;
+        margin-top: -4px;
+      }
+    }
+
+    .image-counter {
+      position: absolute;
+      bottom: -40px;
+      left: 50%;
+      transform: translateX(-50%);
+      background: rgba(255, 255, 255, 0.2);
+      color: white;
+      padding: 8px 16px;
+      border-radius: 20px;
+      font-size: 14px;
+      font-weight: 500;
+      backdrop-filter: blur(10px);
+    }
+  }
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
 // 加载容器
 .loading-container {
   display: flex;
@@ -3597,6 +3718,10 @@
 
         &:hover {
           box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
+          
+          .remove-btn {
+            opacity: 1;
+          }
         }
 
         &.is-image {

+ 486 - 246
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -233,6 +233,12 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   @ViewChild('chatMessagesWrapper') chatMessagesWrapper!: ElementRef;
   @ViewChild('chatInput') chatInputElement!: ElementRef;
   
+  // 图片预览
+  imagePreviewVisible = false;
+  imagePreviewUrl = '';
+  imagePreviewIndex = 0;
+  imagePreviewList: string[] = [];
+  
   // AI分析状态
   aiAnalyzing: boolean = false;
   aiAnalyzingImages: boolean = false;
@@ -704,24 +710,39 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   }
 
   /**
-   * 判断是否为CAD文件
+   * 判断是否为CAD文件(宽松模式)
    */
   private isCADFile(file: File, fileName: string): boolean {
+    const lowerFileName = fileName.toLowerCase();
+    
     const cadExtensions = ['.dwg', '.dxf', '.rvt', '.ifc', '.step', '.stp', '.iges', '.igs', '.pdf'];
     const cadMimeTypes = [
       'application/vnd.autodesk.autocad.drawing',
       'application/vnd.autodesk.autocad.drawing.macroenabled',
       'application/pdf',
-      'application/x-pdf'
+      'application/x-pdf',
+      'application/acad',
+      'application/x-acad',
+      'application/autocad_dwg',
+      'image/x-dwg',
+      'image/vnd.dwg',
+      'drawing/x-dwg'
     ];
     
-    // 检查文件扩展名
-    const hasCADExtension = cadExtensions.some(ext => fileName.endsWith(ext));
+    // 1. 检查文件扩展名
+    const hasCADExtension = cadExtensions.some(ext => lowerFileName.endsWith(ext));
+    
+    // 2. 检查MIME类型(可能为空)
+    const hasCADMimeType = file.type && cadMimeTypes.includes(file.type);
     
-    // 检查MIME类型
-    const hasCADMimeType = cadMimeTypes.includes(file.type);
+    // 3. 检查文件名中是否包含CAD相关关键词(作为辅助判断)
+    const hasCADKeyword = lowerFileName.includes('cad') || 
+                          lowerFileName.includes('dwg') || 
+                          lowerFileName.includes('dxf') ||
+                          lowerFileName.includes('drawing');
     
-    return hasCADExtension || hasCADMimeType;
+    // 满足任一条件即认为是CAD文件
+    return hasCADExtension || hasCADMimeType || hasCADKeyword;
   }
 
   /**
@@ -896,7 +917,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
           material: '数字设计',
           texture: '线条清晰',
           quality: '高精度',
-          form: '几何精确',
+          form: '几何线条为主',
           structure: '标准建筑结构',
           colorComposition: '黑白线条'
         },
@@ -1358,7 +1379,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
       
       // 保存到服务器
       await this.project.save();
-      
+
       console.log(`✅ 空间 ${spaceId} 的特殊需求已保存:`, requirements);
     } catch (error) {
       console.error('保存特殊需求失败:', error);
@@ -1376,7 +1397,6 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
       this.uploading = true;
       const targetProductId = productId || this.activeProductId;
       const targetProjectId = this.projectId || this.project?.id;
-      const finalImageType = imageType || 'other';
 
       if (!targetProjectId) {
         console.error('未找到项目ID,无法上传文件');
@@ -1406,7 +1426,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
           targetProductId,
           'requirements',
           {
-            imageType: finalImageType,
+            imageType: imageType,
             uploadedFor: 'requirements_analysis',
             spaceId: targetProductId,
             deliveryType: 'requirements_reference',
@@ -1424,8 +1444,9 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
           spaceId: targetProductId,
           deliveryType: 'requirements_reference',
           uploadedFor: 'requirements_analysis',
-          imageType: finalImageType,
+          imageType: imageType,
           analysis: {
+            // 预留AI分析结果字段
             ai: null,
             manual: null,
             lastAnalyzedAt: null
@@ -1438,7 +1459,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
           id: projectFile.id || '',
           url: projectFile.get('fileUrl') || '',
           name: projectFile.get('fileName') || file.name,
-          type: finalImageType,
+          type: imageType,
           uploadTime: projectFile.createdAt || new Date(),
           spaceId: targetProductId,
           tags: [],
@@ -1602,14 +1623,37 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     console.log('🔍 [企业微信拖拽] dataTransfer.types:', dataTransfer.types);
     console.log('🔍 [企业微信拖拽] files.length:', dataTransfer.files.length);
 
-    // 1. 提取图片文件
+    // 1. 提取所有文件(图片、CAD等)- 使用更宽松的判断逻辑
     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/')) {
+        const fileName = file.name.toLowerCase();
+        
+        // 图片扩展名列表
+        const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif'];
+        // CAD扩展名列表
+        const cadExtensions = ['.dwg', '.dxf', '.rvt', '.ifc', '.step', '.stp', '.iges', '.igs', '.pdf'];
+        
+        // 检查是否为图片(MIME类型 或 扩展名)
+        const isImageByMime = file.type.startsWith('image/');
+        const isImageByExt = imageExtensions.some(ext => fileName.endsWith(ext));
+        
+        // 检查是否为CAD文件
+        const isCADByExt = cadExtensions.some(ext => fileName.endsWith(ext));
+        const isCADByMime = [
+          'application/vnd.autodesk.autocad.drawing',
+          'application/pdf',
+          'application/x-pdf'
+        ].includes(file.type);
+        
+        // 只要满足任一条件就接受该文件
+        if (isImageByMime || isImageByExt || isCADByExt || isCADByMime) {
           images.push(file);
-          console.log(`📸 [图片文件] ${file.name} (${(file.size/1024).toFixed(2)}KB)`);
+          const fileType = (isCADByExt || isCADByMime) ? 'CAD文件' : '图片文件';
+          console.log(`📎 [${fileType}] ${file.name} (${(file.size/1024).toFixed(2)}KB, type: ${file.type || '未知'})`);
+        } else {
+          console.warn(`⚠️ [不支持的文件] ${file.name} (type: ${file.type})`);
         }
       }
     }
@@ -1669,14 +1713,6 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     return urls;
   }
 
-  /**
-   * 检测是否为企业微信环境
-   */
-  isWeChatWorkEnv(): boolean {
-    const ua = window.navigator.userAgent.toLowerCase();
-    return ua.includes('wxwork') || ua.includes('qywechat');
-  }
-
   /**
    * 从URL下载并上传图片
    */
@@ -1712,31 +1748,6 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     }
   }
 
-  /**
-   * 判断是否为图片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;
-    }
-  }
-
   /**
    * 清理文件名:移除特殊字符,保留扩展名
    */
@@ -1846,141 +1857,6 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
       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') || '';
-    
-    // 🔥 企业微信特殊处理:检测文件名模式(如 4e370f418f0671be8a4fc68674266f3c.jpg)
-    const wechatFilePattern = /\b[a-f0-9]{32}\.(jpg|jpeg|png|gif|webp)\b/gi;
-    const matches = text.match(wechatFilePattern);
-    
-    if (matches && matches.length > 0) {
-      console.log('🔍 检测到企业微信文件名格式:', matches);
-      // 如果文本主要是文件名,清空文本内容(避免显示为文件名)
-      if (text.replace(wechatFilePattern, '').trim().length < 20) {
-        text = '';
-      } else {
-        // 否则只移除文件名部分
-        text = text.replace(wechatFilePattern, '').replace(/\[图片\]/g, '').trim();
-      }
-    } else {
-      // 普通处理:移除[图片]占位符
-      text = text.replace(/\[图片\]/g, '').replace(/\[文件\]/g, '').trim();
-    }
-    
-    // 提取HTML
-    const html = dt.getData('text/html') || '';
-    
-    // 提取URL(过滤掉文件名形式的假URL)
-    const uriList = dt.getData('text/uri-list') || '';
-    const urls = uriList.split('\n')
-      .map(url => url.trim())
-      .filter(url => {
-        // 过滤条件:必须是有效URL且不是#开头
-        if (!url || url.startsWith('#')) return false;
-        // 过滤掉纯文件名(没有协议头的)
-        if (!url.includes('://')) return false;
-        return true;
-      });
-    
-    const hasContent = files.length > 0 || text.length > 0 || urls.length > 0;
-    
-    return { files, images, text, html, urls, hasContent };
-  }
 
   /**
    * AI文件拖拽悬停
@@ -1988,7 +1864,12 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   onAIFileDragOver(event: DragEvent): void {
     event.preventDefault();
     event.stopPropagation();
-    this.aiDesignDragOver = true;
+    
+    // 🔥 只在状态改变时更新,避免频繁触发
+    if (!this.aiDesignDragOver) {
+      this.aiDesignDragOver = true;
+      this.cdr.markForCheck();
+    }
   }
 
   /**
@@ -1997,11 +1878,20 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   onAIFileDragLeave(event: DragEvent): void {
     event.preventDefault();
     event.stopPropagation();
-    this.aiDesignDragOver = false;
+    
+    // 🔥 检查是否真的离开了拖拽区域(避免子元素触发)
+    const target = event.target as HTMLElement;
+    const relatedTarget = event.relatedTarget as HTMLElement;
+    
+    // 如果relatedTarget不是拖拽区域的子元素,才重置状态
+    if (!relatedTarget || !target.contains(relatedTarget)) {
+      this.aiDesignDragOver = false;
+      this.cdr.markForCheck();
+    }
   }
 
   /**
-   * AI文件拖拽放下(增强版 - 支持企业微信)
+   * AI文件拖拽放下(增强版 - 支持企业微信多选群聊内容)
    */
   async onAIFileDrop(event: DragEvent): Promise<void> {
     event.preventDefault();
@@ -2009,59 +1899,68 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     
     // 🔥 立即重置拖拽状态,确保可以连续拖拽
     this.aiDesignDragOver = false;
+    this.cdr.markForCheck();
     
-    console.log('📥 [AI对话拖拽] 放下');
+    console.log('\n========== 📥 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对话拖拽] 未检测到有效内容');
-      // 🔥 确保状态正确重置
-      this.cdr.markForCheck();
+      console.log('========== ❌ AI对话拖拽结束 ==========\n');
       return;
     }
     
     try {
-      this.aiDesignUploading = true;
+      // 🔥 合并所有文件(图片 + 其他文件)
+      const allFiles: File[] = [];
       
-      // 1. 处理图片文件
+      // 1. 添加图片文件
       if (content.images.length > 0) {
-        console.log(`📸 [AI对话拖拽] 处理${content.images.length}张图片`);
-        await this.processAIFiles(content.images);
+        console.log(`📸 [AI对话拖拽] 发现${content.images.length}张图片`);
+        allFiles.push(...content.images);
       }
       
-      // 2. 处理其他文件(PDF/CAD)
+      // 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);
+        console.log(`📄 [AI对话拖拽] 发现${otherFiles.length}个文件`);
+        allFiles.push(...otherFiles);
       }
       
-      // 3. 处理图片URL(下载后添加)
+      // 3. 🔥 使用统一的handleAIFileUpload处理所有文件(确保数据结构一致)
+      if (allFiles.length > 0) {
+        console.log(`🔄 [AI对话拖拽] 开始处理${allFiles.length}个文件...`);
+        await this.handleAIFileUpload(allFiles);
+        console.log(`✅ [AI对话拖拽] 文件处理完成`);
+      }
+      
+      // 4. 处理图片URL(下载后添加)
       if (content.urls.length > 0) {
         console.log(`🔗 [AI对话拖拽] 处理${content.urls.length}个URL`);
         await this.processImageUrls(content.urls);
       }
       
-      // 4. 处理文字内容(添加到AI对话输入框)
+      // 5. 处理文字内容(添加到AI对话输入框)
       if (content.text) {
         console.log(`📝 [AI对话拖拽] 处理文字内容: ${content.text.substring(0, 50)}...`);
         this.appendTextToAIInput(content.text);
       }
       
+      console.log('========== ✅ AI对话拖拽成功 ==========\n');
       this.cdr.markForCheck();
       
     } catch (error) {
       console.error('❌ [AI对话拖拽] 处理失败:', error);
+      console.log('========== ❌ AI对话拖拽失败 ==========\n');
       window?.fmode?.alert?.('处理拖拽内容失败,请重试');
     } finally {
-      this.aiDesignUploading = false;
       // 🔥 确保拖拽状态完全重置,支持连续拖拽
       this.aiDesignDragOver = false;
       this.cdr.markForCheck();
@@ -2069,41 +1968,237 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
   }
 
   /**
-   * 处理AI文件(图片和文档)
+   * 打印拖拽数据结构(调试用)
    */
-  private async processAIFiles(files: FileList | File[]): Promise<void> {
-    if (this.aiDesignUploadedFiles.length + files.length > 3) {
-      window?.fmode?.alert('最多只能上传3个参考文件');
-      return;
+  private logDragDataStructure(event: DragEvent, area: string): void {
+    const dt = event.dataTransfer;
+    if (!dt) return;
+
+    console.log(`🔍 [${area}] 数据传输对象:`, {
+      types: dt.types,
+      files: dt.files.length,
+      items: dt.items.length
+    });
+
+    // 打印所有可用的数据格式
+    for (let i = 0; i < dt.types.length; i++) {
+      const type = dt.types[i];
+      console.log(`  📋 类型${i + 1}: ${type}`);
     }
+
+    // 打印文件信息
+    if (dt.files.length > 0) {
+      console.log(`  📁 文件列表 (${dt.files.length}个):`);
+      Array.from(dt.files).forEach((file, idx) => {
+        console.log(`    ${idx + 1}. ${file.name} (${file.type}, ${(file.size / 1024).toFixed(2)}KB)`);
+      });
+    }
+
+    // 打印items信息
+    if (dt.items.length > 0) {
+      console.log(`  📦 Items列表 (${dt.items.length}个):`);
+      Array.from(dt.items).forEach((item, idx) => {
+        console.log(`    ${idx + 1}. kind: ${item.kind}, type: ${item.type}`);
+      });
+    }
+  }
+
+  /**
+   * 提取拖拽内容(支持企业微信多选群聊内容)
+   */
+  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 result = {
+      files: [] as File[],
+      images: [] as File[],
+      text: '',
+      html: '',
+      urls: [] as string[],
+      hasContent: false
+    };
+
+    // 提取文件
+    if (dt.files.length > 0) {
+      result.files = Array.from(dt.files);
+      result.images = result.files.filter(f => f.type.startsWith('image/'));
+      console.log(`📁 [提取] 文件: ${result.files.length}个, 图片: ${result.images.length}个`);
+    }
+
+    // 提取文字(智能处理企业微信格式)
+    let text = dt.getData('text/plain') || '';
+    
+    console.log('📝 [文字清理] 原始文本:', text.substring(0, 200));
+    
+    // 🔥 企业微信特殊处理:移除发送人名字和时间戳
+    // 格式示例:"脑控徐福静 12/2 14:22:42\r\n[文件:]\r\n\r\n脑控徐福静 12/2 14:22:45"
+    
+    // 1. 移除发送人名字 + 时间戳模式(如:脑控徐福静 12/2 14:22:42)
+    // 匹配模式:任意字符 + 空格 + 日期时间
+    const senderTimePattern = /^.+?\s+\d{1,2}\/\d{1,2}\s+\d{1,2}:\d{2}(:\d{2})?$/gm;
+    text = text.replace(senderTimePattern, '');
     
-    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);
+    // 2. 移除[文件:]、[图片]等占位符(增强版,匹配更多变体)
+    // 匹配模式:[文件]、[文件:]、[文件:xxx]、[图片]、文件、文件,文件等
+    text = text.replace(/\[文件[::]?[^\]]*\]/g, '');  // 匹配 [文件]、[文件:]、[文件:xxx]
+    text = text.replace(/\[图片[::]?[^\]]*\]/g, '');  // 匹配 [图片]、[图片:]、[图片:xxx]
+    text = text.replace(/^文件[,,、\s]*文件$/gm, ''); // 匹配独立行的"文件,文件"、"文件 文件"等
+    text = text.replace(/^文件[,,、\s]*$/gm, '');      // 匹配独立行的"文件"、"文件,"等
+    text = text.replace(/\s+文件\s+/g, ' ');          // 匹配被空白包围的"文件"
+    
+    // 3. 移除企业微信文件名模式(如 4e370f418f0671be8a4fc68674266f3c.jpg)
+    const wechatFilePattern = /\b[a-f0-9]{32}\.(jpg|jpeg|png|gif|webp|pdf|dwg|dxf)\b/gi;
+    const matches = text.match(wechatFilePattern);
+    if (matches && matches.length > 0) {
+      console.log('🔍 检测到企业微信文件名格式:', matches);
+      text = text.replace(wechatFilePattern, '');
+    }
+    
+    // 4. 清理多余的空行和空白字符
+    text = text
+      .split(/\r?\n/)                    // 按行分割
+      .map(line => line.trim())           // 清理每行的首尾空白
+      .filter(line => line.length > 0)    // 移除空行
+      .join('\n');                        // 重新组合
+    
+    console.log('✨ [文字清理] 清理后文本:', text.substring(0, 200));
+    
+    result.text = text;
+
+    // 提取HTML
+    result.html = dt.getData('text/html') || '';
+    if (result.html) {
+      console.log(`📄 [提取] HTML: ${result.html.substring(0, 100)}...`);
+    }
+
+    // 提取URL
+    const urlData = dt.getData('text/uri-list') || '';
+    if (urlData) {
+      result.urls = urlData.split('\n').filter(url => url.trim().length > 0);
+      console.log(`🔗 [提取] URL: ${result.urls.length}个`);
+    }
+
+    // 判断是否有内容
+    result.hasContent = result.files.length > 0 || 
+                        result.text.length > 0 || 
+                        result.urls.length > 0;
+
+    return result;
+  }
+
+  /**
+   * 检测是否为企业微信环境
+   */
+  private isWeChatWorkEnv(): boolean {
+    const ua = window.navigator.userAgent.toLowerCase();
+    return ua.includes('wxwork') || ua.includes('qywechat');
+  }
+
+  /**
+   * 检查是否为图片URL(宽松模式)
+   * 对于无扩展名的URL(如微信/企业微信的图片URL),默认尝试当作图片处理
+   */
+  private isImageUrl(url: string): boolean {
+    const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif'];
+    const lowerUrl = url.toLowerCase();
+    
+    // 1. 检查是否包含图片扩展名
+    const hasImageExt = imageExtensions.some(ext => lowerUrl.includes(ext));
+    
+    // 2. 检查URL中是否包含图片相关关键词
+    const hasImageKeyword = lowerUrl.includes('image') || 
+                            lowerUrl.includes('photo') || 
+                            lowerUrl.includes('img') ||
+                            lowerUrl.includes('pic');
+    
+    // 3. 检查是否为常见的图片CDN域名
+    const isImageCDN = lowerUrl.includes('qpic.cn') ||  // 腾讯图片CDN
+                       lowerUrl.includes('file-cloud.fmode.cn') ||  // 项目CDN
+                       (lowerUrl.includes('cdn') && /\.(jpg|jpeg|png|gif|webp)/i.test(lowerUrl));
+    
+    // 4. 对于微信/企业微信的无扩展名图片(纯哈希值),也尝试当作图片
+    // 如果URL不包含明显的非图片扩展名,就当作图片尝试
+    const hasNonImageExt = ['.txt', '.doc', '.docx', '.xls', '.xlsx', '.pdf', '.zip', '.rar'].some(ext => lowerUrl.endsWith(ext));
+    const isHashUrl = /[a-f0-9]{32}/i.test(url); // 检测32位哈希值(微信图片常见格式)
+    
+    // 满足任一条件即认为是图片URL
+    return hasImageExt || hasImageKeyword || isImageCDN || (!hasNonImageExt && isHashUrl);
+  }
+
+  /**
+   * 从URL提取文件名(增强版)
+   * 对于无扩展名的URL(如微信图片),自动添加.jpg扩展名
+   */
+  private extractFileNameFromUrl(url: string): string {
+    try {
+      const urlObj = new URL(url);
+      const pathname = urlObj.pathname;
+      const parts = pathname.split('/');
+      let fileName = parts[parts.length - 1] || '';
+      
+      // 移除查询参数
+      fileName = fileName.split('?')[0];
       
-      let url = '';
-      if (isImage) {
-        // 读取为DataURL用于预览
-        url = await this.readFileAsDataURL(file);
+      // 如果文件名为空或只包含数字和字母(哈希值),生成新名称
+      if (!fileName || /^[a-f0-9]{32}$/i.test(fileName)) {
+        const timestamp = Date.now();
+        const random = Math.random().toString(36).substring(2, 8);
+        fileName = `image_${timestamp}_${random}.jpg`;
+        console.log(`📝 [生成文件名] ${fileName}`);
+        return fileName;
       }
       
-      this.aiDesignUploadedFiles.push({
-        file: file,
-        name: file.name,
-        size: file.size,
-        extension: extension,
-        url: url
-      });
+      // 检查是否有扩展名
+      const hasExtension = /\.[a-z0-9]{2,4}$/i.test(fileName);
       
-      if (isImage) {
-        this.aiDesignUploadedImages.push(url);
+      // 如果没有扩展名,根据URL判断并添加
+      if (!hasExtension) {
+        const lowerUrl = url.toLowerCase();
+        
+        // 检查是否为CAD文件URL
+        if (lowerUrl.includes('dwg') || lowerUrl.includes('cad')) {
+          fileName += '.dwg';
+        } else if (lowerUrl.includes('dxf')) {
+          fileName += '.dxf';
+        } else if (lowerUrl.includes('pdf')) {
+          fileName += '.pdf';
+        } else {
+          // 默认为图片
+          fileName += '.jpg';
+        }
+        
+        console.log(`📝 [添加扩展名] ${fileName}`);
       }
       
-      console.log(`✅ [AI对话拖拽] 文件已添加: ${file.name}`);
+      return fileName;
+    } catch (error) {
+      console.warn('⚠️ [文件名提取失败] 使用默认名称', error);
+      const timestamp = Date.now();
+      return `image_${timestamp}.jpg`;
     }
   }
 
+  /**
+   * 处理AI文件(图片和文档)
+   * @deprecated 已废弃,请使用 handleAIFileUpload 方法
+   * 保留此方法仅为兼容性,内部调用 handleAIFileUpload
+   */
+  private async processAIFiles(files: FileList | File[]): Promise<void> {
+    console.warn('⚠️ processAIFiles已废弃,自动转发到handleAIFileUpload');
+    const fileArray = Array.from(files);
+    await this.handleAIFileUpload(fileArray);
+  }
+
   /**
    * 读取文件为DataURL
    */
@@ -2155,6 +2250,12 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
    * 将文字内容添加到AI输入框
    */
   private appendTextToAIInput(text: string): void {
+    // 只添加有实际内容的文字
+    if (!text || text.trim().length === 0) {
+      console.log('⚠️ [AI对话拖拽] 文字为空,跳过添加');
+      return;
+    }
+    
     // 如果有AI对话输入框,添加文字
     if (this.aiChatInput) {
       this.aiChatInput = this.aiChatInput 
@@ -2165,10 +2266,149 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
     }
     
     console.log(`✅ [AI对话拖拽] 文字已添加到输入框: ${text.substring(0, 50)}...`);
+    this.cdr.markForCheck();
   }
   
   // ==================== End: AI对话拖拽支持 ====================
   
+  /**
+   * 预览图片
+   */
+  previewImage(url: string, index?: number): void {
+    this.imagePreviewUrl = url;
+    this.imagePreviewVisible = true;
+    
+    // 构建预览列表(所有已上传的图片)
+    this.imagePreviewList = this.aiDesignUploadedFiles
+      .filter(f => f.isImage && f.url)
+      .map(f => f.url);
+    
+    // 设置当前索引
+    if (index !== undefined) {
+      this.imagePreviewIndex = index;
+    } else {
+      this.imagePreviewIndex = this.imagePreviewList.indexOf(url);
+    }
+    
+    this.cdr.markForCheck();
+    console.log('🖼️ 打开图片预览:', url.substring(0, 50));
+  }
+  
+  /**
+   * 关闭图片预览
+   */
+  closeImagePreview(): void {
+    this.imagePreviewVisible = false;
+    this.imagePreviewUrl = '';
+    this.imagePreviewList = [];
+    this.cdr.markForCheck();
+  }
+  
+  /**
+   * 上一张图片
+   */
+  previousImage(event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    
+    if (this.imagePreviewList.length === 0) return;
+    
+    this.imagePreviewIndex = (this.imagePreviewIndex - 1 + this.imagePreviewList.length) % this.imagePreviewList.length;
+    this.imagePreviewUrl = this.imagePreviewList[this.imagePreviewIndex];
+    this.cdr.markForCheck();
+  }
+  
+  /**
+   * 下一张图片
+   */
+  nextImage(event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    
+    if (this.imagePreviewList.length === 0) return;
+    
+    this.imagePreviewIndex = (this.imagePreviewIndex + 1) % this.imagePreviewList.length;
+    this.imagePreviewUrl = this.imagePreviewList[this.imagePreviewIndex];
+    this.cdr.markForCheck();
+  }
+  
+  /**
+   * 预览文件(PDF、CAD等)
+   */
+  previewFile(file: any): void {
+    if (!file || !file.url) {
+      console.warn('⚠️ 文件URL不存在');
+      return;
+    }
+
+    console.log('📄 预览文件:', file.name, file.category);
+
+    // 对于base64文件,需要转换为Blob URL或直接在新窗口打开
+    if (file.isBase64 && file.url.startsWith('data:')) {
+      // 将base64转换为Blob URL
+      const base64Data = file.url.split(',')[1];
+      const byteCharacters = atob(base64Data);
+      const byteNumbers = new Array(byteCharacters.length);
+      for (let i = 0; i < byteCharacters.length; i++) {
+        byteNumbers[i] = byteCharacters.charCodeAt(i);
+      }
+      const byteArray = new Uint8Array(byteNumbers);
+      const blob = new Blob([byteArray], { type: file.type });
+      const blobUrl = URL.createObjectURL(blob);
+      
+      // 在新窗口打开
+      window.open(blobUrl, '_blank');
+      
+      // 5秒后释放URL
+      setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
+    } else {
+      // 普通URL直接在新窗口打开
+      window.open(file.url, '_blank');
+    }
+  }
+  
+  /**
+   * 删除AI对话区域的图片
+   */
+  removeAIDialogImage(index: number): void {
+    if (index < 0 || index >= this.aiDesignUploadedFiles.length) return;
+    
+    const removedFile = this.aiDesignUploadedFiles[index];
+    this.aiDesignUploadedFiles.splice(index, 1);
+    
+    // 如果是图片,同时从图片数组中移除
+    if (removedFile.isImage && removedFile.url) {
+      const imgIndex = this.aiDesignUploadedImages.indexOf(removedFile.url);
+      if (imgIndex !== -1) {
+        this.aiDesignUploadedImages.splice(imgIndex, 1);
+      }
+    }
+    
+    console.log('🗑️ 已移除文件:', removedFile.name);
+    this.cdr.markForCheck();
+  }
+  
+  /**
+   * 触发文件选择器
+   */
+  triggerAIDialogFileInput(): void {
+    const input = document.createElement('input');
+    input.type = 'file';
+    input.multiple = true;
+    input.accept = 'image/*,.pdf,.dwg,.dxf,.dwf';
+    
+    input.onchange = async (e: any) => {
+      const files = Array.from(e.target.files || []) as File[];
+      if (files.length > 0) {
+        await this.handleAIFileUpload(files);
+      }
+    };
+    
+    input.click();
+  }
+  
   /**
    * 删除参考图片 - 同时删除服务器文件
    */
@@ -2269,7 +2509,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
         const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
           file,
           targetProjectId,
-          'cad_drawing',
+          'cad_file',
           targetProductId,
           'requirements', // stage参数
           {
@@ -2740,6 +2980,9 @@ CAD文件分析:${JSON.stringify(this.aiAnalysisResults.cadAnalysis, null, 2)}
       });
 
       this.cdr.markForCheck();
+      
+      // 保存对话历史到数据库
+      await this.saveChatHistory();
 
     } catch (error) {
       console.error('AI聊天失败:', error);
@@ -2829,7 +3072,7 @@ ${context}
       const cadFiles = await this.projectFileService.getProjectFiles(
         targetProjectId,
         {
-          fileType: 'cad_drawing',
+          fileType: 'cad_file',
           stage: 'requirements'
         }
       );
@@ -3112,6 +3355,7 @@ ${context}
         globalRequirements: this.globalRequirements,
         spaceRequirements: this.spaceRequirements,
         crossSpaceRequirements: this.crossSpaceRequirements,
+        spaceSpecialRequirements: this.spaceSpecialRequirements, // 🔥 保存特殊需求
         referenceImages: this.referenceImages.map(img => ({
           id: img.id,
           url: img.url,
@@ -3130,6 +3374,22 @@ ${context}
         aiAnalysisResults: this.aiAnalysisResults,
         confirmedAt: new Date().toISOString()
       };
+      
+      // 🔥 保存AI对话历史(如果有选择的空间)
+      if (this.aiDesignCurrentSpace?.id && this.aiChatMessages.length > 0) {
+        if (!data.aiChatHistory) {
+          data.aiChatHistory = {};
+        }
+        data.aiChatHistory[this.aiDesignCurrentSpace.id] = {
+          messages: this.aiChatMessages.map(m => ({
+            role: m.role,
+            content: m.content,
+            timestamp: m.timestamp.toISOString(),
+            images: m.images
+          })),
+          lastUpdated: new Date().toISOString()
+        };
+      }
 
       // 重新生成阶段截止时间 (基于当前确认时间与项目交付日期)
       this.rebuildPhaseDeadlines(data, confirmedAt);
@@ -3827,16 +4087,6 @@ ${context}
     }
   }
 
-  /**
-   * 触发AI对话框文件选择
-   */
-  triggerAIDialogFileInput(): void {
-    const element = document.getElementById('aiDesignFileInput') as HTMLInputElement;
-    if (element) {
-      element.click();
-    }
-  }
-
   /**
    * 处理AI对话框文件选择
    */
@@ -3853,16 +4103,6 @@ ${context}
     input.value = '';
   }
 
-  /**
-   * 移除AI对话框中的图片
-   */
-  removeAIDialogImage(index: number): void {
-    this.aiDesignUploadedImages.splice(index, 1);
-    this.aiDesignUploadedFiles.splice(index, 1);
-    this.cdr.markForCheck();
-  }
-
-
   /**
    * 处理AI文件上传(统一处理点击和拖拽)
    * 🔥 扩展支持:图片、PDF、CAD文件

+ 9 - 9
src/modules/project/services/project-file.service.ts

@@ -40,19 +40,19 @@ export class ProjectFileService {
       const storage = await NovaStorage.withCid(cid);
 
       // 构建prefixKey
-      let prefixKey = `project/${projectId}`;
-      if (spaceId) {
-        prefixKey += `/space/${spaceId}`;
-      }
-      if (stage) {
-        prefixKey += `/stage/${stage}`;
-      }
+      let prefixKey = `project/${file.name}`;
+      // if (spaceId) {
+      //   prefixKey += `/space/${spaceId}`;
+      // }
+      // if (stage) {
+      //   prefixKey += `/stage/${stage}`;
+      // }
 
       // 🔥 清理文件名,避免存储服务错误
-      const cleanedFile = this.createCleanedFile(file);
+      // const cleanedFile = this.createCleanedFile(file);
       
       // 上传文件
-      const uploadedFile: NovaFile = await storage.upload(cleanedFile, {
+      const uploadedFile: NovaFile = await storage.upload(file, {
         prefixKey,
         onProgress: (progress: { total: { percent: number } }) => {
           if (onProgress) {

+ 204 - 0
src/styles.scss

@@ -192,3 +192,207 @@ app-upload-success-modal {
     }
   }
 }
+
+/* ============ Material DatePicker 响应式样式 ============ */
+/* 修复所有屏幕下日期选择器被截断的问题 */
+
+/* 通用修复 - 适用于所有屏幕尺寸 */
+.cdk-overlay-container {
+  .mat-datepicker-content {
+    /* 优化日历弹窗样式 */
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important;
+    border-radius: 12px !important;
+    overflow: hidden !important;
+  }
+}
+
+/* 平板和小屏幕优化 */
+@media (max-width: 1024px) {
+  .cdk-overlay-container {
+    .cdk-overlay-pane {
+      /* 确保日期选择器在小屏幕下不会超出视口 */
+      max-width: calc(100vw - 32px) !important;
+      max-height: none !important; /* 移除高度限制,让内容自然展开 */
+      /* 强制居中定位,避免被截断 */
+      left: 50% !important;
+      transform: translateX(-50%) !important;
+      right: auto !important;
+      margin: 16px auto !important;
+      /* 确保弹窗在屏幕内 */
+      position: fixed !important;
+      top: 50% !important;
+      transform: translate(-50%, -50%) !important; /* 垂直水平居中 */
+    }
+
+    .mat-datepicker-content {
+      /* 调整日期选择器内容容器 */
+      max-width: 100% !important;
+      width: auto !important;
+      max-height: calc(100vh - 80px) !important; /* 限制在视口内 */
+      overflow-y: auto !important; /* 允许垂直滚动查看所有日期 */
+      overflow-x: hidden !important; /* 禁止横向滚动 */
+      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2) !important;
+      border-radius: 12px !important;
+      
+      .mat-calendar {
+        /* 确保日历组件适应容器宽度 */
+        width: 100% !important;
+        min-width: 280px !important;
+        max-width: min(320px, calc(100vw - 48px)) !important; /* 适应更小屏幕 */
+        height: auto !important; /* 自动高度 */
+        
+        /* 调整日历头部样式 */
+        .mat-calendar-header {
+          padding: 8px 8px 0 8px !important;
+          position: sticky !important; /* 滚动时头部固定 */
+          top: 0 !important;
+          background: white !important;
+          z-index: 1 !important;
+        }
+        
+        /* 调整日历内容区域 */
+        .mat-calendar-content {
+          padding: 0 8px 8px 8px !important;
+          height: auto !important; /* 自动高度 */
+          overflow: visible !important; /* 确保所有日期可见 */
+        }
+        
+        /* 确保星期标题可见 */
+        .mat-calendar-table-header {
+          position: sticky !important;
+          top: 48px !important; /* 头部下方 */
+          background: white !important;
+          z-index: 1 !important;
+        }
+        
+        /* 调整日期单元格大小 - 确保7列完整显示 */
+        .mat-calendar-body-cell {
+          width: calc((100% - 16px) / 7) !important; /* 平均分配宽度 */
+          min-width: 32px !important;
+          max-width: 40px !important;
+          height: 36px !important;
+          line-height: 36px !important;
+          padding: 0 !important;
+        }
+        
+        /* 调整月份/年份选择视图 */
+        .mat-calendar-body {
+          min-width: 280px !important;
+          width: 100% !important;
+          height: auto !important; /* 自动高度 */
+        }
+        
+        /* 确保日期表格完整显示 */
+        .mat-calendar-table {
+          width: 100% !important;
+          height: auto !important;
+          table-layout: fixed !important; /* 固定表格布局,确保列宽均匀 */
+        }
+        
+        /* 确保行完整显示 */
+        .mat-calendar-body-row {
+          display: flex !important;
+          width: 100% !important;
+        }
+      }
+    }
+    
+    /* 调整触摸目标大小 */
+    .mat-calendar-body-cell-content {
+      width: 32px !important;
+      height: 32px !important;
+      line-height: 32px !important;
+    }
+  }
+}
+
+/* 更小屏幕的额外调整 */
+@media (max-width: 480px) {
+  .cdk-overlay-container {
+    .cdk-overlay-pane {
+      /* 在更小的屏幕上进一步减小边距 */
+      max-width: calc(100vw - 16px) !important;
+      max-height: none !important;
+      /* 保持居中定位 */
+      left: 50% !important;
+      right: auto !important;
+      margin: 8px auto !important;
+      position: fixed !important;
+      top: 50% !important;
+      transform: translate(-50%, -50%) !important; /* 垂直水平居中 */
+    }
+    
+    .mat-datepicker-content {
+      max-height: calc(100vh - 60px) !important;
+      overflow-y: auto !important; /* 允许垂直滚动 */
+      overflow-x: hidden !important; /* 禁止横向滚动 */
+      
+      .mat-calendar {
+        min-width: 260px !important;
+        max-width: min(300px, calc(100vw - 32px)) !important;
+        width: 100% !important;
+        height: auto !important;
+        
+        .mat-calendar-header {
+          position: sticky !important;
+          top: 0 !important;
+          background: white !important;
+          z-index: 1 !important;
+        }
+        
+        .mat-calendar-table-header {
+          position: sticky !important;
+          top: 44px !important;
+          background: white !important;
+          z-index: 1 !important;
+        }
+        
+        .mat-calendar-content {
+          height: auto !important;
+          overflow: visible !important;
+        }
+        
+        .mat-calendar-body {
+          width: 100% !important;
+          height: auto !important;
+        }
+        
+        .mat-calendar-table {
+          width: 100% !important;
+          height: auto !important;
+          table-layout: fixed !important;
+        }
+        
+        .mat-calendar-body-row {
+          display: flex !important;
+          width: 100% !important;
+        }
+        
+        /* 进一步缩小日期单元格但确保7列显示 */
+        .mat-calendar-body-cell {
+          width: calc((100% - 12px) / 7) !important; /* 平均分配 */
+          min-width: 28px !important;
+          max-width: 36px !important;
+          height: 32px !important;
+          line-height: 32px !important;
+          padding: 0 !important;
+        }
+        
+        .mat-calendar-body-cell-content {
+          width: 26px !important;
+          height: 26px !important;
+          line-height: 26px !important;
+          font-size: 12px !important;
+        }
+        
+        /* 调整控制按钮大小 */
+        .mat-calendar-arrow,
+        .mat-calendar-previous-button,
+        .mat-calendar-next-button {
+          width: 32px !important;
+          height: 32px !important;
+        }
+      }
+    }
+  }
+}