富文本编辑器bootstrap-wysiwyg

 

 之前demo相关博客查看的,只有3.x多的版本才有这些图标显示出来。应该是某些属性是用的老的样式,新版的没研究先用这3.x版本的吧。

https://www.bootcdn.cn/font-awesome/

 

https://plugins.jquery.com/hotkeys/

需要引入css:

    <link  th:href="@{/css/bootstrap-combined.no-icons.min.css}" rel="stylesheet"/>
    <link  th:href="@{/bootstrap-4.4.1-dist/css/bootstrap.min.css}" rel="stylesheet" type="text/css" />
    <link  th:href="@{/css/font-awesome.css}" rel="stylesheet"/>
    <link  th:href="@{/css/index.css}" rel="stylesheet"/>

js:

<script    th:src="@{/js/jquery-3.4.1.min.js}">script>

<script    th:src="@{/js/jquery.hotkeys.js}">script>



<script  th:src="@{/js/popper.min.js}">script>
<script    th:src="@{/bootstrap-4.4.1-dist/js/bootstrap.min.js}">script>

<script    th:src="@{/js/bootstrap-wysiwyg-define.js}">script>

整体demo页面html:

  1 DOCTYPE html>
  2 <html lang="en" xmlns:th=http://www.thymeleaf.org>
  3 <head>
  4     <meta charset="UTF-8">
  5     <title>编辑新闻详情页面title>
  6     <link  th:href="@{/css/bootstrap-combined.no-icons.min.css}" rel="stylesheet"/>
  7     <link  th:href="@{/bootstrap-4.4.1-dist/css/bootstrap.min.css}" rel="stylesheet" type="text/css" />
  8     <link  th:href="@{/css/font-awesome.css}" rel="stylesheet"/>
  9     <link  th:href="@{/css/index.css}" rel="stylesheet"/>
 10     <style>
 11 
 12         .editor-container { /* 编辑器容器样式 */
 13 
 14                 padding: 25px 20px 25px;
 15 
 16                 margin-bottom: 10px;
 17 
 18                 background-color: #eeeeee;
 19 
 20                 -webkit-border-radius: 6px;
 21 
 22                 -moz-border-radius: 6px;
 23 
 24                 border-radius: 6px;
 25 
 26         }
 27 
 28          
 29 
 30         .btn-toolbar {
 31 
 32                 font-size: 0;
 33 
 34                 margin-top: 10px;
 35 
 36                 margin-bottom: 10px;
 37 
 38         }
 39 
 40          
 41 
 42         #editor { /* 编辑框样式 */
 43 
 44                 max-height: 400px;
 45 
 46                 height: 400px;
 47 
 48                 width: 100%;
 49 
 50                 background-color: white;
 51 
 52                 border-collapse: separate;
 53 
 54                 border: 1px solid rgb(204, 204, 204);
 55 
 56                 padding: 4px;
 57 
 58                 box-sizing: content-box;
 59 
 60                 -webkit-box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset;
 61 
 62                 box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset;
 63 
 64                 border-top-right-radius: 3px;
 65 
 66                 border-bottom-right-radius: 3px;
 67 
 68                 border-bottom-left-radius: 3px;
 69 
 70                 border-top-left-radius: 3px;
 71 
 72                 overflow: scroll;
 73 
 74                 outline: none;
 75 
 76         }
 77 
 78          
 79 
 80             /* 工具条里按钮样式,这个样式其实是.btn-default的样式,
 81 
 82     Button按钮之所以没有直接加上.btn-default样式是因为按钮选中后wysiwyg会给选中的按钮加.btn-info样式,
 83 
 84     .btn-default和.btn-info同时存在样式会冲突 */
 85             .btn-toolbar .btn {
 86 
 87 
 88                      color: #333;
 89 
 90                      background-color: #fff;
 91 
 92                      border-color: #ccc;
 93 
 94              }
 95 
 96          
 97 
 98             /* 这个样式其实是.btn-info的样式,重写一遍是为了提高优先级,
 99 
100     否则.btn-info的样式会被.btn-toolbar .btn覆盖,这个样式要写在.btn-toolbar .btn之下 */
101         .btn-toolbar .btn-info {
102 
103                 color: #fff;
104 
105                 background-color: #5bc0de;
106 
107                 border-color: #46b8da;
108 
109         }
110 
111     style>
112 head>
113 <body>
114     <div class="btn-toolbar" data-role="editor-toolbar" data-target="#editor">
115         <div class="btn-group">
116             <a class="btn dropdown-toggle" data-toggle="dropdown" title="Font"><i class="icon-font">i><b class="caret">b>a>
117             <ul class="dropdown-menu"> ul>
118         div>
119         <div class="btn-group">
120             <a class="btn dropdown-toggle" data-toggle="dropdown" title="Font Size"><i class="icon-text-height">i> <b class="caret">b>a>
121             <ul class="dropdown-menu">
122                 <li><a data-edit="fontSize 5"><font size="5">Hugefont>a>li>
123                 <li><a data-edit="fontSize 3"><font size="3">Normalfont>a>li>
124                 <li><a data-edit="fontSize 1"><font size="1">Smallfont>a>li>
125             ul>
126         div>
127         <div class="btn-group">
128             <a class="btn" data-edit="bold" title="Bold (Ctrl/Cmd+B)"><i class="icon-bold">i>a> 
129             <a class="btn" data-edit="italic" title="Italic (Ctrl/Cmd+I)"><i class="icon-italic">i>a>
130             <a class="btn" data-edit="strikethrough" title="Strikethrough"><i class="icon-strikethrough">i>a>
131             <a class="btn" data-edit="underline" title="Underline (Ctrl/Cmd+U)"><i class="icon-underline">i>a>
132         div>
133         <div class="btn-group">
134             <a class="btn" data-edit="insertunorderedlist" title="Bullet list"><i class="icon-list-ul">i>a>
135             <a class="btn" data-edit="insertorderedlist" title="Number list"><i class="icon-list-ol">i>a>
136             <a class="btn" data-edit="outdent" title="Reduce indent (Shift+Tab)"><i class="icon-indent-left">i>a>
137             <a class="btn" data-edit="indent" title="Indent (Tab)"><i class="icon-indent-right">i>a>
138         div>
139         <div class="btn-group">
140             <a class="btn" data-edit="justifyleft" title="Align Left (Ctrl/Cmd+L)"><i class="icon-align-left">i>a>
141             <a class="btn" data-edit="justifycenter" title="Center (Ctrl/Cmd+E)"><i class="icon-align-center">i>a>
142             <a class="btn" data-edit="justifyright" title="Align Right (Ctrl/Cmd+R)"><i class="icon-align-right">i>a>
143             <a class="btn" data-edit="justifyfull" title="Justify (Ctrl/Cmd+J)"><i class="icon-align-justify">i>a>
144         div>
145         <div class="btn-group">
146             <a class="btn dropdown-toggle" data-toggle="dropdown" title="Hyperlink"><i class="icon-link">i>a>
147             <div class="dropdown-menu input-append">
148                 <input class="span2" placeholder="URL" type="text" data-edit="createLink"/>
149                 <button class="btn" type="button">Addbutton>
150             div>
151             <a class="btn" data-edit="unlink" title="Remove Hyperlink"><i class="icon-cut">i>a>
152         div>
153         <div class="btn-group">
154             <a class="btn" title="Insert picture (or just drag & drop)" id="pictureBtn"><i class="icon-picture">i>a>
155         <input type="file" id="pictureInput" name="picture" data-role="magic-overlay" data-target="#pictureBtn" data-edit="insertImage" action="/ylxtWeb/news/upload"/>
156         div>
157         <div class="btn-group">
158             <a class="btn" data-edit="undo" title="Undo (Ctrl/Cmd+Z)"><i class="icon-undo">i>a>
159             <a class="btn" data-edit="redo" title="Redo (Ctrl/Cmd+Y)"><i class="icon-repeat">i>a>
160         div>
161         <input type="text" data-edit="inserttext" id="voiceBtn" x-webkit-speech="">
162     div>
163 
164     <div id="editor" contenteditable="true" th:text="${news?.content}">div>
165     <div style="text-align: center;margin-top: 10px;">
166         <button class="btn-info" onclick="cancel()" >返回button>
167         <button class="btn-info" onclick="clea()" style="margin: 0 36px;">清空button>
168         <button class="btn-info" id="confirmBtn" onclick="confir()" th:attr="news_id=${news?.id}" >确定button>
169     div>
170 <form action="" id="myForm" style="display: none;">
171     <input type="text" name="news" th:value="${news}">
172 form>
173 body>
174 <script    th:src="@{/js/jquery-3.4.1.min.js}">script>
175 
176 <script    th:src="@{/js/jquery.hotkeys.js}">script>
177 
178 
179 
180 <script  th:src="@{/js/popper.min.js}">script>
181 <script    th:src="@{/bootstrap-4.4.1-dist/js/bootstrap.min.js}">script>
182 
183 <script    th:src="@{/js/bootstrap-wysiwyg-define.js}">script>
184 
185 <script th:inline="javascript">
186 function cancel(){
187 window.location.href = baseUrl+"/news/toPageNews";
188 }
189 function clea(){
190     console.log(222);
191     //获取清除HTML标签后的内容:
192     $("#editor").empty();
193 }
194 
195 function confir(){
196     //获取富文本编辑器的内容,和获取普通div内容一样。
197     var cont = $("#editor").html();
198     console.log(cont);
199 
200     var news_id = $("#confirmBtn").attr("news_id");
201     var data_news=[[${news}]]
202     data_news.content = cont;
203 var formEle = document.getElementById("myForm");
204 formEle.action = baseUrl+"/news/updateNews";
205 var formData = new FormData(formEle);
206 /*Object.keys(data_news).forEach((key) => {
207 formData.append(key, data_news[key]);
208 });
209 console.log(formData.get("title")+"5555"+formData.get("isDelete"));*/
210             //formEle.submit();
211             //提交表单
212 var xhr = new XMLHttpRequest();
213 xhr.open('PUT', baseUrl+"/news", true);
214 xhr.onload = function () {
215   if (xhr.status !== 200) {
216     console.log('An error occurred!');
217   }
218 };
219 xhr.send(formData);
220 }
221 var baseUrl = "";
222 $(function(){
223         var pathName = window.location.pathname.substring(1);
224         var webName = pathName == '' ? '' : pathName.substring(0, pathName.indexOf('/'));
225         baseUrl = window.location.protocol + '//' + window.location.host+'/' + webName + '/';
226         // 初始化工具条
227         initToolbarBootstrapBindings();
228         $('#editor').wysiwyg();
229     });
230 
231     // 初始化工具条
232     function initToolbarBootstrapBindings() {
233         // 字体样式
234         var fonts = [ 'Serif', 'Sans', 'Arial', 'Arial Black', 'Courier',
235                 'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact',
236                 'Lucida Grande', 'Lucida Sans', 'Tahoma', 'Times',
237                 'Times New Roman', 'Verdana' ],
238             fontTarget = $('[title=Font]').siblings('.dropdown-menu');
239         $.each(fonts,function(idx, fontName) {
240             fontTarget.append($('
  • ' + fontName  241                     +'" style="font-family:\''+ fontName +'\'">' + fontName + '
  • ')); 242         }); 243 244         $('button[title]').tooltip({ 245             container : 'body' 246         }); 247 248         // .dropdown-menu下的input事件 249         $('.dropdown-menu input').click(function() { 250             return false; 251         }) 252         .change(function() { 253             $(this).parent('.dropdown-menu').siblings('.dropdown-toggle').dropdown('toggle'); 254         }) 255         .keydown('esc', function() { 256             this.value = ''; 257             $(this).change(); 258         }); 259 260         // [data-role=magic-overlay]的样式 261         $('[data-role=magic-overlay]').each(function() { 262             var overlay = $(this), target = $(overlay.data('target')); 263             overlay.css('opacity', 0).css('position', 'absolute') 264                     .offset(target.offset()).width(target.outerWidth()) 265                     .height(target.outerHeight()); 266         }); 267     }; 268 269 script> 270 html>
    View Code

    查了好多博客里边的demo的图片都是没有上传到服务器的,需要修改bootstrap-wysiwyg.js文件,原理为:选中图片后就上传到服务器,然后服务端返回图片访问链接,前端把链接写入img标签的src属性内,并放到编辑框内,

    修改后的bootstrap-wysiwyg.js文件,更名为bootstrap-wysiwyg-define.js引入了thymeleaf模板页面。

    /* http://github.com/mindmup/bootstrap-wysiwyg */
    /*global jQuery, $, FileReader*/
    /*jslint browser:true*/
    (function ($) {
        'use strict';
        /*转码图片*/
        var readFileIntoDataUrl = function (fileInfo) {
            var loader = $.Deferred(),  //jq延迟对象
                fReader = new FileReader();
            fReader.onload = function (e) {
                loader.resolve(e.target.result);
            };
            fReader.onerror = loader.reject; //拒绝
            fReader.onprogress = loader.notify;
            fReader.readAsDataURL(fileInfo); //转码图片
            return loader.promise();  //返回promise对象,观察某种类型被绑定到集合的所有行动,是否已被加入到队列中。
        };
        //一、修改原readFileIntoDataUrl方法
            /*var readFileIntoDataUrl = function (fileInfo) {
                var loader = $.Deferred(),
                    fReader = new FileReader(),
                    img = '';
                fReader.onload = function (e) {
                    img = e.target.result;
                    $.ajax({
                        url: ctxPath+'news/upload',
                        type: 'post',
                        async: false,
                        dataType: 'json',
                        data: {"file":fileInfo},
                        success: function(data){
                         if(data.responseCode == 1){
                        loader.resolve(服务器返回的图片的地址);
                          }else if(data.responseCode == 0){
                              alert('上传失败');
                          }
                        }
                    });
                };
                fReader.onerror = loader.reject;
                fReader.onprogress = loader.notify;
                fReader.readAsDataURL(fileInfo);
                return loader.promise();
            };*/
    var uploadFileToServer = function(id, action, callback) {
     var fileObj = document.getElementById("pictureInput").files[0]; // js 获取文件对象
    
                   var tokenv="ssssssss";
    
                   //var data = {"id":id,"picture":fileObj};
                var formData = new FormData();//document.getElementById("myForm")
                //formData.append("picture",$("#pictureInput")[0].files[0]);
                formData.append("file",fileObj);
                formData.append("token",tokenv);
    console.log(formData);
     $.ajax({
      type: 'post',
      url : action,
      cache: false,
      data: formData,
      processData: false,//如果processData不設置為false,jquery會把formData轉換為字符串。
      contentType : false,//不设置是:application/x-www-form-urlencoded;
      dataType : 'json',
      success : function(obj) {
      if (obj.status) {
       callback(obj.imgsrc);
      } else
       options.fileUploadError("server-internal-exception",
        obj.message);
      },
      error : function(e) {
      console.log(e);
      //options.fileUploadErroe("upload-failure", "");
      }
     });}
        /*清空内容*/
        $.fn.cleanHtml = function () {
            var html = $(this).html();
            return html && html.replace(/(
    |\s|

    <\/div>| )*$/, ''); }; $.fn.wysiwyg = function (userOptions) { var editor = this, //设置ui-jq='设置的插件别名的dom元素'(此句注释可忽略,是针对我的项目结构写的) selectedRange, options, toolbarBtnSelector, //更新工具栏 updateToolbar = function () { if (options.activeToolbarClass) { $(options.toolbarSelector).find(toolbarBtnSelector).each(function () { var command = $(this).data(options.commandRole); //判断光标所在位置以确定命令的状态,为真则显示为激活 if (document.queryCommandState(command)) { $(this).addClass(options.activeToolbarClass); } else { $(this).removeClass(options.activeToolbarClass); } }); } }, //插入内容 execCommand = function (commandWithArgs, valueArg) { var commandArr = commandWithArgs.split(' '), command = commandArr.shift(), args = commandArr.join(' ') + (valueArg || ''); document.execCommand(command, 0, args); updateToolbar(); }, //用jquery.hotkeys.js插件监听键盘 bindHotkeys = function (hotKeys) { $.each(hotKeys, function (hotkey, command) { editor.keydown(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); execCommand(command); } }).keyup(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); } }); }); }, //获取当前range对象 getCurrentRange = function () { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); //从当前selection对象中获得一个range对象。 } }, //保存 saveSelection = function () { selectedRange = getCurrentRange(); }, //恢复 restoreSelection = function () { var selection = window.getSelection(); //获取当前既获区,selection是对当前激活选中区(即高亮文本)进行操作 if (selectedRange) { try { //移除selection中所有的range对象,执行后anchorNode、focusNode被设置为null,不存在任何被选中的内容。 selection.removeAllRanges(); } catch (ex) { document.body.createTextRange().select(); document.selection.empty(); } //将range添加到selection当中,所以一个selection中可以有多个range。 //注意Chrome不允许同时存在多个range,它的处理方式和Firefox有些不同。 selection.addRange(selectedRange); } }, //插入文件(这里指图片) //将insertFiles方法作改写如下: insertFiles = function (files, id, action) { editor.focus(); //遍历插入(应为可以多文件插入) $.each(files, function (idx, fileInfo) { //只可插入图片文件 if (/^image\//.test(fileInfo.type)) { //转码图片 /* * $.when(readFileIntoDataUrl(fileInfo)).done(function(dataUrl) { * execCommand('insertimage', dataUrl); }).fail(function(e) { * options.fileUploadError("file-reader", e); }); */ uploadFileToServer(id, action, function(src) { execCommand('insertimage', src); }); } else { //非图片文件会调用config的错误函数 options.fileUploadError("unsupported-file-type", fileInfo.type); } }); }, //TODO 暂不了解用意 markSelection = function (input, color) { restoreSelection(); //确定命令是否被支持,返回true或false if (document.queryCommandSupported('hiliteColor')) { document.execCommand('hiliteColor', 0, color || 'transparent'); } saveSelection(); input.data(options.selectionMarker, color); }, //绑定工具栏相应工具事件 bindToolbar = function (toolbar, options) { //给所有工具栏上的控件绑定点击事件 toolbar.find(toolbarBtnSelector).click(function () { restoreSelection(); editor.focus(); //获取焦点 //设置相应配置的工具execCommand execCommand($(this).data(options.commandRole)); //保存 saveSelection(); }); //对[data-toggle=dropdown]进行单独绑定点击事件处理 字体大小 toolbar.find('[data-toggle=dropdown]').click(restoreSelection); //对input控件进行单独处理,webkitspeechchange为语音事件 toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () { var newValue = this.value; //获取input 的value this.value = ''; //清空value防止冲突 restoreSelection(); if (newValue) { editor.focus();//获取焦点 //设置相应配置的工具execCommand execCommand($(this).data(options.commandRole), newValue); } saveSelection(); }).on('focus', function () { //获取焦点 var input = $(this); if (!input.data(options.selectionMarker)) { markSelection(input, options.selectionColor); input.focus(); } }).on('blur', function () { //失去焦点 var input = $(this); if (input.data(options.selectionMarker)) { markSelection(input, false); } }); //图片按钮添加了监听器事件 toolbar.find('input[type=file][data-' + options.commandRole + ']') .change( function() { restoreSelection(); if (this.type === 'file' && this.files && this.files.length > 0) { insertFiles(this.files, $(this).attr('id'), $(this).attr('action')); } saveSelection(); this.value = ''; }); }, //初始化拖放事件 initFileDrops = function () { editor.on('dragenter dragover', false) .on('drop', function (e) { var dataTransfer = e.originalEvent.dataTransfer; e.stopPropagation(); e.preventDefault(); if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { insertFiles(dataTransfer.files); } }); }; //合并传入的配置对象userOptions和默认的配置对象config options = $.extend({}, $.fn.wysiwyg.defaults, userOptions); //设置查找字符串:a[data-edit] button[data-edit] input[type=button][data-edit] toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + ']'; //设置热键 容器有[data-role=editor-toolbar]属性的dom元素 bindHotkeys(options.hotKeys); //是否允许拖放 允许则配置拖放 if (options.dragAndDropImages) {initFileDrops();} //配置工具栏 bindToolbar($(options.toolbarSelector), options); //设置编辑区域为可编辑状态并绑定事件mouseup keyup mouseout editor.attr('contenteditable', true) .on('mouseup keyup mouseout', function () { saveSelection(); updateToolbar(); }); //编辑区域绑定图片点击事件 //TODO 这是我自己添加的,因为有时要对图片进行一些操作 editor.on('mousedown','img', function (e) { e.preventDefault(); }).on('click', 'img', function (e) { var $img = $(e.currentTarget); console.log($img); e.preventDefault(); e.stopPropagation(); }); //window绑定touchend事件 $(window).bind('touchend', function (e) { var isInside = (editor.is(e.target) || editor.has(e.target).length > 0), currentRange = getCurrentRange(), clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); if (!clear || isInside) { saveSelection(); updateToolbar(); } }); return this; }; //配置参数 $.fn.wysiwyg.defaults = { hotKeys: { //热键 应用hotkeys.js jquery插件 'ctrl+b meta+b': 'bold', 'ctrl+i meta+i': 'italic', 'ctrl+u meta+u': 'underline', 'ctrl+z meta+z': 'undo', 'ctrl+y meta+y meta+shift+z': 'redo', 'ctrl+l meta+l': 'justifyleft', 'ctrl+r meta+r': 'justifyright', 'ctrl+e meta+e': 'justifycenter', 'ctrl+j meta+j': 'justifyfull', 'shift+tab': 'outdent', 'tab': 'indent' }, toolbarSelector: '[data-role=editor-toolbar]', commandRole: 'edit', activeToolbarClass: 'btn-info', selectionMarker: 'edit-focus-marker', selectionColor: 'darkgrey', dragAndDropImages: true, //是否支持拖放,默认为支持 fileUploadError: function (reason, detail) { console.log("File upload error", reason, detail); } }; }(window.jQuery));

    测试图片上传成功:

    富文本编辑器bootstrap-wysiwyg_第1张图片

     

     后台Java接收图片方法:

       /**
         *
         * @param file  MultipartFile 这个类一般是用来接受前台传过来的文件
         * @param request
         * @return
         */
        @ResponseBody
        @RequestMapping(value = "/upload",method = RequestMethod.POST)
        public Map multiUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
            Map res = new HashMap();
            try {
                String scheme = request.getScheme();//http
                String serverName = request.getServerName();//localhost
                int serverPort = request.getServerPort();//8080
                String contextPath = request.getContextPath();//项目名
                String url = scheme+"://"+serverName+":"+serverPort+contextPath;//http://127.0.0.1:8080/test
    
                LOGGER.info("url:"+url);
                String imgUrl = "";
                //String imgUrl=imgPath.substring(0,imgPath.length()-2);//截取到的是"/static/",如果配置访问路径的话
                String path = request.getContextPath();
                String intactPath =null;
                String filename=file.getOriginalFilename();
                String imgDir = imgServerPath.substring(imgServerPath.indexOf("file:") + 5, imgServerPath.length());
                File file2 = new File(imgDir, filename);
                LOGGER.info("imgDir:"+imgDir);
                //得到原始文件的输入流
                InputStream inputStream=file.getInputStream();
                FileOutputStream fileOutputStream=new FileOutputStream(file2);
                //写出文件
                IOUtils.copy(inputStream,fileOutputStream);
                inputStream.close();
                fileOutputStream.close();
                if(!file.isEmpty()){
                    imgUrl = url+File.separator + file.getOriginalFilename();
                    System.out.println(intactPath);
                    int pre = (int) System.currentTimeMillis();
    
                }
                res.put("status","succ");
                res.put("imgsrc",imgUrl);
            } catch (Exception e) {
                res.put("status","error");
                res.put("msg",e.getMessage());
                e.printStackTrace();
                LOGGER.info("multiUpload方法异常!",e);
                System.out.println(e.getMessage());
            }
            return res;
        }

    参考链接:

    https://www.jb51.net/article/85252.htm

    你可能感兴趣的:(富文本编辑器bootstrap-wysiwyg)