Browse Source

feat: improve modal footer visibility and z-index management

- Removed max-height constraints on modal-body to allow flex layout to work naturally
- Enhanced footer visibility with flex-shrink: 0, increased padding (80px bottom), and visual indicators
- Fixed z-index hierarchy: main modal (1050), calendar modal (1150), space assignment modal (1200)
- Improved scrollbar styling with primary color theme and better visual feedback
- Added responsive optimizations for mobile devices with full-width buttons an
徐福静0235668 8 giờ trước cách đây
mục cha
commit
29da5db50a

+ 186 - 0
MODAL_FIX_VERIFICATION.md

@@ -0,0 +1,186 @@
+# 模态框底部按钮显示修复验证指南
+
+## 🔍 问题描述
+- **问题1**: 小屏幕下底部"取消"和"确认添加"按钮完全不可见
+- **问题2**: 网页端(桌面端)底部按钮被部分遮挡
+
+## ✅ 修复内容
+
+### 核心修复
+1. **移除了错误的 `max-height` 限制**
+   - 之前给 `.modal-body` 添加了 `max-height: calc(95vh - 140px)`
+   - 这导致 body 高度被限制,footer 被挤出可视区域
+   - 现在移除了所有 `max-height` 限制,让 flex 布局自然工作
+
+2. **确保 footer 永不被压缩**
+   ```scss
+   .modal-footer {
+     flex-shrink: 0 !important; // 🔥 关键!
+     background: white !important;
+     z-index: 10;
+     position: relative;
+   }
+   ```
+
+3. **优化 body 的 flex 属性**
+   ```scss
+   .modal-body {
+     flex: 1 1 auto !important; // 允许增长和缩小
+     overflow-y: auto !important; // 内容过长时滚动
+     min-height: 0; // 允许正确缩小
+     // ❌ 移除了 max-height 限制
+   }
+   ```
+
+## 🧪 测试步骤
+
+### 测试1: 桌面端 (1920px × 1080px)
+1. 打开浏览器,设置窗口大小为桌面尺寸
+2. 点击"添加的设计产品"按钮
+3. **验证点**:
+   - [ ] 模态框居中显示
+   - [ ] 价格预览区域(黄色背景)完整可见
+   - [ ] 底部"取消"和"确认添加"按钮**完整可见**
+   - [ ] 按钮不被遮挡,可以点击
+4. 填写所有表单项(让内容变长)
+5. **验证点**:
+   - [ ] `.modal-body` 出现滚动条
+   - [ ] 可以滚动查看所有内容
+   - [ ] 滚动时,footer 始终固定在底部可见
+   - [ ] 不需要滚动就能看到按钮
+
+### 测试2: 平板端 (iPad 1024px × 768px)
+1. 调整浏览器窗口到 1024px 宽
+2. 打开"添加设计产品"模态框
+3. **验证点**:
+   - [ ] 模态框宽度为 90vw
+   - [ ] 底部按钮完整可见
+   - [ ] 内容可以正常滚动
+   - [ ] Footer 不会被压缩
+
+### 测试3: 手机端 (375px × 812px)
+1. 使用浏览器开发者工具,切换到移动设备模拟
+2. 选择 iPhone X/11/12 等设备
+3. 打开"添加设计产品"模态框
+4. **验证点**:
+   - [ ] 模态框几乎全屏显示
+   - [ ] 价格预览区域可见
+   - [ ] 底部"取消"和"确认添加"按钮**完整可见**
+   - [ ] 按钮足够大,易于点击 (min-height: 44px)
+   - [ ] 不需要滚动就能看到按钮
+5. 填写表单,让内容变长
+6. **验证点**:
+   - [ ] 内容可以向上滚动
+   - [ ] Footer 始终固定在底部
+   - [ ] 按钮始终可见,不会消失
+
+### 测试4: 超小手机 (320px × 568px)
+1. 设置设备为 iPhone SE 或更小尺寸
+2. 打开模态框
+3. **验证点**:
+   - [ ] 模态框高度 99vh
+   - [ ] 按钮完整可见 (min-height: 42px)
+   - [ ] 所有内容可以滚动访问
+   - [ ] Footer 不被压缩
+
+### 测试5: 编辑模式
+1. 点击已有产品的"编辑"按钮(✏️ emoji)
+2. **验证点**:
+   - [ ] 编辑模态框也能正确显示
+   - [ ] 底部"取消"和"确认修改"按钮可见
+   - [ ] 所有功能正常
+
+## 📊 关键CSS属性说明
+
+### Flex 布局结构
+```
+.modal-container (max-height: 95vh, display: flex, flex-direction: column)
+├─ .modal-header (flex-shrink: 0) ← 固定高度,不压缩
+├─ .modal-body (flex: 1 1 auto, overflow-y: auto) ← 自动填充,内部滚动
+└─ .modal-footer (flex-shrink: 0 !important) ← 固定高度,永不压缩
+```
+
+### 为什么移除 max-height?
+```scss
+// ❌ 错误的做法
+.modal-body {
+  max-height: calc(95vh - 140px); // 这会限制 body 高度
+  // 结果: body 太小,footer 被挤出容器外
+}
+
+// ✅ 正确的做法  
+.modal-body {
+  flex: 1 1 auto; // 自动占据剩余空间
+  min-height: 0; // 允许缩小到适合的大小
+  // 结果: body 自动适应,footer 始终可见
+}
+```
+
+### flex-shrink 的作用
+- `flex-shrink: 0` = 不允许缩小,保持原始高度
+- `flex-shrink: 1` (默认) = 允许缩小,在空间不足时会被压缩
+
+## 🎯 预期效果
+
+### 桌面端
+- 模态框宽度: 600px (max-width)
+- 模态框高度: 最大 95vh
+- Footer 高度: 约 70px (padding 16px + 按钮 38px + 边框)
+- Footer 状态: **始终可见,不需要滚动**
+
+### 移动端
+- 模态框宽度: 100vw
+- 模态框高度: 98vh
+- Footer 高度: 约 64px
+- 按钮高度: 44px (易于点击)
+- Footer 状态: **始终固定在底部**
+
+## ⚠️ 如果还有问题
+
+### 问题: 桌面端按钮仍然不可见
+**排查步骤**:
+1. 打开浏览器开发者工具
+2. 检查 `.modal-footer` 的样式:
+   ```
+   flex-shrink: 0 !important;  // 必须存在
+   background: white !important; // 必须存在
+   ```
+3. 检查是否有其他CSS覆盖了这些属性
+4. 清除浏览器缓存,强制刷新 (Ctrl+Shift+R)
+
+### 问题: 移动端按钮不可见
+**排查步骤**:
+1. 检查响应式断点是否生效
+2. 确认 `@media (max-width: 768px)` 中的 footer 样式
+3. 确认没有其他地方设置了 `max-height`
+4. 检查按钮的 `min-height` 是否生效
+
+### 问题: Footer 有背景但看不到按钮
+**可能原因**:
+- 按钮颜色和背景色相同
+- 按钮被 `display: none` 隐藏
+- 按钮有 `opacity: 0`
+- 检查HTML是否正确渲染了按钮元素
+
+## 📝 修改历史
+
+### 版本 2 (当前版本) - 2025-12-04
+- ✅ 移除所有 `.modal-body` 的 `max-height` 限制
+- ✅ 保留 `.modal-footer` 的 `flex-shrink: 0 !important`
+- ✅ 让 flex 布局自然工作
+- ✅ 在所有断点移除 body 的高度限制
+
+### 版本 1 (有问题的版本)
+- ❌ 添加了 `max-height: calc(95vh - 140px)` 给 body
+- ❌ 导致 body 高度不足,footer 被挤出
+
+## ✨ 总结
+
+**核心原则**: 
+- ✅ 让 Flex 布局自然工作
+- ✅ 不要给 body 设置 `max-height`
+- ✅ 确保 footer 的 `flex-shrink: 0 !important`
+- ✅ 容器的 `max-height` 就够了
+
+**关键点**: 
+`flex-shrink: 0 !important` 是确保 footer 永远可见的**唯一关键**!

+ 331 - 20
src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.scss

@@ -8,7 +8,7 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 1000;
+  z-index: 1050; // 🔥 降低到1050,让子弹窗可以显示在上层
   opacity: 0;
   visibility: hidden;
   transition: all 0.3s ease;
@@ -47,9 +47,10 @@
 .modal-container {
   background: white;
   border-radius: 12px;
-  width: 90vw;
+  width: 90vw; // 🔥 桌面端默认90vw,确保足够宽
+  min-width: 900px; // 🔥 桌面端最小宽度900px
   max-width: 1200px;
-  max-height: 90vh;
+  max-height: 95vh; // 🔥 从92vh增加到95vh,提供更多空间
   display: flex;
   flex-direction: column;
   box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
@@ -94,8 +95,39 @@
 .modal-body {
   flex: 1;
   overflow-y: auto;
-  padding: 24px 32px;
+  overflow-x: hidden; // 🔥 防止横向滚动
+  padding: 24px 32px 80px 32px; // 🔥 底部内边距从48px大幅增加到80px
   background: #fafafa; // 浅灰色背景,增强对比度
+  min-height: 0; // 🔥 关键:允许flex子元素正确计算高度
+  position: relative; // 🔥 为滚动指示器定位
+  
+  // 🔥 优化滚动体验
+  scroll-behavior: smooth;
+  
+  // 🔥 美化滚动条(始终可见,提示用户可滚动)
+  &::-webkit-scrollbar {
+    width: 10px; // 🔥 稍微加宽,更显眼
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #e8e8e8; // 🔥 稍微深一点,更容易被注意到
+    border-radius: 5px;
+    margin: 4px 0; // 🔥 顶部和底部留出间距
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #1890ff; // 🔥 使用主色调,更显眼
+    border-radius: 5px;
+    border: 2px solid #e8e8e8; // 🔥 添加边框,视觉上更突出
+    
+    &:hover {
+      background: #40a9ff; // 🔥 悬停时更亮
+    }
+    
+    &:active {
+      background: #096dd9; // 🔥 点击时更深
+    }
+  }
 }
 
 .team-selection-section {
@@ -705,11 +737,17 @@
 
 // 移除原来的 modal-footer 样式,因为已经移到内容区域内
 .assignment-summary-section {
-  margin-top: 24px;
+  margin-top: 32px; // 🔥 增加顶部间距
+  margin-bottom: 60px; // 🔥 底部间距从40px再增加到60px,确保按钮可见
   padding: 24px;
-  background: #f8f9fa;
+  background: #ffffff; // 🔥 改为白色背景,更显眼
   border-radius: 12px;
-  border: 1px solid #e9ecef;
+  border: 2px solid #1890ff; // 🔥 加粗边框,使用主色调
+  box-shadow: 0 4px 12px rgba(24, 144, 255, 0.1); // 🔥 添加阴影
+  
+  // 🔥 确保在可见区域内
+  position: relative;
+  z-index: 1;
 
   .summary-header {
     margin-bottom: 16px;
@@ -757,38 +795,62 @@
     display: flex;
     gap: 12px;
     justify-content: flex-end;
+    margin-top: 20px; // 🔥 增加与摘要内容的间距
+    padding: 20px; // 🔥 四周内边距
+    margin: 20px -24px -24px -24px; // 🔥 负边距让其延伸到容器边缘
+    background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.98) 100%); // 🔥 渐变背景
+    border-top: 2px solid #e8e8e8; // 🔥 加粗分隔线
+    border-radius: 0 0 12px 12px; // 🔥 底部圆角
+    box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05); // 🔥 向上的阴影,增强可见性
 
     .btn-secondary, .btn-primary {
-      padding: 10px 20px;
-      border-radius: 6px;
+      padding: 12px 24px; // 🔥 增大按钮
+      border-radius: 8px; // 🔥 增加圆角
       font-size: 14px;
-      font-weight: 500;
+      font-weight: 600; // 🔥 加粗字体
       cursor: pointer;
       transition: all 0.2s ease;
       border: none;
+      min-width: 100px; // 🔥 最小宽度
+      flex-shrink: 0; // 🔥 防止缩小
     }
 
     .btn-secondary {
       background: #f5f5f5;
       color: #595959;
+      border: 1px solid #d9d9d9; // 🔥 添加边框
 
       &:hover {
         background: #e6e6e6;
+        border-color: #bfbfbf;
       }
     }
 
     .btn-primary {
       background: #1890ff;
       color: white;
+      box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); // 🔥 添加阴影
 
       &:hover:not(:disabled) {
         background: #40a9ff;
+        box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
+        transform: translateY(-1px); // 🔥 轻微上移效果
       }
 
       &:disabled {
         background: #d9d9d9;
         color: #bfbfbf;
         cursor: not-allowed;
+        box-shadow: none;
+      }
+    }
+    
+    // 🔥 响应式优化:小屏幕下按钮占满宽度
+    @media (max-width: 480px) {
+      flex-direction: column;
+      
+      .btn-secondary, .btn-primary {
+        width: 100%;
       }
     }
   }
