Explorar o código

feat: improve delivery messages and fix mobile modal UX

- Updated message templates to use more polite, customer-friendly language across all project stages
- Fixed mobile modal scroll issues by preventing body scroll and making modal full-screen on small devices
- Changed project status badge styling (stalled period now uses red color scheme for better visibility)
- Moved delivery message modal footer buttons inside scrollable area to ensure accessibility on all screen sizes
徐福静0235668 hai 1 día
pai
achega
a61c1ec478

+ 18 - 12
src/app/pages/services/delivery-message.service.ts

@@ -29,24 +29,30 @@ export interface DeliveryMessage {
  */
 export const MESSAGE_TEMPLATES = {
   white_model: [
-    '老师我这里硬装模型做好了,看下是否有问题,如果没有,我去做渲染',
-    '老师,白模阶段完成,请查看确认',
-    '硬装结构已完成,请审阅'
+    '您好,硬装模型已完成,麻烦您看下是否有需要调整的地方',
+    '给您发一下白模阶段的设计,请您查看确认',
+    '硬装结构已完成,请审阅'
   ],
   soft_decor: [
-    '软装好了,准备渲染,有问题可以留言',
-    '老师,软装设计已完成,请查看',
-    '家具配置完成,准备进入渲染阶段'
+    '软装设计已完成,准备进入渲染阶段,有问题随时和我说',
+    '给您发一下软装方案,请您查看',
+    '家具配置已完成,请您确认后我们开始渲染'
   ],
   rendering: [
-    '老师,渲染图已完成,请查看效果',
-    '效果图已出,请审阅',
-    '渲染完成,请查看是否需要调整'
+    '渲染图已出,请您查看效果',
+    '效果图已完成,麻烦您看一下',
+    '渲染完成,请查看是否需要调整'
   ],
   post_process: [
-    '老师,后期处理完成,请验收',
-    '最终成品已完成,请查看',
-    '所有修图完成,请确认'
+    '后期处理已完成,请您验收',
+    '最终成品已完成,请您查看',
+    '所有修图已完成,请您确认'
+  ],
+  delivery_list: [
+    '该空间的所有交付物已完成,请您查看',
+    '这个空间的全部设计已完成(白模、软装、渲染、后期),请您验收',
+    '给您发一下该空间的所有图纸,请您查看确认',
+    '该空间交付完成,如有需要修改的地方请随时和我说'
   ]
 };
 

+ 16 - 12
src/modules/project/components/drag-upload-modal/drag-upload-modal.component.scss

@@ -9,21 +9,22 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 10000;
+  z-index: 99999; // 🔥 增加z-index确保完全覆盖所有内容(包括底部栏)
   padding: 20px;
   backdrop-filter: blur(4px);
+  overflow-y: auto; // 🔥 允许overlay内部滚动,而非body滚动
+  overscroll-behavior: contain; // 🔥 防止滚动传播到body
   
   // 移动端适配
   @media (max-width: 768px) {
-    padding: 10px;
-    align-items: flex-start;
-    padding-top: 10px; // 🔥 减小顶部间距
+    padding: 0; // 🔥 小屏幕下移除padding,让弹窗占满屏幕
+    align-items: stretch; // 🔥 伸展填充整个高度
+    justify-content: stretch;
   }
   
   // 🔥 企业微信端专用优化
   @media (max-width: 480px) {
-    padding: 5px;
-    padding-top: 10px;
+    padding: 0;
   }
 }
 
@@ -41,17 +42,20 @@
   
   // 移动端适配
   @media (max-width: 768px) {
-    width: 95%;
+    width: 100%; // 🔥 占满宽度
     max-width: 100%;
-    max-height: 90vh;
-    border-radius: 8px;
+    min-height: 100vh; // 🔥 最小高度为全屏,防止露出底部内容
+    max-height: 100vh; // 🔥 最大高度为全屏
+    border-radius: 0; // 🔥 移除圆角,占满屏幕
+    margin: 0;
   }
   
   // 🔥 企业微信端专用优化
   @media (max-width: 480px) {
-    width: 98%;
-    max-height: 92vh;
-    border-radius: 6px;
+    width: 100%;
+    min-height: 100vh;
+    max-height: 100vh;
+    border-radius: 0;
   }
 }
 

+ 26 - 0
src/modules/project/components/drag-upload-modal/drag-upload-modal.component.ts

