前端批量导入内容——word模板方案实现

1.需求分析

在日常教学和考试系统中,经常需要批量导入大量题目到系统中。手动逐条添加不仅效率低下,而且容易出错。因此我们需要一种能够从 Word 文档中批量导入题目的解决方案。

1.1 模板格式要求

Word模板采用指定排版格式,格式排版如下

模板下载地址

1.2 目标数据结构

最终需要将 Word 内容转换为以下 JSON 格式

json 复制代码
[{
  "questionContent": "题目内容",
  "options": [
    {"index": "A", "content": "选项A内容"},
    {"index": "B", "content": "选项B内容"}
  ],
  "answer": "正确答案",
  "primaryQuesType": "题型",
  "analysis": "解析内容",
  "cardType": "证书类型",
  "catalogue": "题目分类",
  "knowledgePointsSimple": "知识点",
  "source": "来源",
  "remarks": "备注",
  "quesLevel": "难度级别"
}]

2. 实现方案

整体处理流程分为四个主要步骤:

js 复制代码
const handleBatch = async () => {
  // 1. 获取文件对象
  const file = await readDocxFile()
  
  // 2. 解析读取为html
  const htmlString = await convertToHtml()
  
  // 3. 解析成指定JSON格式
  const targetJson = parseHtml()
  
  // 4. 调用批量接口
  // questionAPI.batchAdd(targetJson)
}

2.1 获取文件对象

通过动态创建文件上传元素实现文件选择功能:

js 复制代码
const readDocxFile = async () => {
  return new Promise((resolve, reject) => {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = ".docx,.doc";
    input.style.display = "none";

    input.addEventListener("change", async (event) => {
      const file = event.target.files[0];
      if (!file) {
        reject(new Error("未选择文件"));
        return;
      }
      document.body.removeChild(input);

      resolve(file);
    });

    // 添加到DOM并触发点击
    document.body.appendChild(input);
    input.click();
  });
};

2.2 解析Word文件为HTML

使用 mammoth.js 库将 Word 文档转换为 HTML 格式,该库支持多种输出格式:HTML格式、文本格式、Markdown格式。

预览效果地址 在浏览器中预览 word docx 文档示例 demo

github 链接 mwilliamson/mammoth.js

js 复制代码
import mammoth from "mammoth";

const convertToHtml = async (file) => {
    const arrayBuffer = await file.arrayBuffer();
    // 解析为HTML
    const result = await mammoth.convertToHtml({ arrayBuffer });
    // 解析为Markdown
    // const result = await mammoth.convertToMarkdown({ arrayBuffer })
    // 解析为TEXT
    // const result = await mammoth.extractRawText({ arrayBuffer })
    return result.value
}

转换效果对比:

  • HTML格式:保留完整格式信息,适合进一步解析
  • Text格式:纯文本内容,丢失格式信息
  • Markdown格式:转换为Markdown语法,适合文档处理

html格式结果

text格式结果

markdown格式结果

2.3 解析HTML为JSON格式

转换后的HTML内容解析为目标JSON结构:

js 复制代码
/** 
* 数字转换为字母(1->A, 2->B, ...) 
* @param {number} num 数字 
* @returns {string} 对应字母 
*/
const numberToLetter = (num) => { return String.fromCharCode(64 + num); };

/** 
* 解析HTML内容为题目JSON数组 
* @param {string} html HTML内容 
* @returns {Array} 题目对象数组 
*/
const parseHtml = (html) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");
  const questions = [];

  // 获取所有h3标题(每个题目一个)
  const questionHeaders = doc.querySelectorAll("h3");

  questionHeaders.forEach((header) => {
    const question = {
      questionContent: "",
      options: [],
      answer: "",
      primaryQuesType: "",
      analysis: "",
      cardType: "",
      catalogue: "",
      knowledgePointsSimple: "",
      source: "",
      remarks: "",
      quesLevel: "",
    };

    // 提取题目
    question.questionContent = header.textContent.trim();

    // 提取选项(紧随h3后面的ol中的li)
    const optionsList = header.nextElementSibling;
    if (optionsList && optionsList.tagName === "OL") {
      const optionItems = optionsList.querySelectorAll("li");
      optionItems.forEach((item, index) => {
        question.options.push({
          index: numberToLetter(index + 1),
          content: item.textContent.trim(),
        });
      });
    }

    // 提取答案和其他信息(在ol后面的p中)
    let nextElement = optionsList ? optionsList.nextElementSibling : null;
    while (nextElement) {
      if (nextElement.tagName === "P") {
        const content = nextElement.textContent.trim();
        if (content.startsWith("答案")) {
          question.answer = content.replace("答案:", "").trim();
        } else if (content.startsWith("题型")) {
          question.primaryQuesType = content.replace("题型:", "").trim();
        } else if (content.includes("解析:")) {
          question.analysis = content.replace("解析:", "").trim();
        } else if (content.startsWith("证书类型:")) {
          question.cardType = content.replace("证书类型:", "").trim();
        } else if (content.startsWith("题目来源分类:")) {
          question.catalogue = content.replace("题目来源分类:", "").trim();
        } else if (content.startsWith("知识点:")) {
          question.knowledgePointsSimple = content.replace("知识点:", "").trim();
        } else if (content.startsWith("来源:")) {
          question.source = content.replace("来源:", "").trim();
        } else if (content.startsWith("备注")) {
          question.remarks = content.replace("备注:", "").trim();
        } else if (content.startsWith("难度级别")) {
          question.quesLevel = content.replace("难度级别: ", "").trim();
        }
      }
      nextElement = nextElement.nextElementSibling;
    }

    questions.push(question);
  });

  return questions;
};