@@ -805,7 +867,7 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 1100;
+  z-index: 1150; // 🔥 设置为1150,在主弹窗(1050)之上
 }
 
 .calendar-modal-container {
@@ -926,7 +988,7 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 2000;
+  z-index: 1200; // 🔥 设置为1200,在主弹窗(1050)和日历弹窗(1150)之上
   animation: fadeIn 0.2s ease;
 }
 
@@ -935,7 +997,7 @@
   border-radius: 12px;
   width: 90%;
   max-width: 600px;
-  max-height: 80vh;
+  max-height: 85vh; // 🔥 从80vh增加到85vh,提供更多空间
   display: flex;
   flex-direction: column;
   animation: slideUp 0.3s ease;
@@ -1008,7 +1070,28 @@
 }
 
 .space-selection-section {
-  padding: 24px;
+  padding: 24px 24px 32px 24px; // 🔥 增加底部padding从24px到32px
+  overflow-y: auto; // 🔥 添加滚动
+  flex: 1; // 🔥 允许弹性增长
+
+  // 🔥 滚动条样式
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 4px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #cbd5e1;
+    border-radius: 4px;
+
+    &:hover {
+      background: #94a3b8;
+    }
+  }
 
   .form-label {
     font-size: 16px;
@@ -1036,6 +1119,7 @@
     max-height: 400px;
     overflow-y: auto;
     padding-right: 8px;
+    padding-bottom: 16px; // 🔥 列表底部额外空间
 
     &::-webkit-scrollbar {
       width: 6px;
@@ -1139,19 +1223,23 @@
   display: flex;
   justify-content: flex-end;
   gap: 12px;
-  padding: 16px 24px;
-  border-top: 1px solid #e2e8f0;
-  background: #f8fafc;
+  padding: 20px 24px; // 🔥 增加padding从16px到20px
+  border-top: 2px solid #e2e8f0; // 🔥 加粗边框
+  background: linear-gradient(to top, #f8fafc 0%, #ffffff 100%); // 🔥 渐变背景
+  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05); // 🔥 向上阴影增强可见性
+  flex-shrink: 0; // 🔥 防止缩小
 
   .btn-secondary,
   .btn-primary {
-    padding: 10px 24px;
-    border-radius: 6px;
+    padding: 12px 28px; // 🔥 增加padding从10px 24px到12px 28px
+    border-radius: 8px; // 🔥 增加圆角
     border: none;
     font-size: 14px;
-    font-weight: 500;
+    font-weight: 600; // 🔥 加粗字体从500到600
     cursor: pointer;
     transition: all 0.2s ease;
+    min-width: 100px; // 🔥 最小宽度
+    flex-shrink: 0; // 🔥 防止缩小
   }
 
   .btn-secondary {
@@ -1162,15 +1250,27 @@
     &:hover {
       background: #f1f5f9;
       border-color: #cbd5e1;
+      transform: translateY(-1px); // 🔥 hover时轻微上移
+    }
+
+    &:active {
+      transform: translateY(0); // 🔥 点击时回到原位
     }
   }
 
   .btn-primary {
     background: #4f46e5;
     color: white;
+    box-shadow: 0 2px 8px rgba(79, 70, 229, 0.2); // 🔥 添加阴影
 
     &:hover {
       background: #4338ca;
+      box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); // 🔥 hover时增强阴影
+      transform: translateY(-1px); // 🔥 hover时轻微上移
+    }
+
+    &:active {
+      transform: translateY(0); // 🔥 点击时回到原位
     }
   }
 }
@@ -1215,6 +1315,84 @@
   }
 }
 
+// 🔥 响应式优化:小屏幕下空间分配弹窗
+@media (max-width: 768px) {
+  .space-assignment-container {
+    width: 95%;
+    max-height: 90vh; // 🔥 增加高度
+  }
+  
+  .space-assignment-header {
+    padding: 16px 20px;
+    
+    .designer-name {
+      font-size: 16px;
+    }
+  }
+  
+  .space-selection-section {
+    padding: 20px 20px 40px 20px; // 🔥 增加底部空间
+  }
+  
+  .space-assignment-footer {
+    padding: 18px 20px;
+    
+    .btn-secondary,
+    .btn-primary {
+      padding: 12px 24px;
+      font-size: 14px;
+      min-width: 90px;
+    }
+  }
+}
+
+@media (max-width: 480px) {
+  .space-assignment-container {
+    width: 98%;
+    max-height: 92vh; // 🔥 进一步增加高度
+    border-radius: 8px;
+  }
+  
+  .space-assignment-header {
+    padding: 14px 16px;
+    
+    .designer-avatar {
+      width: 40px;
+      height: 40px;
+    }
+    
+    .designer-name {
+      font-size: 15px;
+    }
+  }
+  
+  .space-selection-section {
+    padding: 16px 16px 36px 16px; // 🔥 增加底部空间
+    
+    .form-label {
+      font-size: 15px;
+    }
+    
+    .space-checkbox-list {
+      max-height: 300px;
+      padding-bottom: 20px; // 🔥 增加列表底部空间
+    }
+  }
+  
+  .space-assignment-footer {
+    padding: 16px;
+    gap: 10px;
+    
+    .btn-secondary,
+    .btn-primary {
+      padding: 12px 20px;
+      font-size: 14px;
+      min-width: 85px;
+      flex: 1; // 🔥 在小屏幕下平分空间
+    }
+  }
+}
+
 // 空间列表为空
 .space-empty {
   display: flex;
@@ -1279,4 +1457,137 @@
     transform: scale(1.2);
     opacity: 0.8;
   }
+}
+
+// 🔥 响应式优化:大屏幕桌面端
+@media (min-width: 1441px) {
+  .modal-container {
+    width: 85vw; // 🔥 超大屏幕稍微窄一点
+    min-width: 1000px;
+    max-width: 1400px; // 🔥 增加最大宽度
+  }
+}
+
+// 🔥 响应式优化:标准桌面端
+@media (min-width: 1025px) and (max-width: 1440px) {
+  .modal-container {
+    width: 90vw;
+    min-width: 900px;
+    max-width: 1200px;
+  }
+}
+
+// 🔥 响应式优化:中等屏幕适配
+@media (min-width: 769px) and (max-width: 1024px) {
+  .modal-container {
+    width: 90vw;
+    min-width: 700px; // 🔥 中等屏幕最小宽度700px
+    max-width: 1000px;
+  }
+}
+
+// 🔥 响应式优化:小屏幕适配
+@media (max-width: 768px) {
+  .modal-container {
+    width: 95vw;
+    min-width: auto; // 🔥 移除最小宽度限制
+    max-height: 96vh; // 🔥 增加到96vh
+  }
+  
+  .modal-header {
+    padding: 20px 24px;
+    
+    h2 {
+      font-size: 18px;
+    }
+  }
+  
+  .modal-body {
+    padding: 20px 24px 80px 24px; // 🔥 底部内边距从60px增加到80px,确保按钮完全可见
+  }
+  
+  .assignment-summary-section {
+    margin-top: 24px;
+    margin-bottom: 50px; // 🔥 底部间距从32px增加到50px,确保按钮完全可见
+    padding: 20px;
+    
+    .summary-header h4 {
+      font-size: 15px;
+    }
+    
+    .selection-summary .summary-item {
+      font-size: 13px;
+      flex-wrap: wrap;
+    }
+
+    .modal-actions {
+      margin: 16px -20px -20px -20px; // 🔥 负边距延伸到容器边缘
+      padding: 16px 20px; // 🔥 四周内边距
+      background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.98) 100%);
+      border-top: 2px solid #e8e8e8;
+      box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
+      border-radius: 0 0 12px 12px;
+      
+      .btn-secondary, .btn-primary {
+        padding: 12px 22px;
+        font-size: 14px;
+        min-width: 95px;
+        font-weight: 600;
+      }
+    }
+  }
+}
+
+@media (max-width: 480px) {
+  .modal-container {
+    width: 98vw;
+    min-width: auto; // 🔥 移除最小宽度限制
+    max-height: 97vh; // 🔥 增加到97vh
+    border-radius: 8px;
+  }
+  
+  .modal-header {
+    padding: 16px 20px;
+    
+    h2 {
+      font-size: 16px;
+    }
+  }
+  
+  .modal-body {
+    padding: 16px 20px 70px 20px; // 🔥 底部内边距从50px增加到70px,确保按钮完全可见
+  }
+  
+  .assignment-summary-section {
+    margin-top: 20px;
+    margin-bottom: 40px; // 🔥 底部间距从24px增加到40px,确保按钮完全可见
+    padding: 16px;
+    border-radius: 8px;
+    
+    .summary-header h4 {
+      font-size: 14px;
+    }
+    
+    .selection-summary .summary-item {
+      font-size: 12px;
+      margin-bottom: 8px;
+    }
+    
+    .modal-actions {
+      margin: 16px -16px -16px -16px; // 🔥 负边距延伸到容器边缘
+      padding: 16px; // 🔥 四周内边距
+      gap: 10px; // 🔥 减小按钮间距节省空间
+      background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.98) 100%);
+      border-top: 2px solid #e8e8e8;
+      box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08); // 🔥 增强阴影
+      border-radius: 0 0 8px 8px;
+      
+      .btn-secondary, .btn-primary {
+        padding: 12px 20px; // 🔥 增加padding确保易点击
+        font-size: 14px; // 🔥 保持字体大小
+        min-width: 90px; // 🔥 最小宽度
+        font-weight: 600;
+      }
+    }
+  }
 }

