wechat-drag-data-structures.md 12 KB

企业微信拖拽数据结构分析文档

🎯 目标

记录从企业微信拖拽不同类型消息到AI对话区域时的完整数据结构,为功能实现提供准确的参考。

📊 测试方法

打印函数

/**
 * 详细打印拖拽数据结构
 */
private logDragDataStructure(event: DragEvent, context: string): void {
  console.log(`\n========== [${context}] 拖拽数据结构分析 ==========`);
  
  const dt = event.dataTransfer;
  if (!dt) {
    console.log('❌ dataTransfer 为空');
    return;
  }

  // 1. 基本信息
  console.log('\n📋 基本信息:');
  console.log('  dropEffect:', dt.dropEffect);
  console.log('  effectAllowed:', dt.effectAllowed);
  console.log('  types:', Array.from(dt.types));
  
  // 2. Files
  console.log('\n📁 Files 对象:');
  console.log('  files.length:', dt.files?.length || 0);
  if (dt.files && dt.files.length > 0) {
    for (let i = 0; i < dt.files.length; i++) {
      const file = dt.files[i];
      console.log(`  [${i}] File对象:`, {
        name: file.name,
        size: file.size,
        type: file.type,
        lastModified: new Date(file.lastModified).toLocaleString(),
        webkitRelativePath: (file as any).webkitRelativePath || ''
      });
    }
  }
  
  // 3. Items
  console.log('\n📦 Items 对象:');
  console.log('  items.length:', dt.items?.length || 0);
  if (dt.items && dt.items.length > 0) {
    for (let i = 0; i < dt.items.length; i++) {
      const item = dt.items[i];
      console.log(`  [${i}] DataTransferItem:`, {
        kind: item.kind,
        type: item.type
      });
      
      // 尝试获取item的内容
      if (item.kind === 'string') {
        item.getAsString((str) => {
          console.log(`    → 字符串内容 (${item.type}):`, str.substring(0, 200));
        });
      } else if (item.kind === 'file') {
        const file = item.getAsFile();
        console.log(`    → 文件对象:`, file);
      }
    }
  }
  
  // 4. 各种数据类型
  console.log('\n📝 getData() 测试:');
  const commonTypes = [
    'text/plain',
    'text/html',
    'text/uri-list',
    'text/rtf',
    'application/json',
    'Files'
  ];
  
  for (const type of commonTypes) {
    try {
      const data = dt.getData(type);
      if (data) {
        console.log(`  ${type}:`, data.length > 200 ? data.substring(0, 200) + '...' : data);
      }
    } catch (e) {
      // 某些类型可能不可访问
    }
  }
  
  // 5. 自定义数据类型
  console.log('\n🔍 自定义数据类型:');
  if (dt.types) {
    for (const type of dt.types) {
      if (!commonTypes.includes(type)) {
        try {
          const data = dt.getData(type);
          console.log(`  ${type}:`, data);
        } catch (e) {
          console.log(`  ${type}: [无法读取]`);
        }
      }
    }
  }
  
  console.log('\n========== 数据结构分析结束 ==========\n');
}

📂 测试场景与数据结构

场景1: 拖拽单张图片

操作: 从企业微信群聊拖拽一张JPG图片

预期数据结构:

{
  基本信息: {
    dropEffect: "none",
    effectAllowed: "all",
    types: ["Files", "text/html", "text/plain"]
  },
  
  Files对象: {
    length: 1,
    [0]: {
      name: "image.jpg",
      size: 1234567,  // 字节
      type: "image/jpeg",
      lastModified: "2025-12-03 10:30:00",
      webkitRelativePath: ""
    }
  },
  
  Items对象: {
    length: 3,
    [0]: { kind: "file", type: "" },
    [1]: { kind: "string", type: "text/html" },
    [2]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/plain": "[图片]",
    "text/html": "<img src='...' />",
    "text/uri-list": ""
  }
}

场景2: 拖拽多张图片

操作: 从企业微信群聊选择3张图片一起拖拽

预期数据结构:

