我们选择多张图片首先要改造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-16 14:21