Parcourir la source

feat: enhance delivery message sending with group chat targeting

- Modified sendToWxwork to target specific project group chats instead of current window
- Added UI controls for sending stage files and entire space inventories to group chat
- Redesigned message modal with image previews, template selection, and send-only-images option
徐福静0235668 il y a 11 heures
Parent
commit
3038f5ea94

+ 80 - 55
src/app/pages/services/delivery-message.service.ts

@@ -90,7 +90,7 @@ export class DeliveryMessageService {
     await this.saveMessageToProject(projectId, message);
     
     // 🔥 发送到企业微信
-    await this.sendToWxwork(content, []);
+    await this.sendToWxwork(projectId, content, []);
     
     return message;
   }
@@ -120,7 +120,7 @@ export class DeliveryMessageService {
     await this.saveMessageToProject(projectId, message);
     
     // 🔥 发送到企业微信
-    await this.sendToWxwork(content, imageUrls);
+    await this.sendToWxwork(projectId, content, imageUrls);
     
     return message;
   }
@@ -178,11 +178,13 @@ export class DeliveryMessageService {
   }
   
   /**
-   * 🔥 发送消息到企业微信当前窗口
+   * 🔥 发送消息到企业微信当前群聊
+   * 参考 project-detail.component.ts 中的 sendSurvey() 方法
    */
