富文本编辑器(wangeditor)导入附件

1、问题描述

之前系统接入了一个富文本,采用的wangeditor。本来用着挺好,但是需求无止境。copy单个图片,可以黏贴进去。单个图片+文字,不行。

复制到mac备忘录,可以复制;

复制到企微邮箱,提示需要授权;

猜测:单个图片,大小可控,允许;多个图片,大小不可控。临时存储到本地,但此时涉及到授权问题,是否有权限获取本地目录。而此目录和使用何种office、系统都有关系。

加之,浏览器自身的安全,总不能随意加载任意的目录。

2、前端整理

原因:由于安全问题浏览器不允许粘贴 file://文件路径的图片

但前端也提供了一个可行的思路,可以自定义一个按钮,进行导入。

3、解决思路

尝试检索该问题,共同的提到(mammoth.js)

Mammoth.js 是一个开源的 JavaScript 库,主要用于将 Microsoft Word 文档(.docx 文件)转换为 HTML、Markdown 或其他文本格式。它的核心目标是通过文档中的语义信息生成简洁、干净的 HTML,而不是完全复制文档的样式。

主要功能

文档转换:将 Word 文档转换为 HTML 或 Markdown 格式,支持标题、列表、表格、图片、链接等多种元素。

样式映射:支持自定义样式映射,可以将 Word 中的样式映射为 HTML 的样式。

在线预览:转换后的 HTML 可以嵌入网页中,实现 Word 文档的在线预览。

灵活配置:提供多种配置选项,例如在转换前对文档进行预处理。

使用场景

在网页中展示 Word 文档内容,例如用户协议、文档预览等。

将 Word 文档转换为 Markdown 或纯文本格式。

在前端项目中实现文档上传和内容展示。

优势

简洁的 HTML 输出:生成的 HTML 结构清晰,便于进一步处理。

跨平台支持:可以在浏览器和 Node.js 环境中使用。

开源且灵活:基于 BSD-2-Clause 许可协议开源,开发者可以根据需要进行定制。


Mammoth.js 是一个高效且实用的工具,特别适合需要将 Word 文档内容转换为网页格式的场景。

于是,尝试集成到我们的项目中去。接下来,主要的问题可能是:

  • wangeditor如何自定义按钮并实现内容回显
  • mammoth使用样例

官网扩展菜单样例:

wangEditor extend modal menu

4、代码实现

1、引入js

<script charset="utf-8" src="/wangeditor/mammoth.browser.min.js"></script>

下载链接,带走不谢。

https://cdn.jsdelivr.net/npm/mammoth@1.4.8/mammoth.browser.min.js

2、参考官网实现按钮自定义

const toolbarConfig = {
    excludeKeys:[
        'fullScreen',
        'emotion',
        'insertImage',
        'insertVideo'
    ],
    insertKeys: {
        index: 0, // 插入的位置,基于当前的 toolbarKeys
        keys: ['myMenu'],
    }
}

// Extend menu
class MyMenu {
    constructor() {
        this.title = '导入'
        // this.iconSvg = '<svg >...</svg>'
        this.tag = 'button'
        this.showModal = true
        this.modalWidth = 300
    }
    getValue(editor) {
        return ''
    }
    isActive(editor) {
        return false // or true
    }
    isDisabled(editor) {
        return false // or true
    }
    exec(editor, value) {
        // do nothing 什么都不用做
    }
    getModalPositionNode(editor) {
        return null // modal 依据选区定位
    }
    getModalContentElem(editor) {

        const $container = $('<div></div>')
        const inputId = `input-${Math.random().toString(16).slice(-8)}`
        const buttonId = `button-${Math.random().toString(16).slice(-8)}`

        window.btnFn = () => {
            // e.preventDefault()
            // const text = $(`#${inputId}`).val();
            // if (!text) return;

            convertWord(inputId);

            editor.restoreSelection(); // 恢复选区
            // editor.insertText(text);
            // editor.insertText(' ');
        }
        const $inputContainer = $(`<label class="babel-container">
        <span>导入附件</span>
        <input type="file" id="${inputId}">
      </label>`)
        const $buttonContainer = $(`<div class="button-container">
        <button id="${buttonId}" onclick="btnFn()">保存</button>
      </div>`)

        $container.append($inputContainer).append($buttonContainer)
        setTimeout(() => {
            $(`#${inputId}`).focus()
        })

        return $container[0]
    }
}
const myMenuConf = {
    key: 'myMenu',
    factory() {
        return new MyMenu()
    }
}
window.wangEditor.Boot.registerMenu(myMenuConf);

