前端批量导入内容——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;
  }
};
相关推荐
人工智能训练师1 小时前
Ubuntu22.04如何安装新版本的Node.js和npm
linux·运维·前端·人工智能·ubuntu·npm·node.js
Seveny071 小时前
pnpm相对于npm,yarn的优势
前端·npm·node.js
yddddddy2 小时前
css的基本知识
前端·css
昔人'2 小时前
css `lh`单位
前端·css
Nan_Shu_6144 小时前
Web前端面试题(2)
前端
知识分享小能手4 小时前
React学习教程,从入门到精通,React 组件核心语法知识点详解(类组件体系)(19)
前端·javascript·vue.js·学习·react.js·react·anti-design-vue
蚂蚁RichLab前端团队5 小时前
🚀🚀🚀 RichLab - 花呗前端团队招贤纳士 - 【转岗/内推/社招】
前端·javascript·人工智能
孩子 你要相信光5 小时前
css之一个元素可以同时应用多个动画效果
前端·css
huangql5205 小时前
npm 发布流程——从创建组件到发布到 npm 仓库
前端·npm·node.js
Days20506 小时前
LeaferJS好用的 Canvas 引擎
前端·开源