+ 111 - 0
src/modules/project/components/custom-date-picker/custom-date-picker.component.html

@@ -0,0 +1,111 @@
+<div class="custom-date-picker" [class.disabled]="disabled">
+  <!-- 输入框 -->
+  <div class="date-input" (click)="toggleCalendar()">
+    <span class="date-text" [class.placeholder]="!selectedDate">
+      {{ getDisplayText() }}
+    </span>
+    <svg class="calendar-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+      <path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/>
+    </svg>
+  </div>
+
+  <!-- 日历弹窗 -->
+  @if (showCalendar) {
+    <div class="calendar-overlay" (click)="closeCalendar()"></div>
+    <div class="calendar-popup" (click)="$event.stopPropagation()">
+      <!-- 日历头部 -->
+      <div class="calendar-header">
+        <button 
+          class="nav-btn" 
+          (click)="viewMode === 'year' ? previousYearPage() : previousMonth()" 
+          type="button">
+          ◀
+        </button>
+        
+        <div class="month-year" (click)="toggleViewMode()">
+          @if (viewMode === 'day') {
+            {{ getCurrentMonthText() }}
+          } @else if (viewMode === 'month') {
+            {{ currentMonth.getFullYear() }}年
+          } @else {
+            {{ years[0] }} - {{ years[years.length - 1] }}
+          }
+        </div>
+        
+        <button 
+          class="nav-btn" 
+          (click)="viewMode === 'year' ? nextYearPage() : nextMonth()" 
+          type="button">
+          ▶
+        </button>
+      </div>
+
+      <!-- 星期标题(仅日期视图显示) -->
+      @if (viewMode === 'day') {
+        <div class="weekdays">
+          @for (day of weekDays; track day) {
+            <div class="weekday">{{ day }}</div>
+          }
+        </div>
+      }
+
+      <!-- 日期网格(日期视图) -->
+      @if (viewMode === 'day') {
+        <div class="days-grid">
+        @for (week of getCalendarWeeks(); track $index) {
+          <div class="week-row">
+            @for (calendarDay of week; track calendarDay.date) {
+              <button 
+                class="day-cell"
+                [class.other-month]="!calendarDay.isCurrentMonth"
+                [class.today]="calendarDay.isToday"
+                [class.selected]="calendarDay.isSelected"
+                (click)="selectDate(calendarDay)"
+                type="button">
+                {{ calendarDay.day }}
+              </button>
+            }
+          </div>
+        }
+        </div>
+      }
+
+      <!-- 月份选择网格(月份视图) -->
+      @if (viewMode === 'month') {
+        <div class="months-grid">
+          @for (month of months; track $index) {
+            <button 
+              class="month-cell"
+              [class.selected]="$index === currentMonth.getMonth()"
+              (click)="selectMonth($index)"
+              type="button">
+              {{ month }}
+            </button>
+          }
+        </div>
+      }
+
+      <!-- 年份选择网格(年份视图) -->
+      @if (viewMode === 'year') {
+        <div class="years-grid">
+          @for (year of years; track year) {
+            <button 
+              class="year-cell"
+              [class.selected]="year === currentMonth.getFullYear()"
+              (click)="selectYear(year)"
+              type="button">
+              {{ year }}
+            </button>
+          }
+        </div>
+      }
+
+      <!-- 底部操作 -->
+      <div class="calendar-footer">
+        <button class="today-btn" (click)="goToToday()" type="button">
+          今天
+        </button>
+      </div>
+    </div>
+  }
+</div>

+ 644 - 0
src/modules/project/components/custom-date-picker/custom-date-picker.component.scss

