Преглед изворни кода

refactor: improve WeChat Work message sending implementation

- Switched from openExistedChatWithMsg to sendChatMessage API for text messages to simplify code
- Reduced message sending delays from 500ms to 300ms for better user experience
- Created standalone delivery message modal component with bottom sheet design optimized for WeChat Work sidebar and mobile
- Added comprehensive documentation for the new modal component including usage examples and styling guidelines
徐福静0235668 пре 6 часа
родитељ
комит
7d4392c1ff

+ 13 - 20
src/app/pages/services/delivery-message.service.ts

@@ -237,23 +237,16 @@ export class DeliveryMessageService {
         throw new Error('群聊ID为空,无法发送消息');
       }
       
-      // 4️⃣ 发送文本消息(使用link类型,参考sendSurvey方法
+      // 4️⃣ 发送文本消息(使用sendChatMessage API
       if (text) {
         console.log('📝 [sendToWxwork] 发送文本消息...');
         console.log('  内容:', text);
         
-        // 🔥 参考project-detail.component.ts的sendSurvey方法
-        // 使用link类型发送消息,这是最可靠的方式
-        await this.wxworkService.ww.openExistedChatWithMsg({
-          chatId: chatId,
-          msg: {
-            msgtype: 'link',
-            link: {
-              title: '交付进度通知',
-              desc: text,  // 🔥 文本内容作为描述
-              url: window.location.href,  // 🔥 链接到当前页面
-              imgUrl: `${document.baseURI}assets/logo.jpg`  // 🔥 使用默认logo
-            }
+        // 🔥 使用sendChatMessage API直接发送文本
+        await this.wxworkService.ww.sendChatMessage({
+          msgtype: 'text',
+          text: {
+            content: text
           }
         });
         
@@ -261,18 +254,18 @@ export class DeliveryMessageService {
         
         // 如果有图片,等待一下再发送图片
         if (imageUrls.length > 0) {
-          await new Promise(resolve => setTimeout(resolve, 500));
+          await new Promise(resolve => setTimeout(resolve, 300));
         }
       }
       
-      // 5️⃣ 发送图片消息(使用link类型,参考sendSurvey方法
+      // 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);
         
-        // 🔥 参考project-detail.component.ts的sendSurvey方法
-        // 使用link类型发送图片,url和imgUrl都指向图片地址
+        // 🔥 使用link类型发送图片,可以显示图片预览
+        // sendChatMessage的image类型需要mediaid,不适合已有URL的场景
         await this.wxworkService.ww.openExistedChatWithMsg({
           chatId: chatId,
           msg: {
@@ -280,8 +273,8 @@ export class DeliveryMessageService {
             link: {
               title: `交付物 ${i + 1}/${imageUrls.length}`,
               desc: '点击查看大图',
-              url: imageUrl,  // 🔥 点击链接打开图片
-              imgUrl: imageUrl  // 🔥 显示图片预览
+              url: imageUrl,      // 🔥 点击打开图片
+              imgUrl: imageUrl    // 🔥 显示图片预览
             }
           }
         });
@@ -290,7 +283,7 @@ export class DeliveryMessageService {
         
         // 避免发送过快,加入延迟
         if (i < imageUrls.length - 1) {
-          await new Promise(resolve => setTimeout(resolve, 500));
+          await new Promise(resolve => setTimeout(resolve, 300));
         }
       }
       

+ 248 - 0
src/modules/project/pages/project-detail/stages/components/delivery-message-modal/README.md

@@ -0,0 +1,248 @@
+# 交付消息发送弹窗组件
+
+## 📦 组件说明
+
+独立的消息发送弹窗组件,专为企业微信侧边栏和移动端优化,采用底部弹出式设计。
+
+## 🎯 功能特性
+
+- ✅ 底部弹出式设计,适配企微侧边栏和移动端
+- ✅ 图片预览网格(4列)
+- ✅ 预设话术选择(可滚动)
+- ✅ 自定义消息输入
+- ✅ 只发图片选项(带自定义checkbox)
+- ✅ 响应式设计(移动端/桌面端)
+- ✅ 弹性动画效果
+- ✅ 企微视觉规范
+
+## 📖 使用方法
+
+### 1. 导入组件
+
+```typescript
+import { DeliveryMessageModalComponent, MessageModalConfig } from './components/delivery-message-modal/delivery-message-modal.component';
+
+@Component({
+  imports: [DeliveryMessageModalComponent, ...],
+  ...
+})
+```
+
+### 2. 配置数据
+
+```typescript
+export class YourComponent {
+  showMessageModal: boolean = false;
+  messageModalConfig: MessageModalConfig | null = null;
+  
+  openMessageModal() {
+    this.messageModalConfig = {
+      projectId: 'project_id',
+      spaceName: '客餐厅',
+      stageName: '白模阶段',
+      stage: 'white_model',
+      imageUrls: ['url1', 'url2', ...]
+    };
+    this.showMessageModal = true;
+  }
+  
+  closeMessageModal() {
+    this.showMessageModal = false;
+    this.messageModalConfig = null;
+  }
+  
+  async onSendMessage(messageData: { text: string; imageUrls: string[] }) {
+    const { text, imageUrls } = messageData;
+    // 处理发送逻辑
+    console.log('发送文本:', text);
+    console.log('发送图片:', imageUrls);
+  }
+}
+```
+
+### 3. 模板使用
+
+```html
+@if (showMessageModal && messageModalConfig) {
+  <app-delivery-message-modal
+    [config]="messageModalConfig"
+    [deliveryMessageService]="deliveryMessageService"
+    (close)="closeMessageModal()"
+    (send)="onSendMessage($event)">
+  </app-delivery-message-modal>
+}
+```
+
+## 组件接口
+
+```typescript
+export interface MessageModalConfig {
+  projectId: string;      // 项目ID
+  spaceName: string;      // 空间名称
+  stageName: string;      // 阶段名称
+  stage: string;          // 阶段ID
+  imageUrls: string[];    // 图片URL数组
+}
+
+// 输入
+@Input() config!: MessageModalConfig;
+@Input() deliveryMessageService!: DeliveryMessageService;
+
+// 输出
+@Output() close = new EventEmitter<void>();
+@Output() send = new EventEmitter<{ text: string; imageUrls: string[] }>();
+```
+
+## 样式特点
+
+### 弹窗尺寸
+- 移动端/企微: 全宽,最大高度80vh
+- 桌面端: 最大宽度480px,最大高度85vh
+
+### 颜色体系
+- 主色调: #576b95(企微蓝)
+- 成功色: #07c160(企微绿)
+- 背景色: #f7f8fa(浅灰)
+- 卡片色: #ffffff(白色)
+
+### 字体规范
+- 标题: 16px / 600
+- 副标题: 12px / 400
+- 内容: 13-14px / 400
+- 提示: 12px / 400
+- 按钮: 14px / 500
+- **内容**: 13-14px / 400
+- **提示**: 12px / 400
+- **按钮**: 14px / 500
+
+### 间距规范
+- **外边距**: 10px
+- **内边距**: 10-16px
+- **按钮间距**: 10px
+- **卡片间距**: 10px
+
+### 圆角规范
+- **弹窗**: 顶部12px,底部0px(移动端);全圆角12px(桌面端)
+- **卡片**: 8px
+- **输入框**: 6px
+- **图片**: 6px
+- **按钮**: 6px
+
+## 📱 响应式设计
+
+### 企微侧边栏(默认)
+```
+弹窗宽度:100%(全屏宽度)
+最大高度:80vh(确保底部按钮可见)
+弹出方式:从底部弹出
+圆角:顶部12px,底部0px
+```
+
+### 桌面端(>640px)
+```
+弹窗宽度:480px(居中)
+最大高度:85vh
+弹出方式:居中显示
+圆角:四周12px
+底部边距:40px
+```
+
+### 小屏幕(<480px)
+```
+最大高度:75vh
+话术区域:150px
+输入框高度:50px
+```
+
+## 🎯 核心优化
+
+### 1. 高度控制
+- 弹窗最大高度80vh,确保底部按钮始终可见
+- 话术选择区域限高200px,避免占用过多空间
+- 自定义输入框固定60px高度
+
+### 2. 滚动优化
+- 内容区域独立滚动,标题和按钮固定
+- 自定义滚动条样式(4px宽度)
+- 话术选择区域独立滚动(3px宽度)
+
+### 3. 自定义Checkbox
+```html
+<input type="checkbox" [(ngModel)]="sendImageOnly">
+<span class="checkbox-custom"></span>
+<span class="checkbox-label">只发图片(不发文字)</span>
+```
+
+样式:
+- 18x18px
+- 绿色选中态(#07c160)
+- 对勾图标
+- 圆角3px
+
+## 🚀 部署测试
+
+### 1. 检查弹窗显示
+- ✅ 从底部弹出,顶部圆角
+- ✅ 宽度100%,适配侧边栏
+- ✅ 最大高度80vh,底部按钮可见
+- ✅ 所有内容正常显示
+- ✅ 动画流畅自然
+
+### 2. 测试功能
+- 选择预设话术
+- 输入自定义消息
+- 勾选"只发图片"
+- 点击发送按钮
+- 确认消息正常发送
+
+### 3. 验证不同端显示
+- **企微侧边栏**: 全宽底部弹出
+- **桌面浏览器**: 480px居中弹窗
+- **手机浏览器**: 全宽底部弹出
+
+## 📝 注意事项
+
+1. **必须传入projectId**: 用于获取预设话术
+2. **imageUrls可为空数组**: 支持纯文本消息
+3. **stage必须匹配预设**: 用于获取对应阶段的话术模板
+4. **发送验证在子组件中完成**: 确保至少有文字或图片
+
+## 🔧 故障排除
+
+### 问题1: 底部按钮被截断
+**原因**: 弹窗高度过大
+**解决**: 降低max-height到80vh
+
+### 问题2: 话术区域过长
+**原因**: 预设话术过多
+**解决**: 限制max-height为200px,添加滚动
+
+### 问题3: checkbox显示错位
+**原因**: 没有使用checkbox-custom
+**解决**: 隐藏原生checkbox,使用自定义span
+
+### 问题4: 在小屏幕上内容溢出
+**原因**: 没有响应式适配
+**解决**: 添加@media查询,降低高度
+
+## 📦 文件结构
+
+```
+delivery-message-modal/
+├── delivery-message-modal.component.ts       # 组件逻辑
+├── delivery-message-modal.component.html     # 模板
+├── delivery-message-modal.component.scss     # 样式
+└── README.md                                 # 使用文档
+```
+
+## 🎉 完成!
+
+现在您已经有了一个完全独立、适配企微侧边栏的消息发送弹窗组件!
+
+**核心改进**:
+- 🎨 底部弹出式设计(更符合移动端习惯)
+- 📱 100%全宽显示(完美适配侧边栏)
+- 🎯 紧凑布局(所有内容可见)
+- ✨ 流畅动画效果(弹性cubic-bezier)
+- 🎨 企微视觉规范(颜色、字体、间距)
+- 🔧 独立可复用(可用于其他项目)

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

@@ -0,0 +1,115 @@
+<div class="message-modal-overlay" (click)="closeModal()">
+  <div class="message-modal-box" (click)="$event.stopPropagation()">
+    <!-- 标题栏 -->
+    <div class="modal-header">
+      <div class="modal-title">
+        <h4>发送到群聊</h4>
+        <p class="modal-subtitle">{{ config.spaceName }} - {{ config.stageName }}</p>
+      </div>
+      <button class="close-btn" (click)="closeModal()">×</button>
+    </div>
+    
+    <!-- 内容区域 -->
+    <div class="modal-body">
+      <!-- 图片预览 -->
+      @if (config.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>图片 ({{ config.imageUrls.length }} 张)</span>
+          </div>
+          <div class="images-preview-grid">
+            @for (imageUrl of config.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(config.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"
+          class="custom-input"></textarea>
+      </div>
+      
+      <!-- 只发图片选项 -->
+      <div class="send-options">
+        <label class="checkbox-option">
+          <input 
+            type="checkbox" 
+            [(ngModel)]="sendImageOnly"
+            (change)="onSendImageOnlyChange()">
+          <span class="checkbox-custom"></span>
+          <span class="checkbox-label">
+            只发图片(不发文字)
+          </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)="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>

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

@@ -0,0 +1,426 @@
+// 消息发送弹窗 - 企微端优化版
+.message-modal-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.4);
+  z-index: 10000;
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 弹窗容器
+.message-modal-box {
+  background: #f7f8fa;
+  border-radius: 12px 12px 0 0;
+  width: 100%;
+  max-width: 100%;
+  max-height: 80vh;  // 🔥 降低到80vh,确保底部按钮可见
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+  animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+  
+  @media (min-width: 641px) {
+    border-radius: 12px;
+    max-width: 480px;
+    max-height: 85vh;
+    margin-bottom: 40px;
+  }
+}
+
+// 标题栏
+.modal-header {
+  padding: 14px 16px;
+  background: white;
+  border-bottom: 1px solid #e5e7eb;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-shrink: 0;
+
+  .modal-title {
+    flex: 1;
+    
+    h4 { 
+      margin: 0; 
+      font-size: 16px;
+      font-weight: 600; 
+      color: #000000;
+      line-height: 1.3;
+    }
+    
+    .modal-subtitle { 
+      font-size: 12px;
+      color: #8c8c8c; 
+      margin-top: 4px;
+      line-height: 1.3;
+    }
+  }
+
+  .close-btn {
+    background: none; 
+    border: none; 
+    font-size: 28px;
+    line-height: 1;
+    color: #8c8c8c; 
+    cursor: pointer;
+    padding: 0;
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: color 0.2s;
+    flex-shrink: 0;
+    
+    &:hover { 
+      color: #000000; 
+    }
+  }
+}
+
+// 内容区域
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 10px;
+  background: #f7f8fa;
+  
+  // 滚动条优化
+  &::-webkit-scrollbar {
+    width: 4px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #d1d5db;
+    border-radius: 2px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+}
+
+// 区块标签
+.section-label {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #000000;
+  margin-bottom: 8px;
+  
+  svg {
+    flex-shrink: 0;
+    color: #576b95;
+  }
+}
+
+// 图片预览区域
+.images-preview-section {
+  background: white;
+  border-radius: 8px;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.images-preview-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 6px;
+  
+  .preview-image-item {
+    aspect-ratio: 1;
+    border-radius: 6px;
+    overflow: hidden;
+    border: 0.5px solid #e0e0e0;
+    background: #f5f5f5;
+    
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+}
+
+// 话术选择区域
+.templates-section {
+  background: white;
+  border-radius: 8px;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.template-options {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  max-height: 200px;  // 🔥 限制高度,避免占用过多空间
+  overflow-y: auto;
+  
+  &::-webkit-scrollbar {
+    width: 3px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #d1d5db;
+    border-radius: 2px;
+  }
+}
+
+.template-option {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  padding: 8px 10px;
+  font-size: 13px;
+  border-radius: 6px;
+  background: #fafafa;
+  border: 1px solid #e0e0e0;
+  cursor: pointer;
+  transition: all 0.2s;
+  line-height: 1.5;
+  
+  &:hover {
+    background: #f0f0f0;
+  }
+  
+  &.selected {
+    border-color: #576b95;
+    background: #e8f0fe;
+    color: #576b95;
+  }
+  
+  .radio-indicator {
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    border: 1.5px solid #d9d9d9;
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 2px;
+    
+    svg {
+      color: #576b95;
+    }
+  }
+  
+  &.selected .radio-indicator {
+    border-color: #576b95;
+  }
+  
+  .template-text {
+    flex: 1;
+    word-break: break-word;
+  }
+}
+
+// 自定义消息区域
+.custom-message-section {
+  background: white;
+  border-radius: 8px;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.custom-input {
+  width: 100%;
+  padding: 8px 10px;
+  font-size: 13px;
+  border: 1px solid #e0e0e0;
+  border-radius: 6px;
+  resize: none;
+  height: 60px;  // 🔥 减小高度
+  font-family: inherit;
+  transition: border-color 0.2s;
+  
+  &:focus {
+    outline: none;
+    border-color: #576b95;
+  }
+  
+  &::placeholder {
+    color: #bfbfbf;
+  }
+}
+
+// 只发图片选项
+.send-options {
+  background: white;
+  border-radius: 8px;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.checkbox-option {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  user-select: none;
+  
+  input[type="checkbox"] {
+    position: absolute;
+    opacity: 0;
+    width: 0;
+    height: 0;
+    
+    &:checked + .checkbox-custom {
+      background: #07c160;
+      border-color: #07c160;
+      
+      &::after {
+        display: block;
+      }
+    }
+  }
+  
+  .checkbox-custom {
+    width: 18px;
+    height: 18px;
+    border: 1.5px solid #d9d9d9;
+    border-radius: 3px;
+    flex-shrink: 0;
+    position: relative;
+    transition: all 0.2s;
+    background: white;
+    
+    // 对勾图标
+    &::after {
+      content: '';
+      position: absolute;
+      display: none;
+      left: 5px;
+      top: 2px;
+      width: 4px;
+      height: 8px;
+      border: solid white;
+      border-width: 0 2px 2px 0;
+      transform: rotate(45deg);
+    }
+  }
+  
+  .checkbox-label {
+    font-size: 13px;
+    color: #000000;
+    line-height: 1.3;
+  }
+}
+
+// 提示信息
+.send-tips {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 10px;
+  background: #fff7e6;
+  border-radius: 6px;
+  font-size: 12px;
+  color: #666666;
+  line-height: 1.4;
+  
+  svg {
+    flex-shrink: 0;
+    color: #faad14;
+  }
+}
+
+// 底部按钮区域
+.modal-footer {
+  padding: 10px;
+  display: flex;
+  gap: 10px;
+  background: white;
+  border-top: 1px solid #e5e7eb;
+  flex-shrink: 0;  // 🔥 防止被压缩
+  
+  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 {
+    max-height: 75vh;  // 🔥 小屏幕进一步降低高度
+  }
+  
+  .template-options {
+    max-height: 150px;  // 🔥 减小话术区域高度
+  }
+  
+  .custom-input {
+    height: 50px;  // 🔥 减小输入框高度
+  }
+}

+ 83 - 0
src/modules/project/pages/project-detail/stages/components/delivery-message-modal/delivery-message-modal.component.ts

@@ -0,0 +1,83 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { DeliveryMessageService } from '../../../../../../../app/pages/services/delivery-message.service';
+
+export interface MessageModalConfig {
+  projectId: string;
+  spaceName: string;
+  stageName: string;
+  stage: string;
+  imageUrls: string[];
+}
+
+@Component({
+  selector: 'app-delivery-message-modal',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './delivery-message-modal.component.html',
+  styleUrls: ['./delivery-message-modal.component.scss']
+})
+export class DeliveryMessageModalComponent implements OnInit {
+  @Input() config!: MessageModalConfig;
+  @Input() deliveryMessageService!: DeliveryMessageService;
+  @Output() close = new EventEmitter<void>();
+  @Output() send = new EventEmitter<{ text: string; imageUrls: string[] }>();
+
+  selectedTemplate: string = '';
+  customMessage: string = '';
+  sendImageOnly: boolean = false;
+  sendingMessage: boolean = false;
+
+  ngOnInit() {
+    // 初始化,如果有预设话术,默认选择第一个
+    const templates = this.deliveryMessageService.getStageTemplates(this.config.stage);
+    if (templates && templates.length > 0) {
+      this.selectedTemplate = templates[0];
+    }
+  }
+
+  onSendImageOnlyChange() {
+    if (this.sendImageOnly) {
+      this.selectedTemplate = '';
+      this.customMessage = '';
+    }
+  }
+
+  closeModal() {
+    this.close.emit();
+  }
+
+  async sendMessage() {
+    // 确定要发送的文本内容
+    let textToSend = '';
+    if (!this.sendImageOnly) {
+      textToSend = this.customMessage || this.selectedTemplate;
+    }
+
+    // 验证
+    if (!this.sendImageOnly && !textToSend && this.config.imageUrls.length === 0) {
+      window?.fmode?.alert('请输入消息内容或选择预设话术');
+      return;
+    }
+
+    if (this.sendImageOnly && this.config.imageUrls.length === 0) {
+      window?.fmode?.alert('没有图片可发送');
+      return;
+    }
+
+    this.sendingMessage = true;
+
+    try {
+      // 发送消息
+      this.send.emit({
+        text: textToSend,
+        imageUrls: this.config.imageUrls
+      });
+    } catch (error) {
+      console.error('发送消息失败:', error);
+      window?.fmode?.alert('发送失败,请重试');
+      this.sendingMessage = false;
+    }
+  }
+}

+ 6 - 111
src/modules/project/pages/project-detail/stages/stage-delivery-new.component.html

@@ -316,115 +316,10 @@
 
 <!-- 消息发送弹窗 -->
 @if (showMessageModal && messageModalConfig) {
-  <div class="message-modal-overlay" (click)="closeMessageModal()">
-    <div class="message-modal-box" (click)="$event.stopPropagation()">
-      <div class="modal-header">
-        <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">
-        <!-- 图片预览 -->
-        @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"
-            class="custom-input"></textarea>
-        </div>
-        
-        <!-- 只发图片选项 -->
-        <div class="send-options">
-          <label class="checkbox-option">
-            <input 
-              type="checkbox" 
-              [(ngModel)]="sendImageOnly"
-              (change)="onSendImageOnlyChange()">
-            <span class="checkbox-label">
-              只发图片(不发文字)
-            </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">
-          <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>
+  <app-delivery-message-modal
+    [config]="messageModalConfig"
+    [deliveryMessageService]="deliveryMessageService"
+    (close)="closeMessageModal()"
+    (send)="onSendMessage($event)">
+  </app-delivery-message-modal>
 }

+ 202 - 149
src/modules/project/pages/project-detail/stages/stage-delivery-new.component.scss

@@ -459,8 +459,8 @@
   to { opacity: 1; transform: translateY(0); }
 }
 
-// Modals (Simplified)
-.stage-gallery-modal-overlay, .message-modal-overlay {
+// Gallery Modal
+.stage-gallery-modal-overlay {
   position: fixed;
   inset: 0;
   background: rgba(0, 0, 0, 0.5);
@@ -472,76 +472,134 @@
   animation: fadeIn 0.2s ease-out;
 }
 
+.stage-gallery-modal {
+  background: white;
+  border-radius: 12px;
+  width: 100%;
+  max-width: 90vw;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  animation: slideUp 0.3s ease-out;
+}
+
+// Message Modal - 底部弹出式
+.message-modal-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.4);
+  z-index: 10000;
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  animation: fadeIn 0.2s ease-out;
+}
+
 @keyframes fadeIn {
   from { opacity: 0; }
   to { opacity: 1; }
 }
 
-.stage-gallery-modal, .message-modal-box {
-  background: white;
-  border-radius: 10px;  // 🔥 减小圆角
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.message-modal-box {
+  background: #f7f8fa;
+  border-radius: 12px 12px 0 0;
   width: 100%;
-  max-width: 400px;  // 🔥 进一步减小到400px,紧凑型布局
-  max-height: 90vh;  // 🔥 调整最大高度
+  max-width: 100%;
+  max-height: 85vh;
   display: flex;
   flex-direction: column;
-  box-shadow: 0 8px 20px rgba(0,0,0,0.15);  // 🔥 减小阴影
+  box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
   overflow: hidden;
-  animation: slideUp 0.25s ease-out;  // 🔥 加快动画
-  margin: 0 auto;  // 🔥 居中显示
+  animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
   
-  // 🔥 移动端/企微端适配
-  @media (max-width: 640px) {
-    max-width: 96vw;  // 🔥 留出4%边距
-    max-height: 92vh;
-    border-radius: 8px;
+  @media (min-width: 641px) {
+    border-radius: 12px;
+    max-width: 500px;
+    max-height: 80vh;
+    margin-bottom: 40px;
   }
 
-  .gallery-header, .modal-header {
-    padding: 12px 14px;  // 🔥 紧凑型内边距
-    border-bottom: 1px solid #e2e8f0;
+  .gallery-header {
+    padding: 16px;
+    border-bottom: 1px solid #e5e7eb;
     display: flex;
     justify-content: space-between;
     align-items: center;
-    flex-shrink: 0;  // 🔥 防止压缩
-    background: linear-gradient(to bottom, #fafafa, #ffffff);  // 🔥 添加渐变
+    background: white;
+    
+    .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;
+      &:hover { color: #ef4444; }
+    }
+  }
+
+  .modal-header {
+    padding: 16px;
+    background: white;
+    border-bottom: 1px solid #e5e7eb;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-shrink: 0;
 
     .modal-title {
       flex: 1;
       h4 { 
         margin: 0; 
-        font-size: 15px;  // 
-        font-weight: 700; 
-        color: #1e293b;
+        font-size: 16px;
+        font-weight: 600; 
+        color: #000000;
       }
       .modal-subtitle { 
-        font-size: 11px;  // 
-        color: #94a3b8; 
-        margin-top: 2px;
+        font-size: 12px;
+        color: #8c8c8c; 
+        margin-top: 4px;
       }
     }
 
-    .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; 
+      font-size: 28px;
+      line-height: 1;
+      color: #8c8c8c; 
       cursor: pointer;
+      padding: 0;
+      width: 32px;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
       transition: color 0.2s;
-      &:hover { color: #ef4444; }
+      &:hover { color: #000000; }
     }
   }
 
-  .gallery-content, .modal-body {
+  .gallery-content {
     flex: 1;
     overflow-y: auto;
-    padding: 12px 14px;  // 🔥 紧凑型内边距
-    overflow-x: hidden;  // 🔥 隐藏横向滚动条
+    padding: 16px;
+    background: white;
 
-    // Gallery Grid
     .images-grid {
       display: grid;
       grid-template-columns: repeat(3, 1fr);
@@ -567,47 +625,52 @@
         }
       }
     }
+  }
+
+  .modal-body {
+    flex: 1;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding: 12px;
+    background: #f7f8fa;
 
     // 消息弹窗专用样式
     .section-label {
       display: flex;
       align-items: center;
-      gap: 5px;  // 🔥 减小间距
-      font-size: 12px;  // 🔥 减小字体
-      font-weight: 600;
-      color: #475569;
-      margin-bottom: 8px;  // 🔥 减小下边距
+      gap: 4px;
+      font-size: 13px;
+      font-weight: 500;
+      color: #000000;
+      margin-bottom: 8px;
       
       svg {
         flex-shrink: 0;
-        color: #6366f1;
-        width: 16px;  // 🔥 减小图标
+        color: #576b95;
+        width: 16px;
         height: 16px;
       }
     }
 
     // 图片预览区域
     .images-preview-section {
-      margin-bottom: 12px;  // 🔥 进一步减小下边距
+      margin-bottom: 12px;
+      padding: 10px;
+      background: white;
+      border-radius: 8px;
       
       .images-preview-grid {
         display: grid;
-        grid-template-columns: repeat(4, 1fr);  // 🔥 4列布局
-        gap: 5px;  // 🔥 进一步减小间距
-        margin-top: 6px;  // 🔥 进一步减小上边距
-        
-        // 🔥 移动端/小屏幕适配
-        @media (max-width: 480px) {
-          grid-template-columns: repeat(3, 1fr);
-          gap: 6px;
-        }
+        grid-template-columns: repeat(4, 1fr);
+        gap: 6px;
+        margin-top: 6px;
         
         .preview-image-item {
           aspect-ratio: 1;
-          border-radius: 4px;  // 🔥 减小圆角
+          border-radius: 6px;
           overflow: hidden;
-          border: 1.5px solid #e2e8f0;  // 🔥 减小边框
-          background: #f8fafc;
+          border: 0.5px solid #e0e0e0;
+          background: #fafafa;
           
           img {
             width: 100%;
@@ -620,34 +683,37 @@
 
     // 预设话术选择
     .templates-section {
-      margin-bottom: 12px;  // 🔥 进一步减小下边距
+      margin-bottom: 12px;
+      padding: 10px;
+      background: white;
+      border-radius: 8px;
       
       .template-options {
         display: flex;
         flex-direction: column;
-        gap: 6px;  // 🔥 减小间距
-        max-height: 200px;  // 🔥 进一步减小最大高度
-        overflow-y: auto;   // 🔥 支持滚动
+        gap: 8px;
+        max-height: 280px;  // 🔥 限制高度,避免过长
+        overflow-y: auto;
         
         .template-option {
           display: flex;
           align-items: flex-start;
-          gap: 8px;  // 🔥 减小间距
-          padding: 8px 10px;  // 🔥 紧凑型内边距
-          border: 1.5px solid #e2e8f0;  // 🔥 减小边框
-          border-radius: 6px;  // 🔥 减小圆角
+          gap: 8px;
+          padding: 10px;
+          border: 1px solid #e5e7eb;
+          border-radius: 6px;
           cursor: pointer;
-          transition: all 0.2s;
-          background: white;
+          transition: all 0.15s;
+          background: #fafafa;
           
           &:hover {
-            border-color: #c7d2fe;
-            background: #f5f8ff;
+            background: #f0f0f0;
+            border-color: #d0d0d0;
           }
           
           &.selected {
-            border-color: #6366f1;
-            background: #eef2ff;
+            border-color: #576b95;
+            background: #e8f0fe;
             
             .radio-indicator {
               border-color: #6366f1;
@@ -655,26 +721,26 @@
           }
           
           .radio-indicator {
-            width: 16px;
-            height: 16px;
-            border: 2px solid #cbd5e1;
+            width: 18px;
+            height: 18px;
+            border: 2px solid #d9d9d9;
             border-radius: 50%;
             flex-shrink: 0;
             display: flex;
             align-items: center;
             justify-content: center;
-            margin-top: 2px;
+            margin-top: 1px;
             
             svg {
-              color: #6366f1;
+              color: #576b95;
             }
           }
           
           .template-text {
             flex: 1;
-            font-size: 12px;  // 🔥 减小字体
-            color: #334155;
-            line-height: 1.4;  // 🔥 减小行高
+            font-size: 13px;
+            color: #000000;
+            line-height: 1.5;
           }
         }
       }
@@ -682,38 +748,41 @@
 
     // 自定义消息输入
     .custom-message-section {
-      margin-bottom: 10px;  // 🔥 进一步减小下边距
+      margin-bottom: 12px;
+      padding: 10px;
+      background: white;
+      border-radius: 8px;
       
       .custom-input {
         width: 100%;
-        padding: 8px 10px;  // 🔥 紧凑型内边距
-        border: 1.5px solid #e2e8f0;  // 🔥 减小边框
-        border-radius: 6px;  // 🔥 减小圆角
-        font-size: 12px;  // 🔥 减小字体
-        color: #334155;
-        resize: vertical;
-        min-height: 60px;  // 🔥 进一步减小最小高度
-        max-height: 100px;  // 🔥 进一步减小最大高度
+        padding: 10px;
+        border: 1px solid #e0e0e0;
+        border-radius: 6px;
+        font-size: 14px;
+        color: #000000;
+        resize: none;  // 🔥 禁止拖动调整
+        height: 70px;  // 🔥 固定高度
         transition: border-color 0.2s;
-        line-height: 1.4;  // 🔥 减小行高
+        line-height: 1.5;
         
         &:focus {
           outline: none;
-          border-color: #6366f1;
+          border-color: #576b95;
+          background: #ffffff;
         }
         
         &::placeholder {
-          color: #94a3b8;
+          color: #bfbfbf;
         }
       }
     }
 
     // 只发图片选项
     .send-options {
-      margin-bottom: 10px;
-      padding: 8px 10px;
-      background: #f8fafc;
-      border-radius: 5px;
+      margin-bottom: 12px;
+      padding: 10px;
+      background: white;
+      border-radius: 8px;
       
       .checkbox-option {
         display: flex;
@@ -730,19 +799,9 @@
         }
         
         .checkbox-label {
-          display: flex;
-          align-items: center;
-          gap: 5px;
-          font-size: 12px;
-          color: #475569;
-          font-weight: 500;
-          white-space: nowrap;
-          
-          svg {
-            color: #6366f1;
-            width: 14px;
-            height: 14px;
-          }
+          font-size: 13px;
+          color: #000000;
+          font-weight: 400;
         }
       }
     }
@@ -750,42 +809,35 @@
     // 提示信息
     .send-tips {
       display: flex;
-      align-items: flex-start;
-      gap: 8px;
-      padding: 10px;
-      background: #f0f9ff;
-      border: 1px solid #bae6fd;
+      align-items: center;
+      gap: 6px;
+      padding: 8px 10px;
+      background: #f0f7ff;
       border-radius: 6px;
       
       svg {
         flex-shrink: 0;
-        color: #0ea5e9;
-        margin-top: 1px;
+        color: #1890ff;
+        width: 14px;
+        height: 14px;
       }
       
       span {
         flex: 1;
         font-size: 12px;
-        color: #0c4a6e;
+        color: #666666;
         line-height: 1.4;
       }
     }
   }
 
-  .gallery-footer, .modal-footer {
-    padding: 10px 14px;  // 🔥 紧凑型内边距
-    border-top: 1px solid #e2e8f0;
+  .modal-footer {
+    padding: 12px;
+    border-top: 1px solid #e5e7eb;
     display: flex;
-    justify-content: flex-end;
-    gap: 8px;  // 🔥 调整按钮间距
-    background: #fafafa;  // 🔥 添加浅灰背景
-    flex-shrink: 0;  // 🔥 防止压缩
-    
-    // 🔥 移动端适配
-    @media (max-width: 480px) {
-      padding: 8px 10px;
-      gap: 6px;
-    }
+    gap: 12px;
+    background: white;
+    flex-shrink: 0;
 
     .add-files-btn {
       display: flex; 
@@ -805,52 +857,53 @@
       }
     }
     
-    .close-gallery-btn, .btn-cancel {
-       background: white; 
-       border: 1px solid #e2e8f0; 
-       color: #64748b; 
-       padding: 8px 14px;  // 🔥 紧凑型内边距
-       border-radius: 5px;  // 🔥 减小圆角
+    .btn-cancel {
+       flex: 1;
+       background: #ffffff; 
+       border: 1px solid #d9d9d9; 
+       color: #000000; 
+       padding: 11px;
+       border-radius: 6px;
        cursor: pointer; 
-       font-size: 12px;  // 🔥 减小字体
-       font-weight: 500;  // 🔥 加粗
+       font-size: 15px;
+       font-weight: 400; 
        transition: all 0.2s;
        
        &:hover {
-         border-color: #cbd5e1;
-         background: #f8fafc;
+         background: #fafafa;
        }
     }
     
     .btn-send {
+       flex: 2;
        display: flex;
        align-items: center;
-       gap: 5px;  // 🔥 减小间距
-       background: #10b981; 
+       justify-content: center;
+       gap: 6px;
+       background: #07c160;
        color: white; 
        border: none; 
-       padding: 8px 16px;  // 🔥 紧凑型内边距
-       border-radius: 5px;  // 🔥 减小圆角
+       padding: 11px;
+       border-radius: 6px;
        cursor: pointer; 
-       font-size: 13px;  // 🔥 减小字体
-       font-weight: 600;
+       font-size: 15px;
+       font-weight: 400;
        transition: all 0.2s;
-       box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2);  // 🔥 添加阴影
        
        &:hover:not(:disabled) {
-         background: #059669;
-         transform: translateY(-1px);
-         box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);  // 🔥 hover阴影
+         background: #06ad56;
        }
        
        &:disabled { 
          opacity: 0.5; 
          cursor: not-allowed;
-         box-shadow: none;  // 🔥 禁用时移除阴影
+         background: #b0b0b0;
        }
        
        svg {
          flex-shrink: 0;
+         width: 16px;
+         height: 16px;
        }
     }
   }

+ 18 - 71
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -13,6 +13,7 @@ import { RevisionTaskListComponent } from '../../../components/revision-task-lis
 import { RevisionTaskService } from '../../../../../app/pages/services/revision-task.service';
 import { DeliveryMessageService, MESSAGE_TEMPLATES } from '../../../../../app/pages/services/delivery-message.service';
 import { WxworkSDKService } from '../../../services/wxwork-sdk.service';
+import { DeliveryMessageModalComponent, MessageModalConfig } from './components/delivery-message-modal/delivery-message-modal.component';
 import { ImageAnalysisService } from '../../../services/image-analysis.service';
 import { PhaseDeadlines, PhaseName } from '../../../../../app/models/project-phase.model';
 import { ensurePhaseDeadlines, mapDeliveryTypeToPhase, markPhaseStatus, updatePhaseOnSubmission } from '../../../../../app/utils/phase-deadline.utils';
@@ -82,7 +83,7 @@ interface DeliveryFile {
 @Component({
   selector: 'app-stage-delivery',
   standalone: true,
-  imports: [CommonModule, FormsModule, DragUploadModalComponent, RevisionTaskModalComponent, RevisionTaskListComponent],
+  imports: [CommonModule, FormsModule, DragUploadModalComponent, RevisionTaskModalComponent, RevisionTaskListComponent, DeliveryMessageModalComponent],
   templateUrl: './stage-delivery-new.component.html',
   styleUrls: ['./stage-delivery-new.component.scss'],
   changeDetection: ChangeDetectionStrategy.OnPush
@@ -128,17 +129,8 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
   
   // 消息发送相关
   showMessageModal: boolean = false;
-  messageModalConfig: {
-    spaceId: string;
-    spaceName: string;
-    stage: string;
-    stageName: string;
-    imageUrls: string[];
-  } | null = null;
-  selectedTemplate: string = '';
-  customMessage: string = '';
+  messageModalConfig: MessageModalConfig | null = null;
   sendingMessage: boolean = false;
-  sendImageOnly: boolean = false; // 只发图片,不发文字
   messageTemplates = MESSAGE_TEMPLATES;
 
   // 交付类型定义
@@ -3260,20 +3252,16 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
     const space = this.projectProducts.find(p => p.id === spaceId);
     const stageInfo = this.deliveryTypes.find(t => t.id === stage);
     
-    if (!space || !stageInfo) return;
+    if (!space || !stageInfo || !this.projectId) return;
     
     this.messageModalConfig = {
-      spaceId,
+      projectId: this.projectId,
       spaceName: this.getSpaceDisplayName(space),
-      stage,
       stageName: stageInfo.name,
+      stage,
       imageUrls
     };
     
-    // 默认选择第一个模板
-    const templates = this.deliveryMessageService.getStageTemplates(stage);
-    this.selectedTemplate = templates[0] || '';
-    this.customMessage = '';
     this.showMessageModal = true;
     this.cdr.markForCheck();
   }
@@ -3300,78 +3288,41 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
   closeMessageModal(): void {
     this.showMessageModal = false;
     this.messageModalConfig = null;
-    this.selectedTemplate = '';
-    this.customMessage = '';
-    this.sendImageOnly = false;
     this.cdr.markForCheck();
   }
   
   /**
-   * 选择话术模板
+   * 发送消息(从子组件接收消息数据)
    */
-  selectTemplate(template: string): void {
-    this.selectedTemplate = template;
-    this.cdr.markForCheck();
-  }
-  
-  /**
-   * 只发图片选项变化处理
-   */
-  onSendImageOnlyChange(): void {
-    if (this.sendImageOnly) {
-      // 勾选"只发图片"时,清空话术和自定义消息
-      this.selectedTemplate = '';
-      this.customMessage = '';
-    }
-    this.cdr.markForCheck();
-  }
-  
-  /**
-   * 发送消息
-   */
-  async sendMessage(): Promise<void> {
+  async onSendMessage(messageData: { text: string; imageUrls: string[] }): Promise<void> {
     if (!this.project || !this.currentUser || !this.messageModalConfig) return;
     
-    // 🔥 如果勾选了"只发图片",则强制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;
-    }
+    const { text, imageUrls } = messageData;
     
     try {
       console.log('📤 [发送消息] 开始发送...');
-      console.log('🔘 [发送消息] 只发图片模式:', this.sendImageOnly);
       this.sendingMessage = true;
       this.cdr.markForCheck();
       
-      console.log('📝 [发送消息] 内容:', content || '(无文字)');
-      console.log('📸 [发送消息] 图片数量:', this.messageModalConfig.imageUrls.length);
+      console.log('📝 [发送消息] 内容:', text || '(无文字)');
+      console.log('📸 [发送消息] 图片数量:', imageUrls.length);
       console.log('🏷️ [发送消息] 阶段:', this.messageModalConfig.stage);
       
-      if (this.messageModalConfig.imageUrls.length > 0) {
-        // 发送图文消息(如果content为空,则只发图片)
+      if (imageUrls.length > 0) {
+        // 发送图文消息(如果text为空,则只发图片)
         await this.deliveryMessageService.createImageMessage(
           this.project.id!,
           this.messageModalConfig.stage,
-          this.messageModalConfig.imageUrls,
-          content,
+          imageUrls,
+          text,
           this.currentUser
         );
-      } else {
+      } else if (text) {
         // 发送纯文本消息
         await this.deliveryMessageService.createTextMessage(
           this.project.id!,
           this.messageModalConfig.stage,
-          content,
+          text,
           this.currentUser
         );
       }
@@ -3500,17 +3451,13 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
     
     // 打开消息弹窗
     this.messageModalConfig = {
-      spaceId,
+      projectId: this.projectId,
       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();
   }