{
  基本信息: {
    types: ["Files", "text/html", "text/plain"]
  },
  
  Files对象: {
    length: 3,
    [0]: { name: "image1.jpg", size: 1234567, type: "image/jpeg" },
    [1]: { name: "image2.png", size: 2345678, type: "image/png" },
    [2]: { name: "image3.jpg", size: 3456789, type: "image/jpeg" }
  },
  
  Items对象: {
    length: 5,  // Files + HTML + Plain
    [0]: { kind: "file", type: "" },
    [1]: { kind: "file", type: "" },
    [2]: { kind: "file", type: "" },
    [3]: { kind: "string", type: "text/html" },
    [4]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/plain": "[图片] [图片] [图片]",
    "text/html": "<img src='...' /><img src='...' /><img src='...' />"
  }
}

场景3: 拖拽纯文字消息

操作: 从企业微信群聊拖拽一条文字消息

预期数据结构:

{
  基本信息: {
    types: ["text/html", "text/plain"]
  },
  
  Files对象: {
    length: 0  // 没有文件
  },
  
  Items对象: {
    length: 2,
    [0]: { kind: "string", type: "text/html" },
    [1]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/plain": "客户要求:现代简约风格,采光要好,预算20万",
    "text/html": "<div class='message'>客户要求:现代简约风格,采光要好,预算20万</div>"
  }
}

场景4: 拖拽图片+文字(混合内容)

操作: 从企业微信群聊同时选择图片和文字消息拖拽

预期数据结构:

{
  基本信息: {
    types: ["Files", "text/html", "text/plain"]
  },
  
  Files对象: {
    length: 2,
    [0]: { name: "image1.jpg", size: 1234567, type: "image/jpeg" },
    [1]: { name: "image2.jpg", size: 2345678, type: "image/jpeg" }
  },
  
  Items对象: {
    length: 4,
    [0]: { kind: "file", type: "" },
    [1]: { kind: "file", type: "" },
    [2]: { kind: "string", type: "text/html" },
    [3]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/plain": "[图片] [图片] 客户要求:现代简约风格...",
    "text/html": "<img src='...' /><img src='...' /><div>客户要求:...</div>"
  }
}

场景5: 拖拽图片URL链接

操作: 从企业微信拖拽图片的URL链接

预期数据结构:

{
  基本信息: {
    types: ["text/uri-list", "text/html", "text/plain"]
  },
  
  Files对象: {
    length: 0
  },
  
  Items对象: {
    length: 3,
    [0]: { kind: "string", type: "text/uri-list" },
    [1]: { kind: "string", type: "text/html" },
    [2]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/uri-list": "https://file-cloud.fmode.cn/path/to/image.jpg",
    "text/plain": "https://file-cloud.fmode.cn/path/to/image.jpg",
    "text/html": "<a href='https://file-cloud.fmode.cn/path/to/image.jpg'>...</a>"
  }
}

场景6: 拖拽文件(非图片)

操作: 从企业微信群聊拖拽PDF或CAD文件

预期数据结构:

{
  基本信息: {
    types: ["Files", "text/html", "text/plain"]
  },
  
  Files对象: {
    length: 1,
    [0]: {
      name: "design.pdf",
      size: 5678901,
      type: "application/pdf",
      lastModified: "2025-12-03 10:30:00"
    }
  },
  
  Items对象: {
    length: 3,
    [0]: { kind: "file", type: "application/pdf" },
    [1]: { kind: "string", type: "text/html" },
    [2]: { kind: "string", type: "text/plain" }
  },
  
  getData测试: {
    "text/plain": "[文件] design.pdf",
    "text/html": "<a href='...' download='design.pdf'>design.pdf</a>"
  }
}

🔍 关键发现

1. Types数组的特点

// 企业微信拖拽通常包含的types
types: [
  "Files",           // 有文件时出现
  "text/html",       // 几乎总是存在
  "text/plain",      // 几乎总是存在
  "text/uri-list"    // 有URL链接时出现
]

2. Files vs Items 的区别

  • Files: 只包含实际的文件对象(File类型)
  • Items: 包含所有拖拽项,包括文件和字符串数据

3. text/plain vs text/html

  • text/plain: 纯文本内容,图片显示为"[图片]"
  • text/html: HTML格式,图片包含<img>标签,文字包含格式