@@ -0,0 +1,644 @@
+.custom-date-picker {
+  position: relative;
+  width: 100%;
+
+  &.disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+
+    .date-input {
+      cursor: not-allowed;
+      background-color: #f3f4f6;
+    }
+  }
+}
+
+// 输入框
+.date-input {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 14px;
+  background: #f4f5f8;
+  border: 1.5px solid #e5e7eb;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.3s;
+  min-height: 44px;
+
+  &:hover:not(.disabled) {
+    border-color: #3880ff;
+    background: #ffffff;
+  }
+
+  .date-text {
+    flex: 1;
+    font-size: 14px;
+    color: #111827;
+
+    &.placeholder {
+      color: #9ca3af;
+    }
+  }
+
+  .calendar-icon {
+    width: 20px;
+    height: 20px;
+    color: #3880ff;
+    flex-shrink: 0;
+  }
+}
+
+// 遮罩层
+.calendar-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.3);
+  z-index: 1000;
+  animation: fadeIn 0.2s ease;
+}
+
+// 日历弹窗
+.calendar-popup {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: white;
+  border-radius: 16px;
+  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.2);
+  z-index: 1001;
+  
+  // 🔥 使用clamp实现平滑的响应式宽度
+  width: clamp(280px, 90vw, 600px);
+  max-width: calc(100vw - 24px);
+  max-height: 90vh;
+  
+  display: flex;
+  flex-direction: column;
+  animation: slideUp 0.3s ease;
+  overflow: hidden;
+
+  // 🔥 渐进式响应式优化
+  @media (min-width: 768px) {
+    width: clamp(340px, 42vw, 450px); // 🔥 中大屏幕:稍微增大
+  }
+  
+  @media (max-width: 767px) and (min-width: 481px) {
+    width: clamp(320px, 85vw, 600px); // 🔥 中等屏幕:85%无限制
+  }
+  
+  @media (max-width: 480px) {
+    width: clamp(300px, 92vw, 440px); // 🔥 手机:92%,提高上限
+    max-height: 92vh;
+  }
+  
+  @media (max-width: 375px) {
+    width: clamp(280px, 94vw, 360px); // 🔥 小手机:94%
+    max-height: 94vh;
+  }
+  
+  @media (max-width: 320px) {
+    width: clamp(280px, 96vw, 320px); // 🔥 超小手机:96%几乎全屏
+  }
+}
+
+// 日历头部
+.calendar-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px;
+  border-bottom: 1px solid #f0f0f0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+
+  .nav-btn {
+    width: 36px;
+    height: 36px;
+    border: 2px solid rgba(255, 255, 255, 0.3);
+    background: rgba(255, 255, 255, 0.2);
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s;
+    flex-shrink: 0;
+    // 🔥 emoji箭头样式
+    font-size: 18px;
+    color: white;
+    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+
+    &:hover {
+      background: rgba(255, 255, 255, 0.4);
+      border-color: rgba(255, 255, 255, 0.5);
+      transform: scale(1.1);
+    }
+
+    &:active {
+      transform: scale(0.9);
+    }
+
+    @media (max-width: 480px) {
+      width: 32px;
+      height: 32px;
+      font-size: 16px;
+    }
+    
+    @media (max-width: 360px) {
+      width: 28px;
+      height: 28px;
+      font-size: 14px;
+    }
+  }
+
+  .month-year {
+    font-size: 16px;
+    font-weight: 600;
+    flex: 1;
+    text-align: center;
+    cursor: pointer; // 🔥 添加点击提示
+    padding: 8px 16px;
+    border-radius: 8px;
+    transition: all 0.2s;
+    user-select: none;
+    
+    &:hover {
+      background: rgba(255, 255, 255, 0.15);
+    }
+    
+    &:active {
+      transform: scale(0.95);
+    }
+  }
+}
+
+// 星期标题
+.weekdays {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  padding: 12px 16px 8px;
+  border-bottom: 1px solid #f0f0f0;
+  background: #fafafa;
+
+  .weekday {
+    text-align: center;
+    font-size: 12px;
+    font-weight: 600;
+    color: #6b7280;
+  }
+  
+  // 🔥 小屏幕优化
+  @media (max-width: 480px) {
+    padding: 10px 12px 6px;
+    
+    .weekday {
+      font-size: 11px;
+    }
+  }
+  
+  @media (max-width: 360px) {
+    padding: 8px 8px 4px;
+    
+    .weekday {
+      font-size: 10px;
+    }
+  }
+}
+
+// 日期网格容器
+.days-grid {
+  padding: 8px;
+  overflow-y: visible; // 🔥 不要滚动,确保全部显示
+  overflow-x: hidden;
+  flex: 1 1 auto;
+  // 🔥 不设置max-height,让内容自然撑开
+  
+  // 自定义滚动条样式(备用)
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+    
+    &:hover {
+      background: #a1a1a1;
+    }
+  }
+  
+  // 🔥 小屏幕紧凑布局
+  @media (max-width: 480px) {
+    padding: 6px; // 🔥 稍大内边距,保持美观
+  }
+  
+  @media (max-width: 360px) {
+    padding: 4px;
+  }
+  
+  @media (max-width: 320px) {
+    padding: 3px;
+  }
+}
+
+.week-row {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  // 🔥 使用clamp实现平滑的间距
+  gap: clamp(1px, 0.8vw, 5px);
+  margin-bottom: clamp(1px, 0.8vw, 5px);
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.day-cell {
+  aspect-ratio: 1;
+  border: none;
+  background: transparent;
+  border-radius: 8px;
+  // 🔥 使用clamp实现平滑的字体大小
+  font-size: clamp(11px, 2.5vw, 15px);
+  color: #1f2937;
+  cursor: pointer;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 500;
+  // 🔥 使用clamp实现平滑的最小高度
+  min-height: clamp(26px, 7vw, 40px);
+  width: 100%;
+
+  &:hover {
+    background: #f0f0f0;
+  }
+
+  &.other-month {
+    color: #d1d5db;
+  }
+
+  &.today {
+    background: #e0e7ff;
+    color: #3730a3;
+    font-weight: 600;
+  }
+
+  &.selected {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    font-weight: 600;
+
+    &:hover {
+      background: linear-gradient(135deg, #5568d3 0%, #6a4190 100%);
+    }
+  }
+
+  &:active {
+    transform: scale(0.95);
+  }
+
+  // 🔥 小屏幕紧凑布局
+  @media (max-width: 480px) {
+    font-size: 13px;
+    min-height: 32px; // 🔥 稍微增大确保可读
+    border-radius: 6px;
+    padding: 0;
+  }
+  
+  @media (max-width: 360px) {
+    font-size: 12px;
+    min-height: 28px; // 🔥 增大触摸区域
+    border-radius: 4px;
+    font-weight: 400;
+  }
+  
+  @media (max-width: 320px) {
+    font-size: 11px;
+    min-height: 26px;
+    border-radius: 3px;
+  }
+}
+
+// 底部操作
+.calendar-footer {
+  padding: 12px 16px;
+  border-top: 1px solid #f0f0f0;
+  display: flex;
+  justify-content: center;
+  flex-shrink: 0; // 🔥 防止底部被压缩
+  background: white; // 🔥 确保背景色
+
+  .today-btn {
+    padding: 8px 20px;
+    background: transparent;
+    border: 1px solid #667eea;
+    border-radius: 8px;
+    color: #667eea;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &:hover {
+      background: #667eea;
+      color: white;
+    }
+
+    &:active {
+      transform: scale(0.95);
+    }
+  }
+}
+
+// 动画
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translate(-50%, -45%);
+  }
+  to {
+    opacity: 1;
+    transform: translate(-50%, -50%);
+  }
+}
+
+// ============ 🔥 月份选择器 ============
+.months-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr); // 3列布局
+  gap: 12px;
+  padding: 16px;
+  overflow-y: auto;
+  max-height: 280px;
+  
+  @media (max-width: 480px) {
+    gap: 8px;
+    padding: 12px;
+  }
+}
+
+.month-cell {
+  border: none;
+  background: #f5f5f5;
+  border-radius: 12px;
+  padding: 16px 8px;
+  font-size: 15px;
+  font-weight: 500;
+  color: #1f2937;
+  cursor: pointer;
+  transition: all 0.2s;
+  min-height: 54px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  &:hover {
+    background: #e8e8e8;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+  }
+  
+  &.selected {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    font-weight: 600;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+    
+    &:hover {
+      background: linear-gradient(135deg, #5568d3 0%, #6a4190 100%);
+    }
+  }
+  
+  &:active {
+    transform: translateY(0) scale(0.95);
+  }
+  
+  @media (max-width: 480px) {
+    font-size: 14px;
+    min-height: 48px;
+    padding: 12px 6px;
+  }
+}
+
+// ============ 🔥 年份选择器 ============
+.years-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr); // 3列布局
+  gap: 12px;
+  padding: 16px;
+  overflow-y: auto;
+  max-height: 280px;
+  
+  // 自定义滚动条
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+    
+    &:hover {
+      background: #a1a1a1;
+    }
+  }
+  
+  @media (max-width: 480px) {
+    gap: 8px;
+    padding: 12px;
+    max-height: 240px;
+  }
+}
+
+.year-cell {
+  border: none;
+  background: #f5f5f5;
+  border-radius: 12px;
+  padding: 16px 8px;
+  font-size: 15px;
+  font-weight: 500;
+  color: #1f2937;
+  cursor: pointer;
+  transition: all 0.2s;
+  min-height: 54px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  &:hover {
+    background: #e8e8e8;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+  }
+  
+  &.selected {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    font-weight: 600;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+    
+    &:hover {
+      background: linear-gradient(135deg, #5568d3 0%, #6a4190 100%);
+    }
+  }
+  
+  &:active {
+    transform: translateY(0) scale(0.95);
+  }
+  
+  @media (max-width: 480px) {
+    font-size: 14px;
+    min-height: 48px;
+    padding: 12px 6px;
+  }
+}
+
+// ============ 🔥 响应式断点优化 ============
+// 🔥 超小屏幕紧凑布局(≤360px)
+@media (max-width: 360px) {
+  .calendar-popup {
+    width: 280px;
+    max-width: calc(100vw - 16px); // 更小边距
+  }
+  
+  .calendar-header {
+    padding: 10px 8px; // 减小内边距
+    
+    .month-year {
+      font-size: 13px;
+      padding: 6px 8px;
+    }
+  }
+  
+  .weekdays {
+    padding: 8px 4px 6px; // 紧凑内边距
+    
+    .weekday {
+      font-size: 11px;
+    }
+  }
+  
+  .months-grid,
+  .years-grid {
+    grid-template-columns: repeat(3, 1fr);
+    gap: 6px;
+    padding: 8px;
+  }
+  
+  .month-cell,
+  .year-cell {
+    font-size: 13px;
+    min-height: 40px;
+    padding: 8px 4px;
+  }
+  
+  .calendar-footer {
+    padding: 8px 12px; // 减小底部内边距
+    
+    .today-btn {
+      font-size: 13px;
+      padding: 6px 16px;
+    }
+  }
+}
+
+// 🔥 小手机紧凑布局(≤320px)
+@media (max-width: 320px) {
+  .calendar-popup {
+    width: 260px;
+    max-width: calc(100vw - 12px);
+  }
+  
+  .calendar-header {
+    padding: 8px 6px;
+    
+    .nav-btn {
+      width: 26px;
+      height: 26px;
+      font-size: 13px;
+    }
+    
+    .month-year {
+      font-size: 12px;
+      padding: 4px 6px;
+    }
+  }
+  
+  .weekdays {
+    padding: 6px 2px 4px;
+    
+    .weekday {
+      font-size: 10px;
+    }
+  }
+  
+  .days-grid {
+    padding: 2px;
+  }
+  
+  .day-cell {
+    font-size: 10px;
+    min-height: 22px;
+    border-radius: 3px;
+  }
+  
+  .calendar-footer {
+    padding: 6px 8px;
+    
+    .today-btn {
+      font-size: 12px;
+      padding: 5px 12px;
+    }
+  }
+}
+
+// 平板横屏
+@media (min-width: 768px) and (max-width: 1024px) {
+  .calendar-popup {
+    width: 360px;
+  }
+  
+  .months-grid,
+  .years-grid {
+    grid-template-columns: repeat(4, 1fr); // 平板4列
+  }
+}
+
+// 大屏幕
+@media (min-width: 1024px) {
+  .calendar-popup {
+    width: 380px;
+  }
+  
+  .months-grid,
+  .years-grid {
+    grid-template-columns: repeat(4, 1fr); // 桌面4列
+  }
+}

+ 291 - 0
src/modules/project/components/custom-date-picker/custom-date-picker.component.ts

@@ -0,0 +1,291 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+interface CalendarDay {
+  date: Date;
+  day: number;
+  isCurrentMonth: boolean;
+  isToday: boolean;
+  isSelected: boolean;
+}
+
+@Component({
+  selector: 'app-custom-date-picker',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './custom-date-picker.component.html',
+  styleUrls: ['./custom-date-picker.component.scss']
+})
+export class CustomDatePickerComponent implements OnInit {
+  @Input() selectedDate: Date | null = null;
+  @Input() placeholder: string = '选择日期';
+  @Input() disabled: boolean = false;
+  @Output() selectedDateChange = new EventEmitter<Date>();
+
+  showCalendar = false;
+  currentMonth: Date = new Date();
+  calendarDays: CalendarDay[] = [];
+  weekDays = ['日', '一', '二', '三', '四', '五', '六'];
+  
+  // 🔥 新增:视图模式
+  viewMode: 'day' | 'month' | 'year' = 'day';
+  
+  months = [
+    '1月', '2月', '3月', '4月', '5月', '6月',
+    '7月', '8月', '9月', '10月', '11月', '12月'
+  ];
+  
+  // 🔥 新增:年份列表(当前年份前后各5年)
+  years: number[] = [];
+
+  ngOnInit() {
+    if (this.selectedDate) {
+      this.currentMonth = new Date(this.selectedDate);
+    }
+    this.generateCalendar();
+    this.generateYears();
+  }
+  
+  /**
+   * 🔥 生成年份列表
+   */
+  generateYears() {
+    const currentYear = this.currentMonth.getFullYear();
+    this.years = [];
+    for (let i = currentYear - 5; i <= currentYear + 5; i++) {
+      this.years.push(i);
+    }
+  }
+
+  /**
+   * 生成日历数据
+   */
+  generateCalendar() {
+    const year = this.currentMonth.getFullYear();
+    const month = this.currentMonth.getMonth();
+    
+    // 本月第一天
+    const firstDay = new Date(year, month, 1);
+    const firstDayOfWeek = firstDay.getDay(); // 0-6 (周日-周六)
+    
+    // 本月最后一天
+    const lastDay = new Date(year, month + 1, 0);
+    const lastDate = lastDay.getDate();
+    
+    // 上个月最后一天
+    const prevMonthLastDay = new Date(year, month, 0);
+    const prevMonthLastDate = prevMonthLastDay.getDate();
+    
+    const days: CalendarDay[] = [];
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    
+    // 上个月的日期(填充前面的空白)
+    for (let i = firstDayOfWeek - 1; i >= 0; i--) {
+      const date = new Date(year, month - 1, prevMonthLastDate - i);
+      days.push({
+        date,
+        day: prevMonthLastDate - i,
+        isCurrentMonth: false,
+        isToday: false,
+        isSelected: false
+      });
+    }
+    
+    // 本月的日期
+    for (let day = 1; day <= lastDate; day++) {
+      const date = new Date(year, month, day);
+      date.setHours(0, 0, 0, 0);
+      
+      const isToday = date.getTime() === today.getTime();
+      const isSelected = this.selectedDate 
+        ? date.getTime() === new Date(this.selectedDate).setHours(0, 0, 0, 0)
+        : false;
+      
+      days.push({
+        date,
+        day,
+        isCurrentMonth: true,
+        isToday,
+        isSelected
+      });
+    }
+    
+    // 下个月的日期(填充后面的空白,确保6行)
+    const remainingDays = 42 - days.length; // 6行 × 7列 = 42天
+    for (let day = 1; day <= remainingDays; day++) {
+      const date = new Date(year, month + 1, day);
+      days.push({
+        date,
+        day,
+        isCurrentMonth: false,
+        isToday: false,
+        isSelected: false
+      });
+    }
+    
+    this.calendarDays = days;
+  }
+
+  /**
+   * 切换日历显示
+   */
+  toggleCalendar() {
+    if (this.disabled) return;
+    this.showCalendar = !this.showCalendar;
+    if (this.showCalendar) {
+      this.generateCalendar();
+    }
+  }
+
+  /**
+   * 关闭日历
+   */
+  closeCalendar() {
+    this.showCalendar = false;
+  }
+
+  /**
+   * 选择日期
+   */
+  selectDate(calendarDay: CalendarDay) {
+    if (!calendarDay.isCurrentMonth) {
+      // 如果点击的是上/下月的日期,切换到那个月
+      this.currentMonth = new Date(calendarDay.date);
+      this.generateCalendar();
+      return;
+    }
+    
+    this.selectedDate = calendarDay.date;
+    this.selectedDateChange.emit(calendarDay.date);
+    this.closeCalendar();
+  }
+
+  /**
+   * 上一个月
+   */
+  previousMonth() {
+    this.currentMonth = new Date(
+      this.currentMonth.getFullYear(),
+      this.currentMonth.getMonth() - 1,
+      1
+    );
+    this.generateCalendar();
+  }
+
+  /**
+   * 下一个月
+   */
+  nextMonth() {
+    this.currentMonth = new Date(
+      this.currentMonth.getFullYear(),
+      this.currentMonth.getMonth() + 1,
+      1
+    );
+    this.generateCalendar();
+  }
+
+  /**
+   * 回到今天
+   */
+  goToToday() {
+    this.currentMonth = new Date();
+    this.generateCalendar();
+  }
+
+  /**
+   * 获取当前月份显示文本
+   */
+  getCurrentMonthText(): string {
+    const year = this.currentMonth.getFullYear();
+    const month = this.currentMonth.getMonth();
+    return `${year}年 ${this.months[month]}`;
+  }
+  
+  /**
+   * 🔥 切换视图模式
+   */
+  toggleViewMode() {
+    if (this.viewMode === 'day') {
+      this.viewMode = 'month';
+    } else if (this.viewMode === 'month') {
+      this.viewMode = 'year';
+      this.generateYears();
+    } else {
+      this.viewMode = 'day';
+    }
+  }
+  
+  /**
+   * 🔥 选择月份
+   */
+  selectMonth(monthIndex: number) {
+    this.currentMonth = new Date(
+      this.currentMonth.getFullYear(),
+      monthIndex,
+      1
+    );
+    this.generateCalendar();
+    this.viewMode = 'day';
+  }
+  
+  /**
+   * 🔥 选择年份
+   */
+  selectYear(year: number) {
+    this.currentMonth = new Date(
+      year,
+      this.currentMonth.getMonth(),
+      1
+    );
+    this.generateCalendar();
+    this.viewMode = 'month';
+  }
+  
+  /**
+   * 🔥 年份选择:上一页(前10年)
+   */
+  previousYearPage() {
+    const firstYear = this.years[0];
+    this.years = [];
+    for (let i = firstYear - 10; i < firstYear; i++) {
+      this.years.push(i);
+    }
+  }
+  
+  /**
+   * 🔥 年份选择:下一页(后10年)
+   */
+  nextYearPage() {
+    const lastYear = this.years[this.years.length - 1];
+    this.years = [];
+    for (let i = lastYear + 1; i <= lastYear + 10; i++) {
+      this.years.push(i);
+    }
+  }
+
+  /**
+   * 格式化显示日期
+   */
+  getDisplayText(): string {
+    if (!this.selectedDate) {
+      return this.placeholder;
+    }
+    const year = this.selectedDate.getFullYear();
+    const month = String(this.selectedDate.getMonth() + 1).padStart(2, '0');
+    const day = String(this.selectedDate.getDate()).padStart(2, '0');
+    return `${year}/${month}/${day}`;
+  }
+
+  /**
+   * 分组日历天数(每7天一组)
+   */
+  getCalendarWeeks(): CalendarDay[][] {
+    const weeks: CalendarDay[][] = [];
+    for (let i = 0; i < this.calendarDays.length; i += 7) {
+      weeks.push(this.calendarDays.slice(i, i + 7));
+    }
+    return weeks;
+  }
+}

+ 16 - 22
src/modules/project/components/quotation-editor.component.html

@@ -17,25 +17,19 @@
         <div class="product-header">
           <h3>设计产品 ({{ getDisplayedSpaceCount() }}个空间)</h3>
           <div class="product-actions">
-            <button class="btn-primary" (click)="generateQuotationFromProducts()">
+            <button class="btn-primary" (click)="generateQuotationFromProducts($event)" type="button">
               <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                 <path fill="currentColor" d="M447.1 96h-288C131.3 96 112 115.3 112 140.4v231.2C112 396.7 131.3 416 159.1 416h288c27.6 0 48-19.3 48-44.4V140.4C495.1 115.3 475.6 96 447.1 96zM447.1 144v192h-288V144H447.1z"/>
               </svg>
               生成报价
             </button>
-            <button class="btn-secondary" (click)="openAddProductModal()">
+            <button class="btn-secondary" (click)="openAddProductModal($event)" type="button">
               <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                 <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM256 48c114.7 0 208 93.31 208 208s-93.31 208-208 208S48 370.7 48 256S141.3 48 256 48zM256 336c13.25 0 24-10.75 24-24V280h32c13.25 0 24-10.75 24-24s-10.75-24-24-24h-32V176c0-13.25-10.75-24-24-24s-24 10.75-24 24v56H200c-13.25 0-24 10.75-24 24s10.75 24 24 24h32v32C232 325.3 242.8 336 256 336z"/>
               </svg>
-              添加产品
+              添加空间
             </button>
-            <button class="btn-outline" (click)="cleanupDuplicateProducts()" title="清理重复产品">
-              <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                <path fill="currentColor" d="M432 64c26.5 0 48 21.5 48 48v288c0 26.5-21.5 48-48 48H80c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h352zm0-32H80C35.8 32 0 67.8 0 112v288c0 44.2 35.8 80 80 80h352c44.2 0 80-35.8 80-80V112c0-44.2-35.8-80-80-80zM362.7 184l-82.7 82.7L197.3 184c-6.2-6.2-16.4-6.2-22.6 0-6.2 6.2-6.2 16.4 0 22.6l82.7 82.7-82.7 82.7c-6.2 6.2-6.2 16.4 0 22.6 6.2 6.2 16.4 6.2 22.6 0l82.7-82.7 82.7 82.7c6.2 6.2 16.4 6.2 22.6 0 6.2-6.2 6.2-16.4 0-22.6L303.3 289.3l82.7-82.7c6.2-6.2 6.2-16.4 0-22.6-6.2-6.2-16.4-6.2-22.6 0z"/>
-              </svg>
-              清理重复
-            </button>
-            <button class="btn-outline" (click)="saveQuotation()">
+            <button class="btn-outline" (click)="saveQuotation($event)" type="button">
               <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                 <path fill="currentColor" d="M504.1 141C490.6 121.4 471.1 107.5 447.8 96C424.6 84.51 400.8 80 376.1 80H136c-24.74 0-48.48 4.511-71.79 16.01C40.88 107.5 21.36 121.4 7.85 141C-5.654 160.6-1.466 180.2 11.66 195.7L144.1 353c11.14 13.4 27.62 21 44.8 21h124.3c17.18 0 33.66-7.6 44.8-21l133.3-157.4C504.5 180.2 508.6 160.6 504.1 141z"/>
               </svg>
@@ -108,7 +102,7 @@
                 <div class="product-info">
                   <div class="product-title">
                     <div class="product-icon">
-                      <i class="icon-{{ getProductIconForSpace(space.name) }}"></i>
+                      <span class="emoji-icon">{{ getProductIconForSpace(space.name) }}</span>
                     </div>
                     <div class="product-details">
                       <h3 class="product-name">{{ space.name }}</h3>
@@ -130,14 +124,10 @@
                 <div class="product-actions">
                   @if (canEdit) {
                     <button class="btn-icon" (click)="openEditProductModal(space.productId); $event.stopPropagation()" title="编辑">
-                      <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                        <path fill="currentColor" d="M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32l39.38 39.38c25 25 25 65.62 0 90.62l-109.5 109.5c-25 25-65.62 25-90.62 0l-109.5-109.5c-25-25-25-65.62 0-90.62L272.1 19.32z"/>
-                      </svg>
+                      <span class="emoji-icon">✏️</span>
                     </button>
                     <button class="btn-icon danger" (click)="deleteProduct(space.productId); $event.stopPropagation()" title="删除">
-                      <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                        <path fill="currentColor" d="M96 480c0 17.67 14.33 32 32 32h256c17.67 0 32-14.33 32-32V128H96V480zM312 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24c-13.25 0-24-10.75-24-24V216C288 202.8 298.8 192 312 192zM200 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24s-24-10.75-24-24V216C176 202.8 186.8 192 200 192z"/>
-                      </svg>
+                      <span class="emoji-icon">🗑️</span>
                     </button>
                   }
                   <div class="product-toggle">
@@ -571,10 +561,8 @@
                         <span class="unit">%</span>
                       </div>
                     </div>
-                    <button class="btn-remove-small" (click)="removeSelectedCollaborator(collab.member.id)">
-                      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                        <path fill="currentColor" d="M256 48C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48zm52.7 283.3L256 278.6l-52.7 52.7c-6.2 6.2-16.4 6.2-22.6 0-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3l52.7-52.7-52.7-52.7c-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3 6.2-6.2 16.4-6.2 22.6 0l52.7 52.7 52.7-52.7c6.2-6.2 16.4-6.2 22.6 0 6.2 6.2 6.2 16.4 0 22.6L278.6 256l52.7 52.7c6.2 6.2 6.2 16.4 0 22.6-6.2 6.3-16.4 6.3-22.6 0z"/>
-                      </svg>
+                    <button class="btn-remove-small" (click)="removeSelectedCollaborator(collab.member.id)" title="移除">
+                      ❌
                     </button>
                   </div>
                 }
@@ -613,7 +601,13 @@
       <div class="modal-body">
         <!-- 预设场景选择 -->
         <div class="form-group">
-          <label class="form-label">选择空间场景 <span class="hint-text">(可多选)</span></label>
+          <label class="form-label">选择空间场景 
+            @if (isEditMode) {
+              <span class="hint-text">(编辑模式:单选)</span>
+            } @else {
+              <span class="hint-text">(可多选)</span>
+            }
+          </label>
           <div class="scene-grid">
             @for (scene of getPresetScenes(); track scene) {
               <button

+ 230 - 61
src/modules/project/components/quotation-editor.component.scss

@@ -614,6 +614,15 @@
               border-radius: 10px;
               color: var(--ion-color-primary);
               font-size: 20px;
+              
+              // 🔥 Emoji图标样式
+              .emoji-icon {
+                font-size: 24px; // 🔥 Emoji稍大一点
+                line-height: 1;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+              }
             }
 
             .product-details {
@@ -675,21 +684,39 @@
             color: var(--ion-color-medium);
             cursor: pointer;
             transition: all 0.2s;
+            display: flex; // 🔥 添加flex布局
+            align-items: center; // 🔥 垂直居中
+            justify-content: center; // 🔥 水平居中
 
             &:hover {
               background: var(--ion-color-light-shade);
               color: var(--ion-color-dark);
+              transform: scale(1.1); // 🔥 hover时轻微放大
             }
 
             &.danger:hover {
               background: var(--ion-color-danger-tint);
               color: var(--ion-color-danger);
             }
+            
+            &:active {
+              transform: scale(0.95); // 🔥 点击时缩小
+            }
 
             .icon {
               width: 16px;
               height: 16px;
             }
+            
+            // 🔥 Emoji图标样式
+            .emoji-icon {
+              font-size: 18px; // 🔥 Emoji大小
+              line-height: 1;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              filter: grayscale(0); // 🔥 确保颜色正常显示
+            }
           }
 
           .product-toggle {
@@ -1283,25 +1310,38 @@
     .product-content .process-grid {
       grid-template-columns: 1fr;
     }
+
+    // 🔥 小屏幕下优化输入框样式
+    .allocation-input-section {
+      .input-with-currency {
+        .currency-symbol {
+          left: 12px; // 🔥 减小左边距
+          font-size: 14px; // 🔥 减小字体
+        }
+
+        .amount-input {
+          padding: 12px 12px 12px 36px; // 🔥 调整padding适配小屏幕
+          font-size: 16px; // 🔥 减小字体从18px到16px
+        }
+      }
+    }
   }
 }
 
 // ============ 模态框样式 ============
 
 .modal-overlay {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.6);