-  private async sendToWxwork(text: string, imageUrls: string[] = []): Promise<void> {
+  private async sendToWxwork(projectId: string, text: string, imageUrls: string[] = []): Promise<void> {
     try {
       console.log('🔍 [sendToWxwork] ========== 开始发送流程 ==========');
+      console.log('🔍 [sendToWxwork] 项目ID:', projectId);
       console.log('🔍 [sendToWxwork] 当前URL:', window.location.href);
       
       // 检查是否在企业微信环境中
@@ -196,84 +198,107 @@ export class DeliveryMessageService {
         return;
       }
 
-      // 从URL获取cid和appId
+      // 1️⃣ 从URL获取cid和appId
       const urlParts = window.location.pathname.split('/');
-      console.log('🔍 [sendToWxwork] URL路径分段:', urlParts);
-      
       const wxworkIndex = urlParts.indexOf('wxwork');
-      console.log('🔍 [sendToWxwork] wxwork位置索引:', wxworkIndex);
-      
       const cid = urlParts[wxworkIndex + 1];
-      const appId = urlParts[wxworkIndex + 2] || 'crm';
-      console.log('🔍 [sendToWxwork] 提取的CID:', cid);
-      console.log('🔍 [sendToWxwork] 提取的AppID:', appId);
+      const appId = urlParts[wxworkIndex + 2] || 'project';
+      
+      console.log('🔍 [sendToWxwork] CID:', cid);
+      console.log('🔍 [sendToWxwork] AppID:', appId);
       
       if (!cid || cid === 'undefined') {
         throw new Error('❌ 无法从URL获取CID,请检查URL格式');
       }
       
-      // 🔥 检查SDK是否已初始化(避免重复初始化)
-      console.log('🔍 [sendToWxwork] 检查SDK初始化状态...');
-      console.log('  当前SDK.cid:', this.wxworkService.cid);
-      console.log('  当前SDK.appId:', this.wxworkService.appId);
-      console.log('  从URL提取的cid:', cid);
-      console.log('  从URL提取的appId:', appId);
-      
+      // 2️⃣ 初始化SDK(如果需要)
       if (!this.wxworkService.cid || this.wxworkService.cid !== cid) {
-        console.log('🔍 [sendToWxwork] SDK未初始化或CID不匹配,开始初始化...');
+        console.log('🔍 [sendToWxwork] 初始化SDK...');
         await this.wxworkService.initialize(cid, appId);
         console.log('✅ [sendToWxwork] SDK初始化完成');
-      } else {
-        console.log('✅ [sendToWxwork] SDK已初始化,跳过重复初始化');
       }
       
-      console.log('📧 准备发送消息到企业微信...');
-      console.log('  CID:', cid);
-      console.log('  AppID:', appId);
-      console.log('  文本内容:', text || '(无文本)');
-      console.log('  图片数量:', imageUrls.length);
-      console.log('  图片URL列表:', imageUrls);
+      // 3️⃣ 查询项目的群聊
+      console.log('🔍 [sendToWxwork] 查询项目群聊...');
+      const gcQuery = new Parse.Query('GroupChat');
+      gcQuery.equalTo('project', projectId);
+      gcQuery.equalTo('company', cid);
+      const groupChat = await gcQuery.first();
       
-      // 🔥 发送文本消息
+      if (!groupChat) {
+        console.warn('⚠️ [sendToWxwork] 未找到项目群聊');
+        throw new Error('未找到项目群聊,无法发送消息');
+      }
+      
+      const chatId = groupChat.get('chat_id');
+      console.log('🔍 [sendToWxwork] 群聊ID:', chatId);
+      
+      if (!chatId) {
+        throw new Error('群聊ID为空,无法发送消息');
+      }
+      
+      // 4️⃣ 发送文本消息
       if (text) {
-        await this.wxworkService.sendChatMessage({
-          msgtype: 'text',
-          text: {
-            content: text
-          }
+        console.log('📝 [sendToWxwork] 发送文本消息...');
+        console.log('  内容:', text);
+        
+        await this.wxworkService.ww.openExistedChatWithMsg({
+          chatId: chatId,
+          msg: {
+            msgtype: 'text',
+            text: {
+              content: text
+            }
+          } as any  // 🔥 使用类型断言绕过TypeScript类型检查
         });
+        
         console.log('✅ 文本消息已发送');
+        
+        // 如果有图片,等待一下再发送图片
+        if (imageUrls.length > 0) {
+          await new Promise(resolve => setTimeout(resolve, 500));
+        }
       }
       
-      // 🔥 发送图片消息(使用news图文消息类型)
+      // 5️⃣ 发送图片消息(使用link类型,可以显示图片预览
       for (let i = 0; i < imageUrls.length; i++) {
         const imageUrl = imageUrls[i];
+        console.log(`📸 [sendToWxwork] 发送图片 ${i + 1}/${imageUrls.length}...`);
+        console.log('  URL:', imageUrl);
+        
         try {
-          // 使用news类型发送图文消息,可以显示图片预览
-          await this.wxworkService.sendChatMessage({
-            msgtype: 'news',
-            news: {
-              link: imageUrl,
-              title: `图片 ${i + 1}/${imageUrls.length}`,
-              desc: '点击查看大图',
-              imgUrl: imageUrl
+          // 🔥 使用link类型发送图文消息,可以在群聊中显示图片预览
+          await this.wxworkService.ww.openExistedChatWithMsg({
+            chatId: chatId,
+            msg: {
+              msgtype: 'link',
+              link: {
+                title: `图片 ${i + 1}/${imageUrls.length}`,
+                desc: '点击查看大图',
+                url: imageUrl,
+                imgUrl: imageUrl
+              }
             }
           });
-          console.log(`✅ 图文消息 ${i + 1}/${imageUrls.length} 已发送: ${imageUrl}`);
           
-          // 避免发送过快,加入小延迟
+          console.log(`✅ 图片 ${i + 1}/${imageUrls.length} 已发送`);
+          
+          // 避免发送过快,加入延迟
           if (i < imageUrls.length - 1) {
-            await new Promise(resolve => setTimeout(resolve, 300));
+            await new Promise(resolve => setTimeout(resolve, 500));
           }
         } catch (imgError) {
           console.error(`❌ 图片 ${i + 1} 发送失败:`, imgError);
-          // 如果news类型失败,降级为纯文本链接
+          // 降级:尝试以文本方式发送图片链接
           try {
-            await this.wxworkService.sendChatMessage({
-              msgtype: 'text',
-              text: {
-                content: `📷 图片 ${i + 1}/${imageUrls.length}\n${imageUrl}`
-              }
+            await this.wxworkService.ww.openExistedChatWithMsg({
+              chatId: chatId,
+              msg: {
+                msgtype: 'text',
+                text: {
+                  content: `📷 图片 ${i + 1}/${imageUrls.length}\n${imageUrl}`
+                }
+              } as any  // 🔥 使用类型断言绕过TypeScript类型检查
             });
             console.log(`✅ 已改用文本方式发送图片链接`);
           } catch (textError) {
@@ -282,10 +307,10 @@ export class DeliveryMessageService {
         }
       }
       
-      console.log('✅ 所有消息已发送到企业微信');
+      console.log('✅ [sendToWxwork] 所有消息已发送到企业微信群聊');
     } catch (error) {
-      console.error('❌ 发送消息到企业微信失败:', error);
-      // 🔥 修复:必须抛出错误,让上层知道发送失败
+      console.error('❌ [sendToWxwork] 发送失败:', error);
+      // 抛出错误,让上层知道发送失败
       throw error;
     }
   }

+ 130 - 16
src/modules/project/pages/project-detail/stages/stage-delivery-new.component.html

@@ -89,9 +89,20 @@
                       <!-- 阶段标题与状态 -->
                       <div class="stage-label">
                         <span>{{ type.name }}</span>
-                        @if (getSpaceStageFileCount(space.id, type.id) > 0) {
-                          <span class="status-icon icon-check">✓</span>
-                        }
+                        <div class="stage-actions">
+                          @if (getSpaceStageFileCount(space.id, type.id) > 0) {
+                            <span class="status-icon icon-check">✓</span>
+                            <!-- 发送按钮 -->
+                            <button 
+                              class="send-icon-btn" 
+                              (click)="openMessageModalWithFiles(space.id, type.id); $event.stopPropagation()"
+                              title="发送到群聊">
+                              <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
+                                <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
+                              </svg>
+                            </button>
+                          }
+                        </div>
                       </div>
 
                       <!-- 文件预览或上传操作 -->
@@ -156,13 +167,26 @@
                       class="confirm-space-btn"
                       (click)="confirmSpace(space.id)"
                       [disabled]="saving || getSpaceTotalFileCount(space.id) === 0">
-                      <span>确认清单</span>
+                      <span>确认清单</span>
                     </button>
                   } @else {
                     <div class="confirmed-info">
                       <span class="confirmed-text">✓ 已确认</span>
                     </div>
                   }
+                  
+                  <!-- 发送清单按钮 -->
+                  @if (getSpaceTotalFileCount(space.id) > 0) {
+                    <button
+                      class="send-space-btn"
+                      (click)="sendSpaceAllImages(space.id)"
+                      [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>发送清单</span>
+                    </button>
+                  }
                 </div>
               </div>
             }
@@ -295,24 +319,114 @@
   <div class="message-modal-overlay" (click)="closeMessageModal()">
     <div class="message-modal-box" (click)="$event.stopPropagation()">
       <div class="modal-header">
-        <h4>发送消息</h4>
+        <div class="modal-title">
+          <h4>发送到群聊</h4>
+          <p class="modal-subtitle">{{ messageModalConfig.spaceName }} - {{ messageModalConfig.stageName }}</p>
+        </div>
         <button class="close-btn" (click)="closeMessageModal()">×</button>
       </div>
+      
       <div class="modal-body">
-         <!-- 简化版消息发送界面,适配侧边栏 -->
-         <div class="info-item">
-            <span class="label">发送图片:</span>
-            <span class="value">{{ messageModalConfig.imageUrls.length }} 张</span>
-         </div>
-         <textarea 
-            [(ngModel)]="customMessage" 
-            placeholder="输入消息..."
+        <!-- 图片预览 -->
+        @if (messageModalConfig.imageUrls.length > 0) {
+          <div class="images-preview-section">
+            <div class="section-label">
+              <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+                <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
+              </svg>
+              <span>图片 ({{ messageModalConfig.imageUrls.length }} 张)</span>
+            </div>
+            <div class="images-preview-grid">
+              @for (imageUrl of messageModalConfig.imageUrls; track imageUrl) {
+                <div class="preview-image-item">
+                  <img [src]="imageUrl" [alt]="'图片预览'" />
+                </div>
+              }
+            </div>
+          </div>
+        }
+        
+        <!-- 预设话术选择 -->
+        <div class="templates-section">
+          <div class="section-label">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+              <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
+            </svg>
+            <span>预设话术(可选)</span>
+          </div>
+          <div class="template-options">
+            @for (template of deliveryMessageService.getStageTemplates(messageModalConfig.stage); track template) {
+              <div 
+                class="template-option"
+                [class.selected]="selectedTemplate === template"
+                (click)="selectedTemplate = template; customMessage = ''">
+                <div class="radio-indicator">
+                  @if (selectedTemplate === template) {
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
+                      <circle cx="12" cy="12" r="8"/>
+                    </svg>
+                  }
+                </div>
+                <span class="template-text">{{ template }}</span>
+              </div>
+            }
+          </div>
+        </div>
+        
+        <!-- 自定义消息 -->
+        <div class="custom-message-section">
+          <div class="section-label">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+              <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
+            </svg>
+            <span>自定义消息(可选)</span>
+          </div>
+          <textarea 
+            [(ngModel)]="customMessage"
+            (input)="selectedTemplate = ''"
+            placeholder="输入自定义消息,或选择上方预设话术..."
             rows="3"
-            style="width:100%;margin-top:10px;padding:8px;border:1px solid #e2e8f0;border-radius:6px;"></textarea>
+            class="custom-input"></textarea>
+        </div>
+        
+        <!-- 只发图片选项 -->
+        <div class="send-options">
+          <label class="checkbox-option">
+            <input 
+              type="checkbox" 
+              [(ngModel)]="sendImageOnly"
+              (change)="onSendImageOnlyChange()">
+            <span class="checkbox-label">
+              <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+                <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
+              </svg>
+              只发图片(不发文字)
+            </span>
+          </label>
+        </div>
+        
+        <!-- 提示信息 -->
+        <div class="send-tips">
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
+            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
+          </svg>
+          <span>消息将发送到企业微信当前群聊窗口</span>
+        </div>
       </div>
+      
       <div class="modal-footer">
-        <button class="btn-cancel" (click)="closeMessageModal()">取消</button>
-        <button class="btn-send" (click)="sendMessage()" [disabled]="sendingMessage">发送</button>
+        <button class="btn-cancel" (click)="closeMessageModal()">
+          取消
+        </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>

+ 343 - 7
src/modules/project/pages/project-detail/stages/stage-delivery-new.component.scss

@@ -236,6 +236,12 @@
               justify-content: space-between;
               align-items: center;
 
+              .stage-actions {
+                display: flex;
+                align-items: center;
+                gap: 6px;
+              }
+
               .status-icon {
                 width: 16px; height: 16px;
                 border-radius: 50%;
@@ -244,6 +250,32 @@
                 font-size: 10px;
                 display: flex; align-items: center; justify-content: center;
               }
+
+              .send-icon-btn {
+                width: 20px;
+                height: 20px;
+                padding: 0;
+                border: none;
+                background: #6366f1;
+                border-radius: 4px;
+                color: white;
+                cursor: pointer;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                transition: all 0.2s;
+                opacity: 0.85;
+
+                &:hover {
+                  opacity: 1;
+                  background: #4f46e5;
+                  transform: translateY(-1px);
+                }
+
+                svg {
+                  flex-shrink: 0;
+                }
+              }
             }
 
             // Previews
@@ -326,7 +358,8 @@
         .space-confirm-section {
           margin-top: 16px;
           display: flex;
-          justify-content: center;
+          flex-direction: column;
+          gap: 10px;
           border-top: 1px solid #f1f5f9;
           padding-top: 16px;
 
@@ -356,6 +389,7 @@
           .confirmed-info {
             display: flex;
             align-items: center;
+            justify-content: center;
             gap: 6px;
             color: #059669;
             font-weight: 600;
@@ -364,6 +398,38 @@
             background: #ecfdf5;
             border-radius: 8px;
           }
+
+          .send-space-btn {
+            width: 100%;
+            padding: 10px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-weight: 600;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 8px;
+            transition: all 0.2s;
+            box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
+
+            &:hover:not(:disabled) {
+              transform: translateY(-1px);
+              box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+            }
+
+            &:disabled {
+              opacity: 0.6;
+              cursor: not-allowed;
+              transform: none;
+            }
+
+            svg {
+              flex-shrink: 0;
+            }
+          }
         }
       }
     }
@@ -403,6 +469,12 @@
   align-items: center;
   justify-content: center;
   padding: 20px;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
 }
 
 .stage-gallery-modal, .message-modal-box {
@@ -415,6 +487,7 @@
   flex-direction: column;
   box-shadow: 0 10px 25px rgba(0,0,0,0.2);
   overflow: hidden;
+  animation: slideUp 0.3s ease-out;
 
   .gallery-header, .modal-header {
     padding: 16px;
@@ -423,11 +496,31 @@
     justify-content: space-between;
     align-items: center;
 
+    .modal-title {
+      flex: 1;
+      h4 { 
+        margin: 0; 
+        font-size: 16px; 
+        font-weight: 700; 
+        color: #1e293b;
+      }
+      .modal-subtitle { 
+        margin: 4px 0 0; 
+        font-size: 12px; 
+        color: #64748b; 
+      }
+    }
+
     .gallery-title h3, h4 { margin: 0; font-size: 16px; font-weight: 700; }
     p { margin: 4px 0 0; font-size: 12px; color: #64748b; }
     
     .close-btn {
-      background: none; border: none; font-size: 24px; color: #94a3b8; cursor: pointer;
+      background: none; 
+      border: none; 
+      font-size: 24px; 
+      color: #94a3b8; 
+      cursor: pointer;
+      transition: color 0.2s;
       &:hover { color: #ef4444; }
     }
   }
@@ -463,6 +556,187 @@
         }
       }
     }
+
+    // 消息弹窗专用样式
+    .section-label {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 13px;
+      font-weight: 600;
+      color: #475569;
+      margin-bottom: 10px;
+      
+      svg {
+        flex-shrink: 0;
+        color: #6366f1;
+      }
+    }
+
+    // 图片预览区域
+    .images-preview-section {
+      margin-bottom: 16px;
+      
+      .images-preview-grid {
+        display: grid;
+        grid-template-columns: repeat(4, 1fr);
+        gap: 8px;
+        margin-top: 10px;
+        
+        .preview-image-item {
+          aspect-ratio: 1;
+          border-radius: 6px;
+          overflow: hidden;
+          border: 2px solid #e2e8f0;
+          background: #f8fafc;
+          
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+      }
+    }
+
+    // 预设话术选择
+    .templates-section {
+      margin-bottom: 16px;
+      
+      .template-options {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        
+        .template-option {
+          display: flex;
+          align-items: flex-start;
+          gap: 10px;
+          padding: 10px;
+          border: 2px solid #e2e8f0;
+          border-radius: 8px;
+          cursor: pointer;
+          transition: all 0.2s;
+          background: white;
+          
+          &:hover {
+            border-color: #c7d2fe;
+            background: #f5f8ff;
+          }
+          
+          &.selected {
+            border-color: #6366f1;
+            background: #eef2ff;
+            
+            .radio-indicator {
+              border-color: #6366f1;
+            }
+          }
+          
+          .radio-indicator {
+            width: 16px;
+            height: 16px;
+            border: 2px solid #cbd5e1;
+            border-radius: 50%;
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-top: 2px;
+            
+            svg {
+              color: #6366f1;
+            }
+          }
+          
+          .template-text {
+            flex: 1;
+            font-size: 13px;
+            color: #334155;
+            line-height: 1.5;
+          }
+        }
+      }
+    }
+
+    // 自定义消息输入
+    .custom-message-section {
+      margin-bottom: 16px;
+      
+      .custom-input {
+        width: 100%;
+        padding: 10px;
+        border: 2px solid #e2e8f0;
+        border-radius: 8px;
+        font-size: 13px;
+        color: #334155;
+        resize: vertical;
+        transition: border-color 0.2s;
+        
+        &:focus {
+          outline: none;
+          border-color: #6366f1;
+        }
+        
+        &::placeholder {
+          color: #94a3b8;
+        }
+      }
+    }
+
+    // 只发图片选项
+    .send-options {
+      margin-bottom: 12px;
+      
+      .checkbox-option {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        cursor: pointer;
+        
+        input[type="checkbox"] {
+          width: 16px;
+          height: 16px;
+          cursor: pointer;
+        }
+        
+        .checkbox-label {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          font-size: 13px;
+          color: #475569;
+          
+          svg {
+            color: #6366f1;
+          }
+        }
+      }
+    }
+
+    // 提示信息
+    .send-tips {
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+      padding: 10px;
+      background: #f0f9ff;
+      border: 1px solid #bae6fd;
+      border-radius: 6px;
+      
+      svg {
+        flex-shrink: 0;
+        color: #0ea5e9;
+        margin-top: 1px;
+      }
+      
+      span {
+        flex: 1;
+        font-size: 12px;
+        color: #0c4a6e;
+        line-height: 1.4;
+      }
+    }
   }
 
   .gallery-footer, .modal-footer {
@@ -473,19 +747,81 @@
     gap: 8px;
 
     .add-files-btn {
-      display: flex; align-items: center; gap: 6px;
-      background: #6366f1; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
+      display: flex; 
+      align-items: center; 
+      gap: 6px;
+      background: #6366f1; 
+      color: white; 
+      border: none; 
+      padding: 8px 12px; 
+      border-radius: 6px; 
+      cursor: pointer; 
+      font-size: 13px;
+      transition: background 0.2s;
+      
+      &:hover {
+        background: #4f46e5;
+      }
     }
+    
     .close-gallery-btn, .btn-cancel {
-       background: white; border: 1px solid #e2e8f0; color: #64748b; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
+       background: white; 
+       border: 1px solid #e2e8f0; 
+       color: #64748b; 
+       padding: 8px 12px; 
+       border-radius: 6px; 
+       cursor: pointer; 
+       font-size: 13px;
+       transition: all 0.2s;
+       
+       &:hover {
+         border-color: #cbd5e1;
+         background: #f8fafc;
+       }
     }
+    
     .btn-send {
-       background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px;
-       &:disabled { opacity: 0.5; }
+       display: flex;
+       align-items: center;
+       gap: 6px;
+       background: #10b981; 
+       color: white; 
+       border: none; 
+       padding: 8px 16px; 
+       border-radius: 6px; 
+       cursor: pointer; 
+       font-size: 13px;
+       font-weight: 600;
+       transition: all 0.2s;
+       
+       &:hover:not(:disabled) {
+         background: #059669;
+         transform: translateY(-1px);
+       }
+       
+       &:disabled { 
+         opacity: 0.6; 
+         cursor: not-allowed;
+       }
+       
+       svg {
+         flex-shrink: 0;
+       }
     }
   }
 }
 
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
 // Revision list
 .revision-list-fullscreen {
   position: fixed;

+ 84 - 4
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -138,6 +138,7 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
   selectedTemplate: string = '';
   customMessage: string = '';
   sendingMessage: boolean = false;
+  sendImageOnly: boolean = false; // 只发图片,不发文字
   messageTemplates = MESSAGE_TEMPLATES;
 
   // 交付类型定义
@@ -3301,6 +3302,7 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
     this.messageModalConfig = null;
     this.selectedTemplate = '';
     this.customMessage = '';
+    this.sendImageOnly = false;
     this.cdr.markForCheck();
   }
   
@@ -3312,29 +3314,51 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
     this.cdr.markForCheck();
   }
   
+  /**
+   * 只发图片选项变化处理
+   */
+  onSendImageOnlyChange(): void {
+    if (this.sendImageOnly) {
+      // 勾选"只发图片"时,清空话术和自定义消息
+      this.selectedTemplate = '';
+      this.customMessage = '';
+    }
+    this.cdr.markForCheck();
+  }
+  
   /**
    * 发送消息
    */
   async sendMessage(): Promise<void> {
     if (!this.project || !this.currentUser || !this.messageModalConfig) return;
     
-    const content = this.customMessage.trim() || this.selectedTemplate;
+    // 🔥 如果勾选了"只发图片",则强制content为空字符串
+    const content = this.sendImageOnly ? '' : (this.customMessage.trim() || this.selectedTemplate);
+    
+    // 🔥 验证:必须有图片或文字内容(不能两者都没有)
     if (!content && !this.messageModalConfig.imageUrls.length) {
       window?.fmode?.alert?.('请输入消息内容或选择图片');
       return;
     }
     
+    // 🔥 验证:如果没有勾选"只发图片",且没有图片时,必须有文字内容
+    if (!this.sendImageOnly && !content && this.messageModalConfig.imageUrls.length === 0) {
+      window?.fmode?.alert?.('请输入消息内容或选择预设话术');
+      return;
+    }
+    
     try {
       console.log('📤 [发送消息] 开始发送...');
+      console.log('🔘 [发送消息] 只发图片模式:', this.sendImageOnly);
       this.sendingMessage = true;
       this.cdr.markForCheck();
       
-      console.log('📝 [发送消息] 内容:', content);
+      console.log('📝 [发送消息] 内容:', content || '(无文字)');
       console.log('📸 [发送消息] 图片数量:', this.messageModalConfig.imageUrls.length);
       console.log('🏷️ [发送消息] 阶段:', this.messageModalConfig.stage);
       
       if (this.messageModalConfig.imageUrls.length > 0) {
-        // 发送图文消息
+        // 发送图文消息(如果content为空,则只发图片)
         await this.deliveryMessageService.createImageMessage(
           this.project.id!,
           this.messageModalConfig.stage,
@@ -3343,7 +3367,7 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
           this.currentUser
         );
       } else {
-        // 发送文本消息
+        // 发送文本消息
         await this.deliveryMessageService.createTextMessage(
           this.project.id!,
           this.messageModalConfig.stage,
@@ -3434,4 +3458,60 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
       }, 1000);
     }
   }
+  
+  /**
+   * 发送空间所有图片(发送清单)
+   */
+  sendSpaceAllImages(spaceId: string): void {
+    if (!this.project || !this.currentUser) return;
+    
+    // 收集该空间所有阶段的图片
+    const allImageUrls: string[] = [];
+    const stageNames: string[] = [];
+    
+    this.deliveryTypes.forEach(type => {
+      const files = this.getProductDeliveryFiles(spaceId, type.id);
+      const imageUrls = files
+        .filter(f => this.isImageFile(f.name))
+        .map(f => f.url);
+      
+      if (imageUrls.length > 0) {
+        allImageUrls.push(...imageUrls);
+        stageNames.push(type.name);
+      }
+    });
+    
+    if (allImageUrls.length === 0) {
+      window?.fmode?.alert?.('该空间暂无可发送的图片');
+      return;
+    }
+    
+    const space = this.projectProducts.find(p => p.id === spaceId);
+    if (!space) return;
+    
+    // 使用第一个有图片的阶段作为stage参数
+    const firstStageWithImages = this.deliveryTypes.find(type => 
+      this.getProductDeliveryFiles(spaceId, type.id).some(f => this.isImageFile(f.name))
+    );
+    
+    if (!firstStageWithImages) return;
+    
+    console.log(`📤 [发送清单] 空间: ${this.getSpaceDisplayName(space)}, 图片总数: ${allImageUrls.length}, 阶段: ${stageNames.join('、')}`);
+    
+    // 打开消息弹窗
+    this.messageModalConfig = {
+      spaceId,
+      spaceName: this.getSpaceDisplayName(space) + ` (${stageNames.join('、')})`,
+      stage: firstStageWithImages.id,
+      stageName: `全部阶段`,
+      imageUrls: allImageUrls
+    };
+    
+    // 默认选择第一个模板
+    const templates = this.deliveryMessageService.getStageTemplates(firstStageWithImages.id);
+    this.selectedTemplate = templates[0] || '';
+    this.customMessage = '';
+    this.showMessageModal = true;
+    this.cdr.markForCheck();
+  }
 }

+ 1 - 9
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -3,28 +3,20 @@ import { CommonModule } from '@angular/common';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { WxworkAuth, FmodeParse } from 'fmode-ng/core';
-import { IonIcon } from '@ionic/angular/standalone';
 import { MatDialog } from '@angular/material/dialog';
 import { ProductSpaceService, Project } from '../../../services/product-space.service';
 import { ProjectFileService } from '../../../services/project-file.service';
 import { DesignAnalysisAIService } from '../../../services/design-analysis-ai.service';
-import { addIcons } from 'ionicons';
-import { add, chevronDown, colorPalette, send, sparkles, trash } from 'ionicons/icons';
 import { AiDesignAnalysisComponent } from './components/ai-design-analysis/ai-design-analysis.component';
 import { SpaceRequirementItemComponent } from './components/space-requirement-item/space-requirement-item.component';
 
-addIcons({
-  add,sparkles,colorPalette,trash,chevronDown,send
-})
-
 @Component({
   selector: 'app-stage-requirements',
   standalone: true,
   imports: [
     CommonModule, 
     FormsModule, 
-    ReactiveFormsModule, 
-    IonIcon,
+    ReactiveFormsModule,
     AiDesignAnalysisComponent,
     SpaceRequirementItemComponent
   ],