3、使用mammoth

/**
 * 转换为html 并设置到文本框
 * @param inputId
 */
function convertWord(inputId) {
    const input = document.getElementById(inputId);
    const file = input.files[0];
    if(!notNull(file)){
        Message.info('附件为空');
        return;
    }

    const reader = new FileReader();
    reader.onload = (evt) => {
        mammoth.convertToHtml({ arrayBuffer: evt.target.result })
            .then((result) => {
                const html = result.value;
                editor.dangerouslyInsertHtml(html); // 将 HTML 插入到编辑器中
            });
    };
    reader.readAsArrayBuffer(file);
}

注,网站示例是参入文本,使用:editor.insertText(text),而此处是使用:editor.dangerouslyInsertHtml(html);

具体使用,可参考:编辑器 API | wangEditor

4、验证保存后,是否回显是否正常

5、遇到的问题

5.1、click事件无法绑定

getModalContentElem(editor) {
    const $container = $('<div></div>')

    const inputId = `input-${Math.random().toString(16).slice(-8)}`
    const buttonId = `button-${Math.random().toString(16).slice(-8)}`

    const $inputContainer = $(`<label class="babel-container">
        <span>Text</span>
        <input type="text" id="${inputId}" value="hello world">
      </label>`)
    const $buttonContainer = $(`<div class="button-container">
        <button id="${buttonId}">insert text</button>
      </div>`)

    $container.append($inputContainer).append($buttonContainer)

    $container.on('click', `#${buttonId}`, e => {
      e.preventDefault()

      const text = $(`#${inputId}`).val()
      if (!text) return

      editor.restoreSelection() // 恢复选区
      editor.insertText(text)
      editor.insertText(' ')
    })

    setTimeout(() => {
      $(`#${inputId}`).focus()
    })

    return $container[0]

    // PS:也可以把 $container 缓存下来,这样不用每次重复创建、重复绑定事件,优化性能
  }
}

5.2、Form is larger than max length 200000 idea jetty

2025-01-20 10:56:24.905 ERROR [qtp517268856-136]com.chaboshi.passport.client.interceptors.ClientCertificationInterceptor(123) | 权限拦截器发生异常
org.eclipse.jetty.http.BadMessageException: 400: Unable to parse form content
    at org.eclipse.jetty.server.Request.getParameters(Request.java:434)
    at org.eclipse.jetty.server.Request.getParameter(Request.java:1059)
    at com.chaboshi.passport.common.utils.URLParamHelper.getString(URLParamHelper.java:22)
    at com.chaboshi.passport.client.interceptors.ClientCertificationInterceptor.before(ClientCertificationInterceptor.java:41)
    at com.chaboshi.wf.mvc.InterceptorHandler.excuteActionBeforeInterceptors(InterceptorHandler.java:61)
    at com.chaboshi.wf.mvc.action.MethodAction.invoke(MethodAction.java:191)
    at com.chaboshi.wf.mvc.Dispatcher.service(Dispatcher.java:34)
    at com.chaboshi.wf.mvc.WFBootstrap.doFilter(WFBootstrap.java:64)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618)
    at com.chaboshi.web.qa.filter.DMaskingFilter.doFilter(DMaskingFilter.java:93)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:549)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:602)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1610)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1369)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:489)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1580)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1284)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:191)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
    at org.eclipse.jetty.server.Server.handle(Server.java:501)
    at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)
    at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:556)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:272)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:375)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.IllegalStateException: Form is larger than max length 200000
    at org.eclipse.jetty.server.Request.extractFormParameters(Request.java:562)
    at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:519)
    at org.eclipse.jetty.server.Request.getParameters(Request.java:430)
    ... 43 more
2025-01-20 10:56:24.911 INFO  [qtp517268856-136]STATS(28) | 1Min average time: 1078, qps: 0.15, concurrent: 0.17, total time: 19408ms, request count: 18.
2025-01-20 10:57:09.019 INFO  [statis]com.chaboshi.wf.mvc.toolbox.metrics.MetricsStatis(46) | HeapUsage : 27.05%, HeapUsed : 984.00M, HeapMax : 3641.00M

Caused by: java.lang.IllegalStateException: Form is larger than max length 200000 idea jetty 启动解决-CSDN博客

5.3、调整数据库长度