@@ -127,6 +127,25 @@ export class DragUploadModalComponent implements OnInit, AfterViewInit, OnChange
       });
     }
     
+    // 🔥 弹窗显示/隐藏时,控制body滚动
+    if (changes['visible']) {
+      if (this.visible) {
+        // 弹窗打开时,禁止body滚动
+        document.body.style.overflow = 'hidden';
+        document.body.style.position = 'fixed'; // 🔥 固定body,彻底防止滚动
+        document.body.style.width = '100%'; // 🔥 保持宽度,防止布局抖动
+        document.body.style.top = '0'; // 🔥 固定在顶部
+        console.log('🔒 已禁止body滚动(固定模式)');
+      } else {
+        // 弹窗关闭时,恢复body滚动
+        document.body.style.overflow = '';
+        document.body.style.position = '';
+        document.body.style.width = '';
+        document.body.style.top = '';
+        console.log('🔓 已恢复body滚动');
+      }
+    }
+    
     // 当弹窗显示或文件发生变化时处理
     if (changes['visible'] && this.visible && this.droppedFiles.length > 0) {
       console.log('📎 弹窗显示,开始处理文件');
@@ -1314,5 +1333,12 @@ export class DragUploadModalComponent implements OnInit, AfterViewInit, OnChange
   ngOnDestroy(): void {
     console.log('🧹 组件销毁,清理ObjectURL资源...');
     this.cleanupObjectURLs();
+    
+    // 🔥 确保恢复body滚动(防止弹窗异常关闭时body仍被锁定)
+    document.body.style.overflow = '';
+    document.body.style.position = '';
+    document.body.style.width = '';
+    document.body.style.top = '';
+    console.log('🔓 组件销毁时已恢复body滚动');
   }
 }

+ 2 - 5
src/modules/project/components/project-bottom-card/project-bottom-card.component.scss

@@ -1,13 +1,10 @@
 .project-bottom-card {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
+  position: relative;
   background: rgba(255, 255, 255, 0.95);
   backdrop-filter: blur(10px);
   border-top: 1px solid #e5e7eb;
   padding: 12px 16px;
-  z-index: 1000;
+  margin-top: 24px;
   box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
 
   .card-skeleton {

+ 14 - 12
src/modules/project/pages/project-detail/project-detail.component.scss

@@ -1425,14 +1425,21 @@
   }
 }
 
-// 🆕 停滞期和改图期状态标记(右上角显示)
+// 🆕 停滞期和改图期状态标记
+// 🔥 默认隐藏所有独立的状态标记(不在 .stage-toolbar 内的)
 .project-status-badges {
-  display: flex;
+  display: none !important;
+}
+
+// 🔥 只显示顶部工具栏内的状态标记(在导航栏右侧)
+.stage-toolbar .project-status-badges {
+  display: flex !important;
   flex-direction: column;
   gap: 8px;
+  max-width: 300px;
   padding-right: 16px;
   flex-shrink: 0;
-  max-width: 300px;
+  position: relative;
 
   .status-badge {
     background: white;
@@ -1547,14 +1554,14 @@
 
     // 停滞期样式
     &.stalled {
-      border-left: 4px solid #8b5cf6;
+      border-left: 4px solid #ef4444; // 🔥 红色边框
 
       .badge-icon {
-        background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
+        background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); // 🔥 红色渐变背景
       }
 
       .badge-title {
-        color: #7c3aed;
+        color: #dc2626; // 🔥 红色标题
       }
     }
 
@@ -1592,12 +1599,7 @@
     gap: 8px;
   }
   