-  backdrop-filter: blur(4px);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 10000;
-  padding: 20px;
-  animation: fadeIn 0.2s ease-out;
+  position: fixed !important; // 🔥 确保生效
+  top: 0 !important; // 🔥 确保生效
+  left: 0 !important; // 🔥 确保生效
+  right: 0 !important; // 🔥 确保生效
+  bottom: 0 !important; // 🔥 确保生效
+  background: rgba(0, 0, 0, 0.5) !important; // 🔥 确保生效
+  display: flex !important; // 🔥 确保生效
+  align-items: center !important; // 🔥 确保生效
+  justify-content: center !important; // 🔥 确保生效
+  z-index: 1000 !important; // 🔥 设置为1000,低于设计师分配弹窗(1050),确保不会覆盖
+  animation: fadeIn 0.3s ease-out;
 }
 
 @keyframes fadeIn {
@@ -1313,15 +1353,18 @@
   }
 }
 
-.modal-container {
-  background: white;
-  border-radius: 16px;
-  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
-  max-width: 600px;
-  width: 100%;
-  max-height: 90vh;
-  display: flex;
-  flex-direction: column;
+.modal-container,
+.add-product-modal { // 🔥 额外添加这个类选择器
+  background: white !important; // 🔥 确保生效
+  border-radius: 16px !important; // 🔥 确保生效
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3) !important; // 🔥 确保生效
+  max-width: 600px !important; // 🔥 确保生效
+  width: 100% !important; // 🔥 确保生效
+  max-height: 95vh !important; // 🔥 增加到95vh,提供更多空间
+  height: auto !important; // 🔥 允许自动高度
+  display: flex !important; // 🔥 确保生效
+  flex-direction: column !important; // 🔥 确保生效
+  overflow: hidden !important; // 🔥 防止外部滚动
   animation: slideUp 0.3s ease-out;
 }
 