org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'article_content' at row 1
### The error may involve com.chaboshi.web.qa.dao.detection.detectionTemplateV2.DetectionV2ComponentLocationPictureStoreDao.saveOrUpdateAll-Inline
### The error occurred while setting parameters
### SQL: UPDATE t_detection_v2_component_location_picture_store SET article_title = ?, add_time = ?, model_id = ?, article_content = ?, update_time = ?, pic = ? WHERE ( id = ?)
### Cause: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'article_content' at row 1


alter table xx
    modify article_content longtext null comment '调整为 longtext |20250120|XX。';

5.4、待定.......(估计还有问题)

6、其他扩展

在 MySQL 中,TEXT 类型是一组用于存储变长文本数据的数据类型,根据存储容量的不同,分为 TINYTEXTTEXTMEDIUMTEXTLONGTEXT。以下是每种类型的特点和最大存储容量:

  1. TINYTEXT
  • 最大存储容量:255 字节(2^8 - 1)
  • 适用场景:存储非常短的文本内容,如标签、简短的备注等。
  • 特点:占用空间小,适合存储少量文本。
  1. TEXT
  • 最大存储容量:65,535 字节(2^16 - 1)
  • 适用场景:存储中等长度的文本内容,如新闻摘要、用户评论等。
  • 特点:适合大多数文本存储需求,但不适合存储非常大的文本。
  1. MEDIUMTEXT
  • 最大存储容量:16,777,215 字节(2^24 - 1,约 16MB)
  • 适用场景:存储较大的文本内容,如文章、产品说明书、HTML 页面等。
  • 特点:适合存储中等长度的文本,但需要注意性能优化。
  1. LONGTEXT
  • 最大存储容量:4,294,967,295 字节(2^32 - 1,约 4GB)
  • 适用场景:存储超大文本内容,如日志文件、富文本内容、HTML 页面等。
  • 特点:适合存储非常大的文本,但可能会影响性能。

字符集对存储容量的影响

存储容量不仅取决于数据类型,还与字符集有关。以下是不同字符集下的最大字符数量:

  • UTF-8
    • 每个字符最多占用 3 字节。
    • TEXT 最多存储约 21,845 个字符。
    • MEDIUMTEXT 最多存储约 5,592,405 个字符。
    • LONGTEXT 最多存储约 1,431,655,765 个字符。
  • UTF-8MB4(支持表情符号):
    • 每个字符最多占用 4 字节。
    • TEXT 最多存储约 16,383 个字符。
    • MEDIUMTEXT 最多存储约 4,194,303 个字符。
    • LONGTEXT 最多存储约 1,073,741,823 个字符。
  • latin1(单字节字符集):
    • 每个字符占用 1 字节。
    • TEXT 最多存储 65,535 个字符。
    • MEDIUMTEXT 最多存储 16,777,215 个字符。
    • LONGTEXT 最多存储 4,294,967,295 个字符。

选择合适的 TEXT 类型

在选择 TEXT 类型时,应根据实际需求选择合适的数据类型:

  • 如果文本内容较短(如标签、简短备注),使用 TINYTEXT
  • 如果文本内容中等长度(如用户评论、新闻摘要),使用 TEXT
  • 如果文本内容较大(如文章、HTML 页面),使用 MEDIUMTEXT
  • 如果文本内容非常大(如日志文件、富文本内容),使用 LONGTEXT
相关推荐
凡大来啦4 分钟前
Axios发起HTTP请求时的先后执行顺序
前端·javascript·http
傻小胖21 分钟前
react中hooks之 React 19 新 Hooks useActionState & useFormStatus用法总结
前端·react.js·前端框架
ᥬ 小月亮21 分钟前
Js:DOM中的样式(包含行内样式、滚动样式、可见区域样式等)
开发语言·javascript·ecmascript
16年上任的CTO28 分钟前
一文大白话讲清楚webpack基本使用——4——vue-loader的配置和使用
前端·javascript·webpack·ecmascript·vue-loader·vueloaderplugin
软件工程师文艺2 小时前
使用HTML5 Canvas 实现呼吸粒子球动画效果的原理
前端·javascript·html·html5
不在··3 小时前
Axios HTTP库基础教程:从安装到GET与POST请求的实现
前端·javascript·vue.js
少油少盐不要辣9 小时前
js截取video视频某一帧为图片
javascript·音视频
zqwang88810 小时前
IOS 安全机制拦截 window.open
前端·javascript
夏天想12 小时前
element-plus中的table为什么相同的数据并没有合并成一个
javascript·vue.js·elementui
清风细雨_林木木12 小时前
Skeleton 骨架屏
javascript·vue.js·ecmascript