From 1ea922106d814be0131ca487525bcb2b0aba3833 Mon Sep 17 00:00:00 2001 From: zhanzhenping <128675240+Zzhenping@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:31:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96=E5=92=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20(#956)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 首页项目拖拽排序功能 * feat: 增加首页项目拖拽排序增加只能管理员进行, 排序失败元素回到原本位置 * perf: 新建文章以后直接进入到编辑文章页面 * perf: 优化文档打开时或刷新时样式闪动问题 * perf: 优化表格样式 * feat: 支持上传视频功能 * feat: 视频样式调整 * feat: 直接粘贴视频上传功能 * perf: 优化markdown目录显示 --- conf/app.conf.example | 2 +- conf/enumerate.go | 6 +- controllers/DocumentController.go | 111 ++++++++-------------- models/AttachmentModel.go | 1 + static/css/markdown.preview.css | 28 ++++-- static/editor.md/css/editormd.css | 2 +- static/editor.md/css/editormd.preview.css | 3 +- static/js/editor.js | 79 +++++++++++++++ static/js/kancloud.js | 23 ++++- static/js/markdown.js | 18 ++-- utils/filetil/filetil.go | 100 ++++++++++++------- views/document/markdown_edit_template.tpl | 1 - 12 files changed, 243 insertions(+), 131 deletions(-) diff --git a/conf/app.conf.example b/conf/app.conf.example index afa11c76f..58d4ab017 100644 --- a/conf/app.conf.example +++ b/conf/app.conf.example @@ -79,7 +79,7 @@ avatar=/static/images/headimgurl.jpg token_size=12 #上传文件的后缀,如果不限制后缀可以设置为 * -upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif +upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif|mp4|webm|avi #上传的文件大小限制 # - 如果不填写, 则默认1GB,如果希望超过1GB,必须带单位 diff --git a/conf/enumerate.go b/conf/enumerate.go index c4a986b06..3a66afa35 100644 --- a/conf/enumerate.go +++ b/conf/enumerate.go @@ -106,9 +106,9 @@ func GetDefaultCover() string { return URLForWithCdnImage(web.AppConfig.DefaultString("cover", "/static/images/book.jpg")) } -// 获取允许的商城文件的类型. +// 获取允许的上传文件的类型. func GetUploadFileExt() []string { - ext := web.AppConfig.DefaultString("upload_file_ext", "png|jpg|jpeg|gif|txt|doc|docx|pdf") + ext := web.AppConfig.DefaultString("upload_file_ext", "png|jpg|jpeg|gif|txt|doc|docx|pdf|mp4") temp := strings.Split(ext, "|") @@ -201,7 +201,7 @@ func GetExportOutputPath() string { return exportOutputPath } -// 判断是否是允许商城的文件类型. +// 判断是否是允许上传的文件类型. func IsAllowUploadFileExt(ext string) bool { if strings.HasPrefix(ext, ".") { diff --git a/controllers/DocumentController.go b/controllers/DocumentController.go index 087c57513..35e6f0c48 100644 --- a/controllers/DocumentController.go +++ b/controllers/DocumentController.go @@ -7,6 +7,7 @@ import ( "html/template" "image/png" "io" + "mime/multipart" "net/http" "net/url" "os" @@ -486,41 +487,23 @@ func (c *DocumentController) Upload() { 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 - name = "file" - files, err = c.GetFiles(name) - // logs.Info(files) - if err == http.ErrMissingFile { - c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty")) - return - } + names := []string{"editormd-file-file", "editormd-image-file", "file", "editormd-resource-file"} + var files []*multipart.FileHeader + for _, name := range names { + file, err := c.GetFiles(name) + if err != nil { + continue + } + if len(file) > 0 && err == nil { + files = append(files, file...) } } - // if err != nil { - // http.Error(w, err.Error(), http.StatusNoContent) - // return - // } - // jMap := make(map[string]interface{}) - // s := []map[int]interface{}{} + if len(files) == 0 { + c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty")) + return + } + result2 := []map[string]interface{}{} var result map[string]interface{} for i, _ := range files { @@ -528,24 +511,6 @@ func (c *DocumentController) Upload() { 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()) @@ -619,19 +584,25 @@ func (c *DocumentController) Upload() { filePath := filepath.Join(conf.WorkingDirectory, "uploads", identify) //将图片和文件分开存放 - // if filetil.IsImageExt(moreFile.Filename) { + attachment := models.NewAttachment() + var strategy filetil.FileTypeStrategy if filetil.IsImageExt(files[i].Filename) { - filePath = filepath.Join(filePath, "images", fileName+ext) + strategy = filetil.ImageStrategy{} + attachment.ResourceType = "image" + } else if filetil.IsVideoExt(files[i].Filename) { + strategy = filetil.VideoStrategy{} + attachment.ResourceType = "video" } else { - filePath = filepath.Join(filePath, "files", fileName+ext) + strategy = filetil.DefaultStrategy{} + attachment.ResourceType = "file" } + filePath = strategy.GetFilePath(filePath, 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() @@ -640,12 +611,6 @@ func (c *DocumentController) Upload() { 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 @@ -662,8 +627,7 @@ func (c *DocumentController) Upload() { attachment.DocumentId = docId } - // if filetil.IsImageExt(moreFile.Filename) { - if filetil.IsImageExt(files[i].Filename) { + if filetil.IsImageExt(files[i].Filename) || filetil.IsVideoExt(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:])) @@ -689,19 +653,20 @@ func (c *DocumentController) Upload() { } } result = map[string]interface{}{ - "errcode": 0, - "success": 1, - "message": "ok", - "url": attachment.HttpPath, - "link": attachment.HttpPath, - "alt": attachment.FileName, - "is_attach": isAttach, - "attach": attachment, + "errcode": 0, + "success": 1, + "message": "ok", + "url": attachment.HttpPath, + "link": attachment.HttpPath, + "alt": attachment.FileName, + "is_attach": isAttach, + "attach": attachment, + "resource_type": attachment.ResourceType, } result2 = append(result2, result) } - if name == "file" { - // froala单图片上传 + if len(files) == 1 { + // froala单文件上传 c.Ctx.Output.JSON(result, true, false) } else { c.Ctx.Output.JSON(result2, true, false) diff --git a/models/AttachmentModel.go b/models/AttachmentModel.go index 0d382159e..4c84d7e95 100644 --- a/models/AttachmentModel.go +++ b/models/AttachmentModel.go @@ -33,6 +33,7 @@ type Attachment struct { FileExt string `orm:"column(file_ext);size(50);description(文件后缀)" json:"file_ext"` CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add;description(创建时间)" json:"create_time"` CreateAt int `orm:"column(create_at);type(int);description(创建人id)" json:"create_at"` + ResourceType string `orm:"-" json:"resource_type"` } // TableName 获取对应上传附件数据库表名. diff --git a/static/css/markdown.preview.css b/static/css/markdown.preview.css index b893fd1b5..bf23a4fff 100644 --- a/static/css/markdown.preview.css +++ b/static/css/markdown.preview.css @@ -20,7 +20,8 @@ width: 100%; overflow: auto; border-bottom: none; - line-height: 1.5 + line-height: 1.5; + display: table; } .editormd-preview-container table td,.editormd-preview-container table th { @@ -50,30 +51,43 @@ width: 100%; } +.whole-article-wrap { + display: flex; + flex-direction: column; +} + .article-body .markdown-toc{ position: fixed; - right: 0; + right: 50px; width: 260px; font-size: 12px; - margin-top: -70px; overflow: auto; - margin-right: 50px; + border: 1px solid #e8e8e8; + border-radius: 6px; } .markdown-toc ul{ list-style:none; } + +.markdown-toc-list { + padding:20px 0 !important; + margin-bottom: 0 !important; +} + .markdown-toc .markdown-toc-list>li{ padding: 3px 10px 3px 16px; line-height: 18px; - border-left: 2px solid #e8e8e8; + /*border-left: 2px solid #e8e8e8;*/ color: #595959; + margin-left: -2px; } .markdown-toc .markdown-toc-list>li.active{ border-right: 2px solid #25b864; } .article-body .markdown-article{ - margin-right: 250px; + width: calc(100% - 260px); + /*margin-right: 250px;*/ } .article-body.content .markdown-toc{ position: relative; @@ -86,7 +100,7 @@ .markdown-toc-list .directory-item { padding: 3px 10px 3px 16px; line-height: 18px; - border-left: 2px solid #e8e8e8; + /*border-left: 2px solid #e8e8e8;*/ color: #595959; } .markdown-toc-list .directory-item-link { diff --git a/static/editor.md/css/editormd.css b/static/editor.md/css/editormd.css index 773533297..cb475be87 100644 --- a/static/editor.md/css/editormd.css +++ b/static/editor.md/css/editormd.css @@ -3594,7 +3594,7 @@ background-color: #f8f8f8; } -.markdown-body img { +.markdown-body img, .markdown-body video { max-width: 100%; -moz-box-sizing: border-box; box-sizing: border-box; diff --git a/static/editor.md/css/editormd.preview.css b/static/editor.md/css/editormd.preview.css index 438b6c6fc..119bef90f 100644 --- a/static/editor.md/css/editormd.preview.css +++ b/static/editor.md/css/editormd.preview.css @@ -2878,7 +2878,8 @@ background-color: #f8f8f8; } -.markdown-body img { + +.markdown-body img, .markdown-body video { max-width: 100%; -moz-box-sizing: border-box; box-sizing: border-box; diff --git a/static/js/editor.js b/static/js/editor.js index 81b7ff4ec..63ca446b8 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -437,6 +437,85 @@ function uploadImage($id, $callback) { }); } + +function uploadResource($id, $callback) { + locales = { + 'zh-CN': { + unsupportType: '不支持的图片/视频格式', + uploadFailed: '图片/视频上传失败' + }, + 'en': { + unsupportType: 'Unsupport image/video type', + uploadFailed: 'Upload image/video failed' + } + } + /** 粘贴上传的资源 **/ + document.getElementById($id).addEventListener('paste', function (e) { + if (e.clipboardData && e.clipboardData.items) { + var clipboard = e.clipboardData; + for (var i = 0, len = clipboard.items.length; i < len; i++) { + if (clipboard.items[i].kind === 'file' || clipboard.items[i].type.indexOf('image') > -1) { + + var resource = clipboard.items[i].getAsFile(); + + var fileName = String((new Date()).valueOf()); + console.log(resource.type) + switch (resource.type) { + case "image/png" : + fileName += ".png"; + break; + case "image/jpg" : + fileName += ".jpg"; + break; + case "image/jpeg" : + fileName += ".jpeg"; + break; + case "image/gif" : + fileName += ".gif"; + break; + case "video/mp4": + fileName += ".mp4"; + break; + case "video/webm": + fileName += ".webm"; + break; + default : + layer.msg(locales[lang].unsupportType); + return; + } + var form = new FormData(); + + form.append('editormd-resource-file', resource, fileName); + + var layerIndex = 0; + + $.ajax({ + url: window.imageUploadURL, + type: "POST", + dataType: "json", + data: form, + processData: false, + contentType: false, + beforeSend: function () { + layerIndex = $callback('before'); + }, + error: function () { + layer.close(layerIndex); + $callback('error'); + layer.msg(locales[lang].uploadFailed); + }, + success: function (data) { + layer.close(layerIndex); + $callback('success', data); + } + }); + e.preventDefault(); + } + } + } + }); +} + /** * 初始化代码高亮 */ diff --git a/static/js/kancloud.js b/static/js/kancloud.js index 378f07970..eaae2174c 100644 --- a/static/js/kancloud.js +++ b/static/js/kancloud.js @@ -143,9 +143,7 @@ function renderPage($data) { $("#doc_id").val($data.doc_id); if ($data.page) { loadComment($data.page, $data.doc_id); - - } - else { + } else { pageClicked(-1, $data.doc_id); } @@ -156,6 +154,7 @@ function renderPage($data) { $("#view_container").removeClass("theme__dark theme__green theme__light theme__red theme__default") $("#view_container").addClass($data.markdown_theme) } + checkMarkdownTocElement(); } /*** @@ -230,6 +229,7 @@ function initHighlighting() { } $(function () { + checkMarkdownTocElement(); $(".view-backtop").on("click", function () { $('.manual-right').animate({ scrollTop: '0px' }, 200); }); @@ -280,7 +280,7 @@ $(function () { $(window).resize(function (e) { - var h = $(".manual-catalog").innerHeight() - 20; + var h = $(".manual-catalog").innerHeight() - 50; $(".markdown-toc").height(h); }).resize(); @@ -417,4 +417,19 @@ function loadCopySnippets() { [].forEach.call(snippets, function (snippet) { Prism.highlightElement(snippet); }); +} + +function checkMarkdownTocElement() { + console.log(111) + let toc = $(".markdown-toc-list"); + let articleComment = $("#articleComment"); + if (toc.length) { + $(".wiki-bottom-left").css("width", "calc(100% - 260px)"); + articleComment.css("width", "calc(100% - 260px)"); + articleComment.css("margin", "30px 0 70px 0"); + } else { + $(".wiki-bottom-left").css("width", "100%"); + articleComment.css("width", "100%"); + articleComment.css("margin", "30px auto 70px auto;"); + } } \ No newline at end of file diff --git a/static/js/markdown.js b/static/js/markdown.js index 778f06902..96e37c36e 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -245,18 +245,22 @@ $(function () { //如果没有选中节点则选中默认节点 openLastSelectedNode(); - uploadImage("docEditor", function ($state, $res) { + uploadResource("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); + if ($res.errcode === 0) { + if ($res.resource_type === 'video') { + let value = ``; + window.editor.insertValue(value); + } else { + let value = '![](' + $res.url + ')'; + window.editor.insertValue(value); + } + } else { + layer.msg("上传失败:" + $res.message); } } }); diff --git a/utils/filetil/filetil.go b/utils/filetil/filetil.go index 83c6f8125..d2175f767 100644 --- a/utils/filetil/filetil.go +++ b/utils/filetil/filetil.go @@ -1,21 +1,43 @@ package filetil import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math" "os" "path/filepath" "strings" - "io" - "fmt" - "math" - "io/ioutil" - "bytes" ) //================================== //更多文件和目录的操作,使用filepath包和os包 //================================== -//返回的目录扫描结果 +type FileTypeStrategy interface { + GetFilePath(filePath, fileName, ext string) string +} + +type ImageStrategy struct{} + +func (i ImageStrategy) GetFilePath(filePath, fileName, ext string) string { + return filepath.Join(filePath, "images", fileName+ext) +} + +type VideoStrategy struct{} + +func (v VideoStrategy) GetFilePath(filePath, fileName, ext string) string { + return filepath.Join(filePath, "videos", fileName+ext) +} + +type DefaultStrategy struct{} + +func (d DefaultStrategy) GetFilePath(filePath, fileName, ext string) string { + return filepath.Join(filePath, "files", fileName+ext) +} + +// 返回的目录扫描结果 type FileList struct { IsDir bool //是否是目录 Path string //文件路径 @@ -25,10 +47,10 @@ type FileList struct { ModTime int64 //文件修改时间戳 } -//目录扫描 -//@param dir 需要扫描的目录 -//@return fl 文件列表 -//@return err 错误 +// 目录扫描 +// @param dir 需要扫描的目录 +// @return fl 文件列表 +// @return err 错误 func ScanFiles(dir string) (fl []FileList, err error) { err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err == nil { @@ -47,7 +69,7 @@ func ScanFiles(dir string) (fl []FileList, err error) { return } -//拷贝文件 +// 拷贝文件 func CopyFile(source string, dst string) (err error) { sourceFile, err := os.Open(source) if err != nil { @@ -56,17 +78,16 @@ func CopyFile(source string, dst string) (err error) { defer sourceFile.Close() - _,err = os.Stat(filepath.Dir(dst)) + _, err = os.Stat(filepath.Dir(dst)) if err != nil { if os.IsNotExist(err) { - os.MkdirAll(filepath.Dir(dst),0766) - }else{ + os.MkdirAll(filepath.Dir(dst), 0766) + } else { return err } } - destFile, err := os.Create(dst) if err != nil { return err @@ -86,7 +107,7 @@ func CopyFile(source string, dst string) (err error) { return } -//拷贝目录 +// 拷贝目录 func CopyDir(source string, dest string) (err error) { // get properties of source dir @@ -107,7 +128,7 @@ func CopyDir(source string, dest string) (err error) { for _, obj := range objects { - sourceFilePointer := filepath.Join(source , obj.Name()) + sourceFilePointer := filepath.Join(source, obj.Name()) destinationFilePointer := filepath.Join(dest, obj.Name()) @@ -205,15 +226,15 @@ func Round(val float64, places int) float64 { return t } -//判断指定目录下是否存在指定后缀的文件 -func HasFileOfExt(path string,exts []string) bool { +// 判断指定目录下是否存在指定后缀的文件 +func HasFileOfExt(path string, exts []string) bool { err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if !info.IsDir() { ext := filepath.Ext(info.Name()) - for _,item := range exts { - if strings.EqualFold(ext,item) { + for _, item := range exts { + if strings.EqualFold(ext, item) { return os.ErrExist } } @@ -224,6 +245,7 @@ func HasFileOfExt(path string,exts []string) bool { return err == os.ErrExist } + // IsImageExt 判断是否是图片后缀 func IsImageExt(filename string) bool { ext := filepath.Ext(filename) @@ -232,25 +254,37 @@ func IsImageExt(filename string) bool { strings.EqualFold(ext, ".jpeg") || strings.EqualFold(ext, ".png") || strings.EqualFold(ext, ".gif") || - strings.EqualFold(ext,".svg") || - strings.EqualFold(ext,".bmp") || - strings.EqualFold(ext,".webp") + strings.EqualFold(ext, ".svg") || + strings.EqualFold(ext, ".bmp") || + strings.EqualFold(ext, ".webp") } -//忽略字符串中的BOM头 -func ReadFileAndIgnoreUTF8BOM(filename string) ([]byte,error) { - data,err := ioutil.ReadFile(filename) +// IsImageExt 判断是否是视频后缀 +func IsVideoExt(filename string) bool { + ext := filepath.Ext(filename) + + return strings.EqualFold(ext, ".mp4") || + strings.EqualFold(ext, ".webm") || + strings.EqualFold(ext, ".ogg") || + strings.EqualFold(ext, ".avi") || + strings.EqualFold(ext, ".flv") || + strings.EqualFold(ext, ".mov") +} + +// 忽略字符串中的BOM头 +func ReadFileAndIgnoreUTF8BOM(filename string) ([]byte, error) { + + data, err := ioutil.ReadFile(filename) if err != nil { - return nil,err + return nil, err } if data == nil { - return nil,nil + return nil, nil } - data = bytes.Replace(data,[]byte("\r"),[]byte(""),-1) + data = bytes.Replace(data, []byte("\r"), []byte(""), -1) if len(data) >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf { - return data[3:],err + return data[3:], err } - - return data,nil + return data, nil } diff --git a/views/document/markdown_edit_template.tpl b/views/document/markdown_edit_template.tpl index 5a141376b..4a37291c9 100644 --- a/views/document/markdown_edit_template.tpl +++ b/views/document/markdown_edit_template.tpl @@ -508,7 +508,6 @@ } }).on("uploadSuccess",function (file, res) { - for(var index in window.vueApp.lists){ var item = window.vueApp.lists[index]; if(item.attachment_id === file.id){