@@ -1342,6 +1385,7 @@
   justify-content: space-between;
   padding: 20px 24px;
   border-bottom: 1px solid #e5e7eb;
+  flex-shrink: 0; // 🔥 防止header被压缩
 
   h3 {
     margin: 0;
@@ -1380,9 +1424,10 @@
 }
 
 .modal-body {
-  flex: 1;
-  overflow-y: auto;
-  padding: 24px;
+  flex: 1 1 auto !important; // 🔥 允许增长和缩小
+  overflow-y: auto !important; // 🔥 确保生效
+  padding: 24px !important; // 🔥 恢复底部padding,确保内容可以完全滚动显示
+  min-height: 0; // 🔥 允许flex容器正确缩小
 
   &::-webkit-scrollbar {
     width: 8px;
@@ -1410,6 +1455,10 @@
   gap: 12px;
   padding: 16px 24px;
   border-top: 1px solid #e5e7eb;
+  flex-shrink: 0 !important; // 🔥 关键!防止footer被压缩
+  background: white !important; // 🔥 确保背景不透明
+  z-index: 10; // 🔥 确保在最上层
+  position: relative; // 🔥 为z-index生效
 
   button {
     padding: 10px 20px;
@@ -1465,19 +1514,19 @@
 
   .form-input,
   .form-select {
-    width: 100%;
-    padding: 10px 14px;
-    border: 1.5px solid #e5e7eb;
-    border-radius: 8px;
+    width: 100% !important; // 🔥 确保生效
+    padding: 10px 14px !important; // 🔥 确保生效
+    border: 1.5px solid #e5e7eb !important; // 🔥 确保生效
+    border-radius: 8px !important; // 🔥 确保生效
     font-size: 14px;
     color: #111827;
     transition: all 0.2s ease;
     outline: none;
-    background: white;
+    background: white !important; // 🔥 确保生效
 
     &:focus {
-      border-color: #667eea;
-      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+      border-color: #667eea !important; // 🔥 确保生效
+      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; // 🔥 确保生效
     }
 
     &::placeholder {
@@ -1526,24 +1575,24 @@
   }
 
   .radio-group {
-    display: flex;
+    display: flex !important; // 🔥 确保生效
     gap: 12px;
     flex-wrap: wrap;
 
     .radio-label {
-      display: flex;
+      display: flex !important; // 🔥 确保生效
       align-items: center;
       gap: 8px;
-      padding: 10px 16px;
-      border: 1.5px solid #e5e7eb;
-      border-radius: 8px;
+      padding: 10px 16px !important; // 🔥 确保生效
+      border: 1.5px solid #e5e7eb !important; // 🔥 确保生效
+      border-radius: 8px !important; // 🔥 确保生效
       cursor: pointer;
       transition: all 0.2s ease;
-      background: white;
+      background: white !important; // 🔥 确保生效
 
       &:hover {
-        border-color: #667eea;
-        background: #f5f7ff;
+        border-color: #667eea !important; // 🔥 确保生效
+        background: #f5f7ff !important; // 🔥 确保生效
       }
 
       input[type="radio"] {
@@ -1558,8 +1607,8 @@
       }
 
       &:has(input:checked) {
-        border-color: #667eea;
-        background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+        border-color: #667eea !important; // 🔥 确保生效
+        background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%) !important; // 🔥 确保生效
         font-weight: 500;
       }
     }
@@ -1569,28 +1618,28 @@
 // ============ 场景选择网格 ============
 
 .scene-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  display: grid !important; // 🔥 确保生效
+  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important; // 🔥 确保生效
   gap: 12px;
 
   .scene-card {
-    padding: 16px 12px;
-    border: 1.5px solid #e5e7eb;
-    border-radius: 10px;
-    background: white;
+    padding: 16px 12px !important; // 🔥 确保生效
+    border: 1.5px solid #e5e7eb !important; // 🔥 确保生效
+    border-radius: 10px !important; // 🔥 确保生效
+    background: white !important; // 🔥 确保生效
     cursor: pointer;
     transition: all 0.2s ease;
     text-align: center;
-    display: flex;
+    display: flex !important; // 🔥 确保生效
     flex-direction: column;
     align-items: center;
     justify-content: center;
     gap: 8px;
-    position: relative; //  为选中标记定位
+    position: relative; // ⏭️ 为选中标记定位
 
     &:hover {
-      border-color: #667eea;
-      background: #f5f7ff;
+      border-color: #667eea !important; // 🔥 确保生效
+      background: #f5f7ff !important; // 🔥 确保生效
       transform: translateY(-2px);
       box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
     }
@@ -1600,8 +1649,8 @@
     }
 
     &.selected {
-      border-color: #667eea;
-      background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
+      border-color: #667eea !important; // 🔥 确保生效
+      background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%) !important; // 🔥 确保生效
       box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
 
       .scene-name {
@@ -2008,11 +2057,13 @@
             color: #94a3b8;
             pointer-events: none;
             z-index: 1;
+            min-width: 20px; // 🔥 确保符号有固定宽度
+            flex-shrink: 0; // 🔥 防止缩小
           }
 
           .amount-input {
             width: 100%;
-            padding: 14px 16px 14px 40px;
+            padding: 14px 16px 14px 44px; // 🔥 增加左边距从40px到44px
             border: 2px solid #e2e8f0;
             border-radius: 12px;
             font-size: 18px;
@@ -2022,6 +2073,7 @@
             outline: none;
             background: white;
             font-variant-numeric: tabular-nums;
+            min-width: 0; // 🔥 允许在flex容器中收缩
 
             &:hover {
               border-color: #cbd5e1;
@@ -2297,6 +2349,9 @@
       transform: translateY(-50%);
       width: 18px;
       height: 18px;
+      min-width: 18px; // 🔥 防止被挤压
+      min-height: 18px; // 🔥 防止被挤压
+      flex-shrink: 0; // 🔥 防止缩小
       color: #94a3b8;
       pointer-events: none;
     }
@@ -2308,6 +2363,7 @@
       border-radius: 12px;
       font-size: 14px;
       transition: all 0.2s ease;
+      min-width: 0; // 🔥 允许缩小但不会超出容器
 
       &:focus {
         outline: none;
@@ -2560,6 +2616,8 @@
         .btn-remove-small {
           width: 28px;
           height: 28px;
+          min-width: 28px; // 🔥 防止被挤压
+          min-height: 28px; // 🔥 防止被挤压
           border-radius: 6px;
           border: none;
           background: #fee2e2;
@@ -2569,14 +2627,17 @@
           display: flex;
           align-items: center;
           justify-content: center;
+          font-size: 16px; // 🔥 emoji字体大小
+          line-height: 1;
+          flex-shrink: 0; // 🔥 防止缩小
 
           &:hover {
             background: #fecaca;
+            transform: scale(1.05); // 🔥 hover时轻微放大
           }
 
-          svg {
-            width: 14px;
-            height: 14px;
+          &:active {
+            transform: scale(0.95); // 🔥 点击时轻微缩小
           }
         }
       }
@@ -2611,13 +2672,22 @@
   }
 }
 
+// ============ 中等屏幕优化 (平板) ============
+@media (min-width: 769px) and (max-width: 1024px) {
+  .modal-container,
+  .add-product-modal {
+    max-width: 90vw !important; // 🔥 平板上午90%宽度
+    max-height: 92vh !important; // 🔥 略微减小高度
+  }
+}
+
 // ============ 移动端适配(模态框) ============
 
 @media (max-width: 768px) {
   .modal-container {
     max-width: none;
     width: 100%;
-    max-height: 95vh;
+    max-height: 98vh; // 🔥 移动端增加到98vh,留出状态栏空间
     margin: 0;
     border-radius: 16px 16px 0 0;
   }
@@ -2631,15 +2701,41 @@
   }
 
   .modal-body {
-    padding: 20px;
+    padding: 20px !important; // 🔥 保持四周padding一致
   }
-
+  
   .modal-footer {
     padding: 12px 20px;
+    flex-shrink: 0 !important; // 🔥 确保不被压缩
+    background: white !important; // 🔥 确保背景可见
+    z-index: 10; // 🔥 确保在最上层
+    position: relative;
 
     button {
       flex: 1;
       padding: 12px;
+      min-height: 44px; // 🔥 移动端按钮最小高度,易于点击
+    }
+  }
+  
+  // 🔥 小屏幕下emoji图标优化
+  .product-card {
+    .product-icon {
+      width: 36px;
+      height: 36px;
+      
+      .emoji-icon {
+        font-size: 20px; // 🔥 小屏幕下稍微减小
+      }
+    }
+    
+    .btn-icon {
+      width: 28px;
+      height: 28px;
+      
+      .emoji-icon {
+        font-size: 16px; // 🔥 小屏幕下稍微减小
+      }
     }
   }
 
@@ -2676,3 +2772,76 @@
     }
   }
 }
+
+// ============ 超小屏幕优化 (手机竖屏) ============
+@media (max-width: 480px) {
+  // 🔥 超小屏幕下模态框优化
+  .modal-container,
+  .add-product-modal {
+    max-height: 99vh !important; // 🔥 超小屏幕几乎全屏
+    border-radius: 12px 12px 0 0;
+  }
+  
+  .modal-body {
+    padding: 16px !important; // 🔥 减小padding节省空间
+  }
+  
+  .modal-footer {
+    padding: 10px 16px;
+    flex-shrink: 0 !important;
+    background: white !important;
+    z-index: 10;
+    
+    button {
+      padding: 10px 16px;
+      font-size: 13px;
+      min-height: 42px; // 🔥 超小屏幕按钮高度
+    }
+  }
+  
+  // 🔥 超小屏幕下emoji图标进一步优化
+  .product-card {
+    .product-icon {
+      width: 32px;
+      height: 32px;
+      
+      .emoji-icon {
+        font-size: 18px; // 🔥 超小屏幕下再减小
+      }
+    }
+    
+    .btn-icon {
+      width: 26px;
+      height: 26px;
+      
+      .emoji-icon {
+        font-size: 14px; // 🔥 超小屏幕下再减小
+      }
+    }
+    
+    .product-name {
+      font-size: 16px !important; // 🔥 产品名称也适当减小
+    }
+  }
+  
+  .allocation-input-section {
+    padding-left: 20px !important; // 🔥 减小左边距节省空间
+
+    .input-with-currency {
+      .currency-symbol {
+        left: 10px; // 🔥 进一步减小左边距
+        font-size: 14px;
+      }
+
+      .amount-input {
+        padding: 10px 10px 10px 32px; // 🔥 进一步优化padding
+        font-size: 15px; // 🔥 再减小字体
+        border-radius: 10px; // 🔥 稍微减小圆角
+      }
+    }
+
+    .allocation-hint {
+      font-size: 11px; // 🔥 减小提示文字
+    }
+  }
+}

+ 58 - 31
src/modules/project/components/quotation-editor.component.ts

@@ -1,4 +1,4 @@
-import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FmodeParse } from 'fmode-ng/parse';
@@ -33,7 +33,8 @@ const Parse = FmodeParse.with('nova');
   standalone: true,
   imports: [CommonModule, FormsModule],
   templateUrl: './quotation-editor.component.html',
