我们选择多张图片首先要改造input支持multiple多选,然后服务端支持批量接收图片,beego框架有解决方案,但是beego的文档中没有写,要去到源码里看。

1 mindoc documentcontroller.go

beego的getfile和getfiles不同,在beego的controller.go中查阅getfile方法,其下方有getfiles的方式,特别注意的是,不能用savetofile方法了,因为savetofile只是对第一个文件进行保存。必须用它代码里提供的例子:io.copy方法。

// 上传附件或图片
func (c *DocumentController) Upload() {
    identify := c.GetString("identify")
    docId, _ := c.GetInt("doc_id")
    isAttach := true

    if identify == "" {
        c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error"))
    }

    name := "editormd-file-file"

    // file, moreFile, err := c.GetFile(name)
    // if err == http.ErrMissingFile || moreFile == nil {
    //     name = "editormd-image-file"
    //     file, moreFile, err = c.GetFile(name)
    //     if err == http.ErrMissingFile || moreFile == nil {
    //         c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty"))
    //         return
    //     }
    // }
    // ****3xxx
    files, err := c.GetFiles(name)
    if err == http.ErrMissingFile {
        name = "editormd-image-file"
        files, err = c.GetFiles(name)
        if err == http.ErrMissingFile {
            c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty"))
            return
        }
    }

    // if err != nil {
    //     http.Error(w, err.Error(), http.StatusNoContent)
    //     return
    // }
    // jMap := make(map[string]interface{})
    // s := []map[int]interface{}{}
    result2 := []map[string]interface{}{}
    for i, _ := range files {
        //for each fileheader, get a handle to the actual file
        file, err := files[i].Open()

        defer file.Close()
        // if err != nil {
        //     http.Error(w, err.Error(), http.StatusInternalServerError)
        //     return
        // }
        //     //create destination file making sure the path is writeable.
        //     dst, err := os.Create("upload/" + files[i].Filename)
        //     defer dst.Close()
        //     if err != nil {
        //         http.Error(w, err.Error(), http.StatusInternalServerError)
        //         return
        //     }
        //     //copy the uploaded file to the destination file
        //     if _, err := io.Copy(dst, file); err != nil {
        //         http.Error(w, err.Error(), http.StatusInternalServerError)
        //         return
        //     }
        // }
        // ****

        if err != nil {
            c.JsonResult(6002, err.Error())
        }

        // defer file.Close()

        type Size interface {
            Size() int64
        }

        // if conf.GetUploadFileSize() > 0 && moreFile.Size > conf.GetUploadFileSize() {
        if conf.GetUploadFileSize() > 0 && files[i].Size > conf.GetUploadFileSize() {
            c.JsonResult(6009, i18n.Tr(c.Lang, "message.upload_file_size_limit"))
        }

        // ext := filepath.Ext(moreFile.Filename)
        ext := filepath.Ext(files[i].Filename)
        //文件必须带有后缀名
        if ext == "" {
            c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_type_error"))
        }
        //如果文件类型设置为 * 标识不限制文件类型
        if conf.IsAllowUploadFileExt(ext) == false {
            c.JsonResult(6004, i18n.Tr(c.Lang, "message.upload_file_type_error"))
        }

        bookId := 0

        // 如果是超级管理员,则不判断权限
        if c.Member.IsAdministrator() {
            book, err := models.NewBook().FindByFieldFirst("identify", identify)

            if err != nil {
                c.JsonResult(6006, i18n.Tr(c.Lang, "message.doc_not_exist_or_no_permit"))
            }

            bookId = book.BookId
        } else {
            book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId)

            if err != nil {
                logs.Error("DocumentController.Edit => ", err)
                if err == orm.ErrNoRows {
                    c.JsonResult(6006, i18n.Tr(c.Lang, "message.no_permission"))
                }

                c.JsonResult(6001, err.Error())
            }

            // 如果没有编辑权限
            if book.RoleId != conf.BookEditor && book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder {
                c.JsonResult(6006, i18n.Tr(c.Lang, "message.no_permission"))
            }

            bookId = book.BookId
        }

        if docId > 0 {
            doc, err := models.NewDocument().Find(docId)
            if err != nil {
                c.JsonResult(6007, i18n.Tr(c.Lang, "message.doc_not_exist"))
            }

            if doc.BookId != bookId {
                c.JsonResult(6008, i18n.Tr(c.Lang, "message.doc_not_belong_project"))
            }
        }

        fileName := "m_" + cryptil.UniqueId() + "_r"
        filePath := filepath.Join(conf.WorkingDirectory, "uploads", identify)

        //将图片和文件分开存放
        // if filetil.IsImageExt(moreFile.Filename) {
        if filetil.IsImageExt(files[i].Filename) {
            filePath = filepath.Join(filePath, "images", fileName+ext)
        } else {
            filePath = filepath.Join(filePath, "files", fileName+ext)
        }

        path := filepath.Dir(filePath)

        _ = os.MkdirAll(path, os.ModePerm)

        // err = c.SaveToFile(name, filePath) // frome beego controller.go: savetofile it only operates the first one of mutil-upload form file field.

        //copy the uploaded file to the destination file
        dst, err := os.Create(filePath)
        defer dst.Close()
        if _, err := io.Copy(dst, file); err != nil {
            logs.Error("保存文件失败 -> ", err)
            c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed"))
        }

        // if err != nil {
        //     logs.Error("保存文件失败 -> ", err)
        //     c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed"))
        // }

        attachment := models.NewAttachment()
        attachment.BookId = bookId
        // attachment.FileName = moreFile.Filename
        attachment.FileName = files[i].Filename
        attachment.CreateAt = c.Member.MemberId
        attachment.FileExt = ext
        attachment.FilePath = strings.TrimPrefix(filePath, conf.WorkingDirectory)
        attachment.DocumentId = docId

        if fileInfo, err := os.Stat(filePath); err == nil {
            attachment.FileSize = float64(fileInfo.Size())
        }

        if docId > 0 {
            attachment.DocumentId = docId
        }

        // if filetil.IsImageExt(moreFile.Filename) {
        if filetil.IsImageExt(files[i].Filename) {
            attachment.HttpPath = "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1)
            if strings.HasPrefix(attachment.HttpPath, "//") {
                attachment.HttpPath = conf.URLForWithCdnImage(string(attachment.HttpPath[1:]))
            }

            isAttach = false
        }

        err = attachment.Insert()

        if err != nil {
            os.Remove(filePath)
            logs.Error("文件保存失败 ->", err)
            c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed"))
        }

        if attachment.HttpPath == "" {
            attachment.HttpPath = conf.URLForNotHost("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId)

            if err := attachment.Update(); err != nil {
                logs.Error("保存文件失败 ->", err)
                c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed"))
            }
        }
        result := map[string]interface{}{
            "errcode":   0,
            "success":   1,
            "message":   "ok",
            "url":       attachment.HttpPath,
            "alt":       attachment.FileName,
            "is_attach": isAttach,
            "attach":    attachment,
        }
        result2 = append(result2, result)
    }

    c.Ctx.Output.JSON(result2, true, false)
    c.StopRun()
}

