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 5 hours ago
parent
commit
fc16b200b3
21 changed files with 2722 additions and 336 deletions
  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;
+        }
+      }
+    }
+  }
+}