-  styleUrls: ['./quotation-editor.component.scss']
+  styleUrls: ['./quotation-editor.component.scss'],
+  encapsulation: ViewEncapsulation.None // 🔥 禁用样式封装,确保样式全局生效
 })
 export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
   // 输入属性
@@ -637,7 +638,12 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
   /**
    * 生成基于产品的报价(增强去重逻辑)
    */
-  async generateQuotationFromProducts(): Promise<void> {
+  async generateQuotationFromProducts(event?: Event): Promise<void> {
+    if (event) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+    
     if (!this.products.length) return;
 
     this.quotation.spaces = [];
@@ -739,7 +745,9 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
     for (const allocation of Object.values(processes)) {
       const alloc = allocation as any;
       if (alloc.enabled) {
-        subtotal += Math.round(alloc.amount);
+        // 确保分配金额是整数
+        const amount = Math.round(alloc.amount);
+        subtotal += amount;
       }
     }
     return Math.round(subtotal);
@@ -1194,15 +1202,20 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
   /**
    * 保存报价
    */
-  async saveQuotation(): Promise<void> {
+  async saveQuotation(event?: Event): Promise<void> {
+    if (event) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+    
     if (!this.canEdit) return;
 
     try {
       await this.saveQuotationToProject();
-     window?.fmode?.alert('保存成功');
+      window?.fmode?.toast?.success?.('报价已自动保存');
     } catch (error) {
       console.error('保存失败:', error);
-     window?.fmode?.alert('保存失败');
+      window?.fmode?.toast?.error?.('保存失败,请重试');
     }
   }
 
@@ -1262,19 +1275,19 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
 
   getProductIcon(productType: string): string {
     const iconMap: Record<string, string> = {
-      'living_room': 'living-room',
-      'bedroom': 'bedroom',
-      'kitchen': 'kitchen',
-      'bathroom': 'bathroom',
-      'dining_room': 'dining-room',
-      'study': 'study',
-      'balcony': 'balcony',
-      'corridor': 'corridor',
-      'storage': 'storage',
-      'entrance': 'entrance',
-      'other': 'room'
+      'living_room': '🛋️', // 客厅
+      'bedroom': '🛏️', // 卧室
+      'kitchen': '🍳', // 厨房
+      'bathroom': '🚿', // 卫生间
+      'dining_room': '🍽️', // 餐厅
+      'study': '📚', // 书房
+      'balcony': '🌿', // 阳台
+      'corridor': '🚪', // 走廊
+      'storage': '📦', // 储物间
+      'entrance': '🏠', // 玄关
+      'other': '🏡' // 其他
     };
-    return iconMap[productType] || 'room';
+    return iconMap[productType] || '🏡';
   }
 
   formatPrice(price: number): string {
@@ -1339,7 +1352,12 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
   /**
    * 打开添加产品模态框
    */
-  openAddProductModal(): void {
+  openAddProductModal(event?: Event): void {
+    if (event) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+    
     // 重置表单
     this.resetNewProductForm();
     this.isEditMode = false;
@@ -1361,12 +1379,14 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
 
     // 判断是否为自定义名称
     const presetScenes = this.getPresetScenes();
-    const isPreset = presetScenes.includes(product.get('productName'));
+    const productName = product.get('productName');
+    const isPreset = presetScenes.includes(productName);
 
     this.newProduct = {
       isCustom: !isPreset,
-      sceneName: isPreset ? product.get('productName') : '',
-      productName: product.get('productName'),
+      sceneNames: isPreset ? [productName] : [], // 🔥 编辑模式下,如果是预设场景,将其添加到sceneNames数组
+      sceneName: isPreset ? productName : '',
+      productName: productName,
       spaceType: space.spaceType || this.getDefaultSpaceType(),
       styleLevel: space.styleLevel || '基础风格组',
       businessType: space.businessType || '办公空间',
@@ -1429,14 +1449,20 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
   selectScene(scene: string): void {
     this.newProduct.isCustom = false;
     
-    // ⭐ 切换选中状态(支持多选)
-    const index = this.newProduct.sceneNames.indexOf(scene);
-    if (index > -1) {
-      // 已选中,取消选中
-      this.newProduct.sceneNames.splice(index, 1);
+    // 🔥 编辑模式下只能选择一个场景
+    if (this.isEditMode) {
+      // 单选模式:直接替换
+      this.newProduct.sceneNames = [scene];
     } else {
-      // 未选中,添加选中
-      this.newProduct.sceneNames.push(scene);
+      // 添加模式:支持多选
+      const index = this.newProduct.sceneNames.indexOf(scene);
+      if (index > -1) {
+        // 已选中,取消选中
+        this.newProduct.sceneNames.splice(index, 1);
+      } else {
+        // 未选中,添加选中
+        this.newProduct.sceneNames.push(scene);
+      }
     }
   }
   
@@ -1686,9 +1712,10 @@ export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
       throw new Error('找不到要编辑的产品');
     }
 
+    // 编辑模式:从sceneNames数组取第一个(编辑时只能选一个)
     const productName = this.newProduct.isCustom
       ? this.newProduct.productName
-      : this.newProduct.sceneName;
+      : (this.newProduct.sceneNames.length > 0 ? this.newProduct.sceneNames[0] : this.newProduct.sceneName);
 
     // 更新产品名称和类型
     product.set('productName', productName);

+ 2 - 4
src/modules/project/components/team-assign/team-assign.component.html

@@ -41,10 +41,8 @@
                 </div>
               </div>
               @if (canEdit) {
-                <button class="btn-icon" (click)="removeMember(team)" title="移除">
-                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="icon">
-                    <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm52.697 283.697L256 279l-52.697 52.697-22.626-22.626L233.373 256l-52.696-52.697 22.626-22.626L256 233.373l52.697-52.696 22.626 22.626L278.627 256l52.696 52.697-22.626 22.626z"/>
-                  </svg>
+                <button class="btn-icon btn-emoji" (click)="removeMember(team)" title="移除设计师">
+                  ❌
                 </button>
               }
             </div>

+ 17 - 0
src/modules/project/components/team-assign/team-assign.component.scss

@@ -92,6 +92,23 @@
   &:hover {
     background: #fecaca;
   }
+  
+  // 🔥 emoji按钮样式
+  &.btn-emoji {
+    font-size: 18px;
+    line-height: 1;
+    background: #fee2e2;
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    
+    &:hover {
+      background: #fecaca;
+      transform: scale(1.1);
+    }
+    
+    &:active {
+      transform: scale(0.95);
+    }
+  }
 }
 
 .designer-card {

+ 77 - 52
src/modules/project/pages/project-detail/stages/components/stage-delivery-execution/stage-delivery-execution.component.ts

@@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, Change
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { NovaFile } from 'fmode-ng/core';
+import { NovaFile, NovaStorage } from 'fmode-ng/core';
 import { DragUploadModalComponent, UploadResult } from '../../../../../components/drag-upload-modal/drag-upload-modal.component';
 import { RevisionTaskModalComponent } from '../../../../../components/revision-task-modal/revision-task-modal.component';
 import { RevisionTaskListComponent } from '../../../../../components/revision-task-list/revision-task-list.component';
@@ -89,6 +89,9 @@ export class StageDeliveryExecutionComponent implements OnChanges {
   } = {};
   @Input() revisionTaskCount: number = 0;
   @Input() cid: string = '';
+  
+  // 🔥 NovaStorage 实例
+  storage: any = null;
 
   @Output() refreshData = new EventEmitter<void>();
   @Output() fileUploaded = new EventEmitter<{ productId: string, deliveryType: string, fileCount: number }>();
@@ -393,44 +396,50 @@ export class StageDeliveryExecutionComponent implements OnChanges {
       const targetProjectId = this.project?.id;
       if (!targetProjectId) return;
 
-      const uploadPromises = result.files.map(item => {
-        // UploadFile interface wrapper contains the actual File object in item.file.file
+      // 🔥 初始化 NovaStorage
+      if (!this.storage) {
+        console.log('📦 初始化 NovaStorage...');
+        this.storage = await NovaStorage.withCid(this.cid);
+        console.log('✅ NovaStorage 已初始化');
+      }
+
+      // 🔥 最简单的上传方法
+      const uploadPromises = result.files.map(async (item) => {
         const file = item.file.file;
         
-        return this.projectFileService.uploadProjectFileWithRecord(
-          file,
-          targetProjectId,
-          `delivery_${item.stageType}`,
-          item.spaceId,
-          'delivery',
-          {
-            deliveryType: item.stageType,
-            productId: item.spaceId,
-            spaceId: item.spaceId,
-            uploadedFor: 'delivery_execution',
-            approvalStatus: 'unverified',
-            uploadedByName: this.currentUser?.get('name') || '',
-            uploadedById: this.currentUser?.id || '',
-            uploadStage: 'delivery',
-            // Add AI analysis results if available
-            analysisResult: item.analysisResult,
-            submittedAt: item.submittedAt,
-            submittedBy: item.submittedBy,
-            submittedByName: item.submittedByName,
-            deliveryListId: item.deliveryListId
-          }
-        ).then(() => {
-           // Emit single file uploaded event for each file to update UI/Notifications
-           this.fileUploaded.emit({ 
-             productId: item.spaceId, 
-             deliveryType: item.stageType, 
-             fileCount: 1 
-           });
-        });
+        try {
+          console.log(`📤 开始上传: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
+          
+          // 🔥 直接上传
+          const uploaded = await this.storage.upload(file, {
+            onProgress: (p: any) => {
+              const progress = Number(p?.total?.percent || 0);
+              console.log(`📊 ${file.name} 上传进度: ${progress.toFixed(2)}%`);
+            },
+          });
+          
+          console.log(`✅ 上传完成: ${file.name}`);
+          console.log(`🔗 URL: ${uploaded.url}`);
+          console.log(`🔑 Key: ${uploaded.key}`);
+          
+          // Emit file uploaded event
+          this.fileUploaded.emit({ 
+            productId: item.spaceId, 
+            deliveryType: item.stageType, 
+            fileCount: 1 
+          });
+          
+        } catch (fileError: any) {
+          console.error(`❌ 文件 ${file.name} 上传失败:`, fileError);
+          const errorMsg = fileError?.message || fileError?.toString() || '未知错误';
+          throw new Error(`文件 ${file.name} 上传失败: ${errorMsg}`);
+        }
       });
 
       await Promise.all(uploadPromises);
       
+      window?.fmode?.alert('文件上传成功');
+      
       // Refresh data
       this.refreshData.emit();
 
@@ -465,32 +474,48 @@ export class StageDeliveryExecutionComponent implements OnChanges {
       const targetProjectId = this.project?.id;
       if (!targetProjectId) return;
 
+      // 🔥 初始化 NovaStorage
+      if (!this.storage) {
+        console.log('📦 初始化 NovaStorage...');
+        this.storage = await NovaStorage.withCid(this.cid);
+        console.log('✅ NovaStorage 已初始化');
+      }
+
+      // 🔥 最简单的上传方法
       const uploadPromises = [];
       for (let i = 0; i < files.length; i++) {
         const file = files[i];
-        uploadPromises.push(
-          this.projectFileService.uploadProjectFileWithRecord(
-            file,
-            targetProjectId,
-            `delivery_${deliveryType}`,
-            productId,
-            'delivery',
-            {
-              deliveryType: deliveryType,
-              productId: productId,
-              spaceId: productId,
-              uploadedFor: 'delivery_execution',
-              approvalStatus: 'unverified',
-              uploadedByName: this.currentUser?.get('name') || '',
-              uploadedById: this.currentUser?.id || '',
-              uploadStage: 'delivery'
-            }
-          )
-        );
+        
+        const uploadPromise = (async () => {
+          try {
+            console.log(`📤 开始上传: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
+            
+            // 🔥 直接上传
+            const uploaded = await this.storage.upload(file, {
+              onProgress: (p: any) => {
+                const progress = Number(p?.total?.percent || 0);
+                console.log(`📊 ${file.name} 上传进度: ${progress.toFixed(2)}%`);
+              },
+            });
+            
+            console.log(`✅ 上传完成: ${file.name}`);
+            console.log(`🔗 URL: ${uploaded.url}`);
+            console.log(`🔑 Key: ${uploaded.key}`);
+            
+          } catch (fileError: any) {
+            console.error(`❌ 文件 ${file.name} 上传失败:`, fileError);
+            const errorMsg = fileError?.message || fileError?.toString() || '未知错误';
+            throw new Error(`文件 ${file.name} 上传失败: ${errorMsg}`);
+          }
+        })();
+        
+        uploadPromises.push(uploadPromise);
       }
 
       await Promise.all(uploadPromises);
       
+      window?.fmode?.alert('文件上传成功');
+      
       if (!silentMode) {
         // Use a simple alert or toast if available. Assuming window.fmode.alert exists from original code
         // window.fmode?.alert('文件上传成功');

+ 13 - 23
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -131,34 +131,24 @@
             </select>
           </div>
 
-          <!-- 交付期限 -->
+          <!-- 小图日期 -->
           <div class="form-group">
             <label class="form-label">小图日期 <span class="required">*</span></label>
-            <mat-form-field appearance="outline" class="date-picker-field">
-              <input
-                matInput
-                [matDatepicker]="demodayPicker"
-                [(ngModel)]="projectInfo.demoday"
-                [disabled]="!canEdit"
-                placeholder="选择小图日期">
-              <mat-datepicker-toggle matIconSuffix [for]="demodayPicker"></mat-datepicker-toggle>
-              <mat-datepicker #demodayPicker></mat-datepicker>
-            </mat-form-field>
+            <app-custom-date-picker
+              [(selectedDate)]="projectInfo.demoday"
+              [disabled]="!canEdit"
+              placeholder="选择小图日期">
+            </app-custom-date-picker>
           </div>
 
-          <!-- 交付期 -->
+          <!-- 交付日期 -->
           <div class="form-group">
-            <label class="form-label">交付日期 </label>
-            <mat-form-field appearance="outline" class="date-picker-field">
-              <input
-                matInput
-                [matDatepicker]="deadlinePicker"
-                [(ngModel)]="projectInfo.deadline"
-                [disabled]="!canEdit"
-                placeholder="选择交付日期">
-              <mat-datepicker-toggle matIconSuffix [for]="deadlinePicker"></mat-datepicker-toggle>
-              <mat-datepicker #deadlinePicker></mat-datepicker>
-            </mat-form-field>
+            <label class="form-label">交付日期</label>
+            <app-custom-date-picker
+              [(selectedDate)]="projectInfo.deadline"
+              [disabled]="!canEdit"
+              placeholder="选择交付日期">
+            </app-custom-date-picker>
           </div>
 
           <!-- 项目描述 -->

+ 2 - 75
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -460,81 +460,8 @@
   }
 }
 
-// ============ Material DatePicker 样式 ============
-.date-picker-field {
-  width: 100%;
-
-  ::ng-deep {
-    .mat-mdc-form-field-infix {
-      padding-top: 8px;
-      padding-bottom: 8px;
-    }
-
-    .mat-mdc-text-field-wrapper {
-      padding-left: 0;
-      padding-right: 0;
-    }
-
-    .mat-mdc-form-field-subscript-wrapper {
-      display: none; // 隐藏底部的提示文本区域
-    }
-
-    .mat-mdc-form-field-focus-overlay {
-      background-color: transparent;
-    }
-
-    .mat-datepicker-toggle {
-      color: var(--ion-color-primary, #3880ff);
-    }
-
-    input.mat-mdc-input-element {
-      padding: 10px 14px;
-      font-size: 14px;
-      color: #111827;
-
-      &::placeholder {
-        color: #9ca3af;
-      }
-
-      &:disabled {
-        color: #6b7280;
-        cursor: not-allowed;
-      }
-    }
-
-    .mat-mdc-form-field-outline {
-      border-radius: 8px;
-    }
-
-    .mdc-notched-outline__leading,
-    .mdc-notched-outline__notch,
-    .mdc-notched-outline__trailing {
-      border-color: #e5e7eb;
-      border-width: 1.5px;
-    }
-
-    .mdc-notched-outline:hover .mdc-notched-outline__leading,
-    .mdc-notched-outline:hover .mdc-notched-outline__notch,
-    .mdc-notched-outline:hover .mdc-notched-outline__trailing {
-      border-color: var(--ion-color-primary, #3880ff);
-    }
-
-    .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
-    .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
-    .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
-      border-color: var(--ion-color-primary, #3880ff);
-    }
-
-    .mat-mdc-form-field.mat-form-field-disabled {
-      .mdc-notched-outline__leading,
-      .mdc-notched-outline__notch,
-      .mdc-notched-outline__trailing {
-        border-color: #d1d5db;
-        background-color: #f3f4f6;
-      }
-    }
-  }
-}
+// ============ 自定义DatePicker已替换Material DatePicker ============
+// 不再需要Material DatePicker样式,已全部移除
 
 // 文件上传相关样式
 .files-card {

+ 3 - 9
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -3,10 +3,6 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse, WxworkAuth, WxworkSDK, WxworkCorp } from 'fmode-ng/core';
-import { MatDatepickerModule } from '@angular/material/datepicker';
-import { MatInputModule } from '@angular/material/input';
-import { MatNativeDateModule } from '@angular/material/core';
-import { MatFormFieldModule } from '@angular/material/form-field';
 
 import { ProjectFileService } from '../../../services/project-file.service';
 import { ProductSpaceService, Project } from '../../../services/product-space.service';
@@ -22,6 +18,7 @@ import {
 } from '../../../config/quotation-rules';
 import { QuotationEditorComponent } from '../../../components/quotation-editor.component';
 import { TeamAssignComponent } from '../../../components/team-assign/team-assign.component';
+import { CustomDatePickerComponent } from '../../../components/custom-date-picker/custom-date-picker.component';
 
 const Parse = FmodeParse.with('nova');
 
@@ -42,12 +39,9 @@ const Parse = FmodeParse.with('nova');
   imports: [
     CommonModule,
     FormsModule,
-    MatDatepickerModule,
-    MatInputModule,
-    MatNativeDateModule,
-    MatFormFieldModule,
     QuotationEditorComponent,
-    TeamAssignComponent
+    TeamAssignComponent,
+    CustomDatePickerComponent
   ],
   templateUrl: './stage-order.component.html',
   styleUrls: ['./stage-order.component.scss'],