4. 企业微信特殊行为

  1. 图片拖拽:

    • Files中包含完整的File对象
    • text/plain显示为"[图片]"占位符
    • text/html包含img标签(可能包含base64或临时URL)
  2. 文字拖拽:

    • Files为空
    • text/plain包含纯文本
    • text/html包含带格式的HTML
  3. 混合拖拽:

    • Files包含所有图片文件
    • text/plain按顺序显示图片和文字
    • text/html混合显示img标签和文字div

💡 实现建议

检测拖拽内容类型

function detectDragContentType(event: DragEvent): string {
  const dt = event.dataTransfer;
  if (!dt) return 'empty';
  
  const types = Array.from(dt.types || []);
  
  // 1. 有文件 - 可能是图片、PDF、CAD
  if (types.includes('Files') && dt.files && dt.files.length > 0) {
    const hasImage = Array.from(dt.files).some(f => f.type.startsWith('image/'));
    const hasPDF = Array.from(dt.files).some(f => f.type === 'application/pdf');
    const hasCAD = Array.from(dt.files).some(f => 
      f.name.toLowerCase().endsWith('.dwg') || 
      f.name.toLowerCase().endsWith('.dxf')
    );
    
    if (hasImage) return 'images';
    if (hasPDF) return 'pdf';
    if (hasCAD) return 'cad';
    return 'files';
  }
  
  // 2. 有URL链接
  if (types.includes('text/uri-list')) {
    const uriList = dt.getData('text/uri-list');
    if (uriList && uriList.startsWith('http')) {
      return 'url';
    }
  }
  
  // 3. 纯文字
  if (types.includes('text/plain')) {
    return 'text';
  }
  
  return 'unknown';
}

提取所有内容

function extractAllDragContent(event: DragEvent): {
  files: File[];
  images: File[];
  text: string;
  html: string;
  urls: string[];
} {
  const dt = event.dataTransfer;
  if (!dt) return { files: [], images: [], text: '', html: '', urls: [] };
  
  // 提取文件
  const files: File[] = dt.files ? Array.from(dt.files) : [];
  const images = files.filter(f => f.type.startsWith('image/'));
  
  // 提取文字
  const text = dt.getData('text/plain') || '';
  const html = dt.getData('text/html') || '';
  
  // 提取URL
  const uriList = dt.getData('text/uri-list') || '';
  const urls = uriList.split('\n')
    .map(url => url.trim())
    .filter(url => url && !url.startsWith('#'));
  
  return { files, images, text, html, urls };
}

📝 测试代码

完整的测试方法

/**
 * 在AI对话拖拽区域添加测试
 */
onAIChat DragDrop(event: DragEvent) {
  event.preventDefault();
  event.stopPropagation();
  
  // 🔍 详细打印数据结构
  this.logDragDataStructure(event, 'AI对话区域');
  
  // 📊 提取并打印内容
  const content = this.extractAllDragContent(event);
  console.log('📊 提取的内容:', content);
  
  // 🎯 检测内容类型
  const contentType = this.detectDragContentType(event);
  console.log('🎯 内容类型:', contentType);
  
  // 继续处理拖拽内容...
}

🎓 使用示例

在stage-requirements组件中使用

// 1. 在AI对话输入区域添加拖拽监听
<div class="ai-chat-input-wrapper"
     (drop)="onAIChatDrop($event)"
     (dragover)="onAIChatDragOver($event)"
     (dragleave)="onAIChatDragLeave($event)"
     [class.drag-over]="aiChatDragOver">
  <!-- AI对话输入框 -->
</div>

// 2. 实现拖拽处理方法
async onAIChatDrop(event: DragEvent) {
  // 打印数据结构(开发测试用)
  this.logDragDataStructure(event, 'AI对话');
  
  // 提取内容
  const content = this.extractAllDragContent(event);
  
  // 处理图片
  if (content.images.length > 0) {
    await this.addImagesToAIChat(content.images);
  }
  
  // 处理文字
  if (content.text && !content.text.includes('[图片]')) {
    this.appendTextToAIInput(content.text);
  }
  
  // 处理URL
  if (content.urls.length > 0) {
    await this.downloadAndAddToAIChat(content.urls);
  }
}

文档版本: 1.0.0
创建时间: 2025-12-03
用途: 数据结构分析、功能实现参考、问题排查