-  // 🔥 小屏幕下:先隐藏所有状态标记
-  .project-status-badges {
-    display: none !important;
-  }
-  
-  // 🔥 然后只显示顶部的(在 .stage-toolbar 内的)
+  // 🔥 小屏幕下:调整顶部状态标记的样式
   .stage-toolbar .project-status-badges {
     display: flex !important;
     padding-right: 8px;

+ 23 - 22
src/modules/project/pages/project-detail/project-detail.component.ts

@@ -415,16 +415,18 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
       this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
 
       const companyId = this.currentUser?.get('company')?.id || localStorage?.getItem("company");
-          // 3. 加载项目
-      if (!this.project) {
-        if (this.projectId) {
-          // 通过 projectId 加载(从后台进入)
-          const query = new Parse.Query('Project');
-          query.include('contact', 'assignee','department','department.leader');
-          this.project = await query.get(this.projectId);
-        } else if (this.chatId) {
-          // 通过 chat_id 查找项目(从企微群聊进入)
-          if (companyId) {
+      
+      // 3. 加载项目
+      // 🔥 关键修改:每次都重新加载项目数据,确保获取最新的停滞期/改图期状态
+      if (this.projectId) {
+        // 通过 projectId 加载(从后台进入)
+        const query = new Parse.Query('Project');
+        query.include('contact', 'assignee','department','department.leader');
+        this.project = await query.get(this.projectId);
+        console.log('🔄 [项目数据] 已从服务器重新加载,停滞期状态:', this.project?.get('data')?.isStalled, '改图期状态:', this.project?.get('data')?.isModification);
+      } else if (this.chatId) {
+        // 通过 chat_id 查找项目(从企微群聊进入)
+        if (companyId) {
             // 先查找 GroupChat
             const gcQuery = new Parse.Query('GroupChat');
             gcQuery.equalTo('chat_id', this.chatId);
@@ -432,23 +434,22 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
             let groupChat = await gcQuery.first();
 
 
-            if (groupChat) {
-              this.groupChat = groupChat;
-              const projectPointer = groupChat.get('project');
+          if (groupChat) {
+            this.groupChat = groupChat;
+            const projectPointer = groupChat.get('project');
 
-              if (projectPointer) {
-                const pQuery = new Parse.Query('Project');
-                pQuery.include('contact', 'assignee','department','department.leader');
-                this.project = await pQuery.get(projectPointer.id);
-              }
+            if (projectPointer) {
+              const pQuery = new Parse.Query('Project');
+              pQuery.include('contact', 'assignee','department','department.leader');
+              // 🔥 强制从服务器获取最新数据(确保停滞期/改图期状态实时更新)
+              this.project = await pQuery.get(projectPointer.id);
             }
+          }
 
-            if (!this.project) {
-              throw new Error('该群聊尚未关联项目,请先在后台创建项目');
-            }
+          if (!this.project) {
+            throw new Error('该群聊尚未关联项目,请先在后台创建项目');
           }
         }
-
       }
 
      

+ 16 - 16
src/modules/project/pages/project-detail/stages/components/delivery-message-modal/delivery-message-modal.component.html

@@ -94,22 +94,22 @@
         </svg>
         <span>消息将发送到企业微信当前群聊窗口</span>
       </div>
-    </div>
-    
-    <!-- 底部按钮 -->
-    <div class="modal-footer">
-      <button class="btn-cancel" (click)="closeModal()">
-        取消
-      </button>
-      <button 
-        class="btn-send" 
-        (click)="sendMessage()" 
-        [disabled]="sendingMessage">
-        <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
-          <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
-        </svg>
-        <span>{{ sendingMessage ? '发送中...' : '发送' }}</span>
-      </button>
+      
+      <!-- 底部按钮 - 移到可滚动区域内 -->
+      <div class="modal-footer-inline">
+        <button class="btn-cancel" (click)="closeModal()">
+          取消
+        </button>
+        <button 
+          class="btn-send" 
+          (click)="sendMessage()" 
+          [disabled]="sendingMessage">
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+            <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
+          </svg>
+          <span>{{ sendingMessage ? '发送中...' : '发送' }}</span>
+        </button>
+      </div>
     </div>
   </div>
 </div>

+ 63 - 1
src/modules/project/pages/project-detail/stages/components/delivery-message-modal/delivery-message-modal.component.scss

@@ -104,6 +104,7 @@
   overflow-y: auto;
   overflow-x: hidden;
   padding: 10px;
+  padding-bottom: 10px;  // 🔥 确保底部有足够空间
   background: #f7f8fa;
   
   // 滚动条优化
@@ -349,7 +350,7 @@
   }
 }
 
-// 底部按钮区域
+// 底部按钮区域(已废弃,保留以防其他地方引用)
 .modal-footer {
   padding: 10px;
   display: flex;
@@ -410,6 +411,67 @@
   }
 }
 
+// 内联底部按钮区域(在可滚动区域内)
+.modal-footer-inline {
+  padding: 10px;
+  display: flex;
+  gap: 10px;
+  background: white;
+  border-radius: 8px;
+  margin-top: 10px;  // 🔥 与上方内容保持间距
+  
+  button {
+    flex: 1;
+    height: 40px;
+    border: none;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    
+    &:active {
+      transform: scale(0.98);
+    }
+    
+    &:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+  }
+  
+  .btn-cancel {
+    background: white;
+    border: 1px solid #d9d9d9;
+    color: #000000;
+    
+    &:hover:not(:disabled) {
+      background: #f5f5f5;
+      border-color: #bfbfbf;
+    }
+  }
+  
+  .btn-send {
+    flex: 2;  // 🔥 发送按钮更宽
+    background: linear-gradient(135deg, #07c160 0%, #06ae56 100%);
+    color: white;
+    box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2);
+    
+    &:hover:not(:disabled) {
+      background: linear-gradient(135deg, #06ae56 0%, #059048 100%);
+      box-shadow: 0 4px 8px rgba(7, 193, 96, 0.3);
+    }
+    
+    svg {
+      flex-shrink: 0;
+    }
+  }
+}
+
 // 响应式优化
 @media (max-width: 480px) {
   .message-modal-box {

+ 57 - 45
src/modules/project/services/image-analysis.service.ts

@@ -818,37 +818,46 @@ export class ImageAnalysisService {
   ): Promise<ImageAnalysisResult> {
     const startTime = Date.now();
     
-    const prompt = `你是室内设计图分类专家,请快速分析这张图片的内容和质量,只输出JSON。
+    const prompt = `你是室内设计图分类专家,快速分析图片并只输出JSON。
 
-JSON格式:
+JSON格式(必须严格遵守):
 {
-  "category": "white_model或soft_decor或rendering或post_process",
-  "confidence": 90,
-  "spaceType": "客厅或卧室等",
-  "description": "简短描述",
-  "hasColor": true,
-  "hasTexture": true,
-  "hasLighting": true,
-  "qualityScore": 85,
-  "qualityLevel": "high",
-  "sharpness": 80,
-  "textureQuality": 85
+  "space": "客厅或卧室或厨房或卫生间或餐厅或书房或阳台等",
+  "stage": "white_model或soft_decor或rendering或post_process"
 }
 
-快速判断规则(严格执行):
+阶段判断规则(严格执行,从后向前判断):
+
+**第一步:判断是否为post_process(照片级后期)**
+✅ post_process的特征(满足3项以上即为后期):
+  - 照片级真实感,无法分辨是否为渲染图
+  - 光影自然柔和,无明显CG痕迹
+  - 细节丰富真实(布料褶皱、木纹纹理、金属反射)
+  - 色彩自然,无过度饱和或过度对比
+  - 景深效果自然(前景清晰,背景自然虚化)
+  - 材质表现真实(大理石纹理、玻璃透明度、金属质感)
+  - 整体画面像单反相机拍摄的照片
 
-- white_model: 统一灰白色/浅色,无材质纹理细节(可有家具和灯光)
+**第二步:排除post_process后,判断是否为rendering(CG渲染)**
+❌ rendering的特征(明显CG感):
+  - 明显的计算机渲染痕迹(过于完美、过于规整)
+  - 光影对比过强或过于均匀(V-Ray/3dsMax典型特征)
+  - 颜色过于饱和或过于艳丽
+  - 材质反射过于理想化(过于光滑、过于规则)
+  - 阴影边缘过于清晰或模糊不自然
 
-- soft_decor: 有真实材质纹理(木纹/布纹),有装饰色彩,但CG感不强
-  ⚠️ 关键:软装可以有灯光!重点是材质真实但CG渲染感不强
+**第三步:判断soft_decor和white_model**
+- soft_decor: 有材质纹理和色彩,但CG感不强(偏手绘或简单渲染)
+- white_model: 灰白色调,无材质细节
 
-- rendering: 有材质纹理,有装饰色彩,CG计算机渲染感明显(V-Ray/3dsMax)
-  ⚠️ 区分:rendering = CG感明显(能看出是3D渲染),质量70-89分
+**判断原则**:
+⚠️ 如果无法确定是rendering还是post_process,优先选择post_process
+⚠️ 现代高质量渲染图(超写实风格)应归类为post_process
+⚠️ 只有明显看出CG痕迹的才是rendering
 
-- post_process: 照片级真实感(看起来像真实拍摄),质量≥90分
-  ⚠️ 区分:post_process = 照片级(不是普通CG渲染)`;
+只输出JSON,不要其他内容。`;
 
-    const output = `{"category":"rendering","confidence":92,"spaceType":"卧室","description":"现代卧室","hasColor":true,"hasTexture":true,"hasLighting":true,"qualityScore":85,"qualityLevel":"high","sharpness":80,"textureQuality":85}`;
+    const output = `{"space":"客厅","stage":"post_process"}`;
 
     try {
       console.log(`⏱️ [快速分析] 开始AI调用,图片Base64大小: ${(imageUrl.length / 1024 / 1024).toFixed(2)} MB`);
@@ -863,12 +872,12 @@ JSON格式:
           model: this.MODEL,
           vision: true,
           images: [imageUrl],
-          max_tokens: 800 // 确保返回完整结果(避免截断)
+          max_tokens: 200 // 🔥 简化格式,只需200 tokens即可
         }
       );
       
       const timeoutPromise = new Promise((_, reject) => {
-        setTimeout(() => reject(new Error('AI分析超时(60秒)')), 60000); // 🔥 增加到60秒,防止大图超时
+        setTimeout(() => reject(new Error('AI分析超时(120秒)')), 120000); // 🔥 增加到120秒,处理大图片
       });
       
       const result = await Promise.race([aiPromise, timeoutPromise]) as any;
@@ -879,42 +888,45 @@ JSON格式:
       const analysisTime = Date.now() - startTime;
       console.log(`✅ [快速分析] AI调用完成,耗时: ${(analysisTime / 1000).toFixed(2)}秒`);
       console.log(`📊 [快速分析] AI返回结果:`, {
-        阶段分类: result.category,
-        置信度: `${result.confidence}%`,
-        空间类型: result.spaceType,
-        有颜色: result.hasColor,
-        有纹理: result.hasTexture,
-        有灯光: result.hasLighting,
-        质量分数: result.qualityScore
+        空间: result.space,
+        阶段: result.stage
       });
       
+      // 🔥 根据阶段类型设置默认质量分数
+      const defaultQualityScore = {
+        'white_model': 60,
+        'soft_decor': 75,
+        'rendering': 85,
+        'post_process': 92
+      }[result.stage] || 75;
+      
       return {
         fileName: '',
         fileSize: 0,
         dimensions: basicInfo.dimensions,
         quality: {
-          score: result.qualityScore || 75,
-          level: result.qualityLevel || 'medium',
-          sharpness: result.sharpness || 75,
+          score: defaultQualityScore,
+          level: defaultQualityScore >= 85 ? 'high' : defaultQualityScore >= 70 ? 'medium' : 'low',
+          sharpness: defaultQualityScore,
           brightness: 70,
           contrast: 75,
           detailLevel: 'basic',
           pixelDensity: megapixels >= 2 ? 'high' : 'medium',
-          textureQuality: result.textureQuality || 75,
+          textureQuality: defaultQualityScore,
           colorDepth: 75
         },
         content: {
-          category: result.category || 'rendering',
-          confidence: result.confidence || 80,
-          spaceType: result.spaceType || '未识别',
-          description: result.description || '室内设计图',
+          category: result.stage || 'rendering', // 🔥 使用stage作为category
+          confidence: 90, // 🔥 默认置信度90%
+          spaceType: result.space || '未识别', // 🔥 使用space作为spaceType
+          description: `${result.space || ''}室内设计图`,
           tags: [],
           isArchitectural: true,
           hasInterior: true,
-          hasFurniture: true,
-          hasLighting: result.hasLighting !== false,
-          hasColor: result.hasColor !== false,
-          hasTexture: result.hasTexture !== false
+          hasFurniture: result.stage !== 'white_model',
+          hasLighting: result.stage === 'rendering' || result.stage === 'post_process',
+          hasColor: result.stage !== 'white_model',
+          hasTexture: result.stage !== 'white_model'
         },
         technical: {
           format: 'image/jpeg',
@@ -923,8 +935,8 @@ JSON格式:
           aspectRatio: this.calculateAspectRatio(basicInfo.dimensions.width, basicInfo.dimensions.height),
           megapixels: megapixels
         },
-        suggestedStage: result.category || 'rendering',
-        suggestedReason: `快速分析:${result.category},置信度${result.confidence}%`,
+        suggestedStage: result.stage || 'rendering', // 🔥 使用stage作为suggestedStage
+        suggestedReason: `快速分析:${result.space} - ${this.getStageName(result.stage)}`,
         analysisTime: analysisTime,
         analysisDate: new Date().toISOString()
       };