image-dialog.js

位置在mindoc\static\editor.md\plugins\image-dialog\image-dialog.js

/*!
 * Image (upload) dialog plugin for Editor.md
 *
 * @file        image-dialog.js
 * @author      pandao
 * @version     1.3.4
 * @updateTime  2015-06-09
 * {@link       https://github.com/pandao/editor.md}
 * @license     MIT
 */
(function() {
  var factory = function(exports) {
    var pluginName = "image-dialog";
    exports.fn.imageDialog = function() {
      var _this = this;
      var cm = this.cm;
      var lang = this.lang;
      var editor = this.editor;
      var settings = this.settings;
      var cursor = cm.getCursor();
      var selection = cm.getSelection();
      var imageLang = lang.dialog.image;
      var classPrefix = this.classPrefix;
      var iframeName = classPrefix + "image-iframe";
      var dialogName = classPrefix + pluginName,
        dialog;

      cm.focus();

      var loading = function(show) {
        var _loading = dialog.find("." + classPrefix + "dialog-mask");
        _loading[(show) ? "show" : "hide"]();
      };

      if (editor.find("." + dialogName).length < 1) {
        var guid = (new Date).getTime();
        var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;

        if (settings.crossDomainUpload) {
          action += "&callback=" + settings.uploadCallbackURL + "&dialog_id=editormd-image-dialog-" + guid;
        }

        var dialogContent = ((settings.imageUpload) ? "<form action=\"" + action + "\" target=\"" + iframeName + "\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\">" : "<div class=\"" + classPrefix + "form\">") +
          ((settings.imageUpload) ? "<iframe name=\"" + iframeName + "\" id=\"" + iframeName + "\" guid=\"" + guid + "\"></iframe>" : "") +
          "<label>" + imageLang.url + "</label>" +
          "<input type=\"text\" data-url />" + (function() {
            return (settings.imageUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
              // 3xxx 下行添加multiple=\"multiple\"
              "<input type=\"file\" name=\"" + classPrefix + "image-file\" accept=\"image/jpeg,image/png,image/gif,image/jpg\" multiple=\"multiple\" />" +
              "<input type=\"submit\" value=\"" + imageLang.uploadButton + "\" />" +
              "</div>" : "";
          })() +
          "<br/>" +
          "<label>" + imageLang.alt + "</label>" +
          "<input type=\"text\" value=\"" + selection + "\" data-alt />" +
          "<br/>" +
          "<label>" + imageLang.link + "</label>" +
          "<input type=\"text\" value=\"http://\" data-link />" +
          "<br/>" +
          ((settings.imageUpload) ? "</form>" : "</div>");
        //var imageFooterHTML = "<button class=\"" + classPrefix + "btn " + classPrefix + "image-manager-btn\" style=\"float:left;\">" + imageLang.managerButton + "</button>";
        dialog = this.createDialog({
          title: imageLang.title,
          width: (settings.imageUpload) ? 465 : 380,
          height: 254,
          name: dialogName,
          content: dialogContent,
          mask: settings.dialogShowMask,
          drag: settings.dialogDraggable,
          lockScreen: settings.dialogLockScreen,
          maskStyle: {
            opacity: settings.dialogMaskOpacity,
            backgroundColor: settings.dialogMaskBgColor
          },
          // 这里将多图片地址改造后插入文档中
          buttons: {
            enter: [lang.buttons.enter, function() {
              var url = this.find("[data-url]").val();
              var alt = this.find("[data-alt]").val();
              var link = this.find("[data-link]").val();
              if (url === "") {
                alert(imageLang.imageURLEmpty);
                return false;
              }
              // 这里增加循环
              let arr = url.split(";");
              var altAttr = (alt !== "") ? " \"" + alt + "\"" : "";
              for (let i = 0; i < arr.length; i++) {
                if (link === "" || link === "http://") {
                  // cm.replaceSelection("![" + alt + "](" + url + altAttr + ")");
                  cm.replaceSelection("![" + alt + "](" + arr[i] + altAttr + ")");
                } else {
                  // cm.replaceSelection("[![" + alt + "](" + url + altAttr + ")](" + link + altAttr + ")");
                  cm.replaceSelection("[![" + alt + "](" + arr[i] + altAttr + ")](" + link + altAttr + ")");
                }
              }
              if (alt === "") {
                cm.setCursor(cursor.line, cursor.ch + 2);
              }
              this.hide().lockScreen(false).hideMask();
              return false;
            }],

            cancel: [lang.buttons.cancel, function() {
              this.hide().lockScreen(false).hideMask();
              return false;
            }]
          }
        });
        dialog.attr("id", classPrefix + "image-dialog-" + guid);
        if (!settings.imageUpload) {
          return;
        }
        var fileInput = dialog.find("[name=\"" + classPrefix + "image-file\"]");
        fileInput.bind("change", function() {
          // 3xxx 20240602
          // let formData = new FormData();
          // 获取文本框dom
          // var doc = document.getElementById('doc');
          // 获取上传控件dom
          // var upload = document.getElementById('upload');
          // let files = upload.files;
          //遍历文件信息append到formData存储
          // for (let i = 0; i < files.length; i++) {
          //     let file = files[i]
          //     formData.append('files', file)
          // }
          // 获取文件名
          // var fileName = upload.files[0].name;
          // 获取文件路径
          // var filePath = upload.value;
          // doc.value = fileName;
          // 3xxx
          console.log(fileInput);
          console.log(fileInput[0].files);
          let files = fileInput[0].files;
          for (let i = 0; i < files.length; i++) {
            var fileName = files[i].name;
            // var fileName  = fileInput.val();
            var isImage = new RegExp("(\\.(" + settings.imageFormats.join("|") + "))$"); // /(\.(webp|jpg|jpeg|gif|bmp|png))$/
            if (fileName === "") {
              alert(imageLang.uploadFileEmpty);
              return false;
            }
            if (!isImage.test(fileName)) {
              alert(imageLang.formatNotAllowed + settings.imageFormats.join(", "));
              return false;
            }
            loading(true);
            var submitHandler = function() {
              var uploadIframe = document.getElementById(iframeName);
              uploadIframe.onload = function() {
                loading(false);
                var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
                var json = (body.innerText) ? body.innerText : ((body.textContent) ? body.textContent : null);
                json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")");
                var url="";
                for (let i = 0; i < json.length; i++) {
                  if (json[i].success === 1) {
                    if (i==0){
                      url=json[i].url;
                    }else{
                      url=url+";"+json[i].url;
                    }
                  } else {
                    alert(json[i].message);
                  }
                }
                dialog.find("[data-url]").val(url)
                return false;
              };
            };
            dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click");
          }
        });
      }
      dialog = editor.find("." + dialogName);
      dialog.find("[type=\"text\"]").val("");
      dialog.find("[type=\"file\"]").val("");
      dialog.find("[data-link]").val("http://");
      this.dialogShowMask(dialog);
      this.dialogLockScreen();
      dialog.show();
    };
  };

  // CommonJS/Node.js
  if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
    module.exports = factory;
  } else if (typeof define === "function") // AMD/CMD/Sea.js
  {
    if (define.amd) { // for Require.js
      define(["editormd"], function(editormd) {
        factory(editormd);
      });
    } else { // for Sea.js
      define(function(require) {
        var editormd = require("./../../editormd");
        factory(editormd);
      });
    }
  } else {
    factory(window.editormd);
  }
})();