2.4 批量上传接口调用

获取到格式化后的JSON数据后,调用后端批量上传接口

3.图片多模态类扩展支持

对于包含图片的题目,需要特殊处理:

3.1 图片提取与上传

Word中的图片会被mammoth.js转换为base64格式的img标签:

html 复制代码
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." alt="示例图片">

需将其上传为在线地址,处理方案:

js 复制代码
/**
 * 处理HTML中的图片,将base64转换为网络URL
 * @param {string} html HTML内容
 * @returns {Promise<string>} 处理后的HTML
 */
const processImages = async (html) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");
  const images = doc.querySelectorAll('img[src^="data:image/"]');
  
  for (const img of images) {
    const base64Data = img.src;
    try {
      // 将base64转换为File对象
      const file = base64ToFile(base64Data, `image_${Date.now()}.png`);
      
      // 上传文件到服务器
      const imageUrl = await uploadFile(file);
      
      // 替换src为网络URL
      img.src = imageUrl;
    } catch (error) {
      console.error('图片处理失败:', error);
      // 可选:移除失败的图片或保留base64格式
      img.remove();
    }
  }
  
  return doc.documentElement.outerHTML;
};

/**
 * 将base64字符串转换为File对象
 * @param {string} base64 base64字符串
 * @param {string} filename 文件名
 * @returns {File} 文件对象
 */
const base64ToFile = (base64, filename) => {
  const arr = base64.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  
  return new File([u8arr], filename, { type: mime });
};

3.2 集成图片处理的完整流程

js 复制代码
const handleBatchWithImages = async () => {
  try {
    // 1. 获取文件对象
    const file = await readDocxFile();
    
    // 2. 解析为HTML
    const htmlString = await convertToHtml(file);
    
    // 3. 处理图片并获取替换后的HTML
    const processedHtml = await processImages(htmlString);
    
    // 4. 解析为JSON格式
    const targetJson = parseHtml(processedHtml);
    
    // 5. 批量上传
    const result = await batchUploadQuestions(targetJson);
    
    return result;
  } catch (error) {
    console.error('批量处理失败:', error);
    throw error;
  }
};
相关推荐
vivo互联网技术11 分钟前
EMNLP 2025|vivo 等提出 DiMo-GUI:模态分治+动态聚焦,GUI 智能体推理时扩展的新范式
前端·人工智能·agent
本末倒置18328 分钟前
Svelte邪修的JSDoc,到底是个啥?
前端·javascript·面试
李明卫杭州33 分钟前
CSS中的background-clip详解
前端·javascript
林太白39 分钟前
Vite+React+ts项目搭建(十分钟搭建个最新版React19项目吧)
前端·后端·react.js
Mr. Cao code40 分钟前
Nginx与Apache:Web服务器性能大比拼
运维·服务器·前端·nginx·apache
中微子1 小时前
用 ECharts + React 打造环形饼图:从 0 到 1 入门
前端
彭于晏爱编程1 小时前
密码的,YOU不能不知道的Next.jsSSR(服务端渲染)
前端·javascript·react.js
再学一点就睡1 小时前
手撕前端常用 7 种设计模式:从原理到实战,附完整代码案例
前端·设计模式
龙在天1 小时前
vue3如何封装统一的弹窗
前端
一枚前端小能手1 小时前
🚀 应用出了问题你都不知道,别着急我来帮你
前端·监控