浏览代码

功能优化和新增 (#956)

* feat: 首页项目拖拽排序功能

* feat: 增加首页项目拖拽排序增加只能管理员进行, 排序失败元素回到原本位置

* perf: 新建文章以后直接进入到编辑文章页面

* perf: 优化文档打开时或刷新时样式闪动问题

* perf: 优化表格样式

* feat: 支持上传视频功能

* feat: 视频样式调整

* feat: 直接粘贴视频上传功能

* perf: 优化markdown目录显示
zhanzhenping 1 年之前
父节点
当前提交
1ea922106d

+ 1 - 1
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,必须带单位

+ 3 - 3
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, ".") {

+ 38 - 73
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)

+ 1 - 0
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 获取对应上传附件数据库表名.

+ 21 - 7
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 {

+ 1 - 1
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;

+ 2 - 1
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;

+ 79 - 0
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();
+                }
+            }
+        }
+    });
+}
+
 /**
  * 初始化代码高亮
  */

+ 19 - 4
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;");
+    }
 }

+ 11 - 7
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 = `<video controls><source src="${$res.url}" type="video/mp4"></video>`;
+                            window.editor.insertValue(value);
+                        } else {
+                            let value = '![](' + $res.url + ')';
+                            window.editor.insertValue(value);
+                        }
+                    } else {
+                        layer.msg("上传失败:" + $res.message);
                     }
                 }
             });

+ 67 - 33
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
 }

+ 0 - 1
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){