func (c *DocumentController) Upload() 接口返回调整成数组后,影响单图复制粘贴功能

修改以下
mindoc\static\js\blog.js
mindoc\static\js\markdown.js
mindoc\static\js\quill.js

uploadImage("docEditor", function ($state, $res) {
    if ($state === "before") {
        return layer.load(1, {
            shade: [0.1, '#fff'] // 0.1 透明度的白色背景
        });
    } else if ($state === "success") {
        // if ($res.errcode === 0) {
            // var value = '![](' + $res.url + ')';
        // 3xxx 20240602
        if ($res[0].errcode === 0) {
            var value = '![](' + $res[0].url + ')';
            window.editor.insertValue(value);
        }
    }
});

uploadImage("docEditor", function ($state, $res) {
    if ($state === "before") {
        return layer.load(1, {
            shade: [0.1, '#fff'] // 0.1 透明度的白色背景
        });
    } else if ($state === "success") {
        // if ($res.errcode === 0) {
        if ($res[0].errcode === 0) {
            var range = window.editor.getSelection();
            // window.editor.insertEmbed(range.index, 'image', $res.url);
            window.editor.insertEmbed(range.index, 'image', $res[0].url);
        }
    }
});

cherry

cherry的批量上传图片与上述方法不同,上面是批量图片一次性由服务端上传,然后返回数组,再对数组进行循环处理,插入编辑器……这个cherry是将多图拆开循环,然后利用cherry_markdown里的myFileUpload 一个个上传。
能否参考上述一次性上传多图,然后服务端返回数组,再处理数组:循环和修改?
cherry-markdown.js里的handleUpload方法里构造input输入框来多选图片,点击后来到var Image$2里的_createClass里调用handleUpload方法,然后到handleUpload里获得选择的文件,
记得更新markdown.css文件
cherry-markdown.js

    /**
     * 上传文件的逻辑,如果有callback,则不再走默认的替换文本的逻辑,而是调用callback
     * @param {string} type 上传文件的类型
     */
    function handleUpload(editor) {
      var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'image';
      var accept = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '*';
      // type为上传文件类型 image|video|audio|pdf|word
      var input = document.createElement('input');
      input.type = 'file';
      input.id = 'fileUpload';
      input.value = '';
      input.style.display = 'none';
      input.accept = accept; // document.body.appendChild(input);
      input.multiple = 'multiple';

    input.addEventListener('change', function (event) {
        // @ts-ignore
        // var _event$target$files = _slicedToArray(event.target.files, 1),
        // file = _event$target$files[0]; // 文件上传后的回调函数可以由调用方自己实现
        // 3xxx 20240607 这里增加对多个文件的循环
           let files = event.target.files;
            for (let i = 0; i < files.length; i++) {

              var file = files[i]
              editor.options.fileUpload(file, function(url) { // 这里会进入cherry_markdown.js
                var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

editor.options.fileUpload(file, function(url) { 设断点,F11进入,会来到
cherry_markdown.js最后位置myFileUpload

function myFileUpload(file, callback) {
    // 创建 FormData 对象以便包含要上传的文件
    var formData = new FormData();
    formData.append("editormd-file-file", file); // "file" 是与你的服务端接口相对应的字段名
    var layerIndex = 0;
    // AJAX 请求
    $.ajax({
        url: window.fileUploadURL, // 确保此 URL 是文件上传 API 的正确 URL
        type: "POST",
        async: false, // 3xxx 20240609这里修改为同步,保证cherry批量上传图片时,插入的图片名称是正确的,否则,插入的图片名称都是最后一个名称
        dataType: "json",
        data: formData,
        processData: false, // 必须设置为 false,因为数据是 FormData 对象,不需要对数据进行序列化处理
        contentType: false, // 必须设置为 false,因为是 FormData 对象,jQuery 将不会设置内容类型头
        beforeSend: function () {
            layerIndex = layer.load(1, {
                shade: [0.1, '#fff'] // 0.1 透明度的白色背景
            });
        },
        error: function () {
            layer.close(layerIndex);
            layer.msg(locales[lang].uploadFailed);
        },
        success: function (data) {
            layer.close(layerIndex);
            if (data[0].errcode !== 0) { // 3xxx 这几个data都要改成data[0]
                layer.msg(data[0].message);
            } else {
                callback(data[0].url); // 假设返回的 JSON 中包含上传文件的 URL,调用回调函数并传入 URL
            }
        }
    });

循环赋值给params,再通过callback进入回到var Image$2方法里的 ——_createClass里,不断修改var finalName = params.name ? params.name : name;

插入图片的菜单在这里cherry_markdown.js

'|',
                'formula',
                {
                    insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'pdf', 'word', 'ruby'],
                },
                'graph',

这个菜单按钮的动作会来到cherry-markdown.js


cherry-markdown.js约61296行会随着上传的图片,不断修改编辑器中的图片链接地址

/**
     * 插入图片
     */
    var Image$2 = /*#__PURE__*/function (_MenuBase) {
      _inherits(Image, _MenuBase);
      var _super = _createSuper$11(Image);
      function Image($cherry) {
        var _this;
        _classCallCheck(this, Image);
        _this = _super.call(this, $cherry);
        _this.setName('image', 'image');
        return _this;
      }
      /**
       * 响应点击事件
       * @param {string} selection 被用户选中的文本内容
       * @returns {string} 回填到编辑器光标位置/选中文本区域的内容
       */

      _createClass(Image, [{
        key: "onClick",
        value: function onClick(selection) {
          var _this2 = this,
              _this$$cherry$options,
              _this$$cherry$options2,
              _this$$cherry$options3;
          if (this.hasCacheOnce()) {
            var _context, _context2;
            // @ts-ignore
            var _this$getAndCleanCach = this.getAndCleanCacheOnce(),
                name = _this$getAndCleanCach.name,
                url = _this$getAndCleanCach.url,
                params = _this$getAndCleanCach.params;
            var begin = '![';
            var end = "](".concat(url, ")");
            this.registerAfterClickCb(function () {
              _this2.setLessSelection(begin, end);
            });
            var finalName = params.name ? params.name : name;
            return concat$5(_context = concat$5(_context2 = "".concat(begin).concat(finalName)).call(_context2, handelParams(params))).call(_context, end);
          }
          var accept = (_this$$cherry$options = (_this$$cherry$options2 = this.$cherry.options) === null || _this$$cherry$options2 === void 0 ? void 0 : (_this$$cherry$options3 = _this$$cherry$options2.fileTypeLimitMap) === null || _this$$cherry$options3 === void 0 ? void 0 : _this$$cherry$options3.image) !== null && _this$$cherry$options !== void 0 ? _this$$cherry$options : '*'; // 插入图片,调用上传文件逻辑

          handleUpload(this.editor, 'image', accept, function (name, url, params) {
            _this2.setCacheOnce({
              name: name,
              url: url,
              params: params
            });
            _this2.fire(null);
          });
          this.updateMarkdown = false;
          return selection;
        }
作者:秦晓川  创建时间:2024-06-02 13:15
最后编辑:秦晓川  更新时间:2024-06-16 14:21