Browse Source

1、实现富文本编辑器
2、实现文档转换为PDF、MOBI、EPUB格式

Minho 7 years ago
parent
commit
e1ec6bb788

+ 0 - 201
LICENSE

@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "{}"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright {yyyy} {name of copyright owner}
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.

+ 9 - 1
controllers/base.go

@@ -109,7 +109,15 @@ func (c *BaseController) ExecuteViewPathTemplate(tplName string,data interface{}
 }
 
 func (c *BaseController) BaseUrl() string {
-	return c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host
+	baseUrl := beego.AppConfig.DefaultString("baseurl","")
+	if baseUrl != "" {
+		if strings.HasSuffix(baseUrl,"/"){
+			baseUrl = strings.TrimSuffix(baseUrl,"/")
+		}
+	}else{
+		baseUrl = c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host
+	}
+	return baseUrl
 }
 
 //显示错误信息页面.

+ 41 - 75
controllers/document.go

@@ -25,7 +25,6 @@ import (
 	"github.com/lifei6671/mindoc/conf"
 	"github.com/lifei6671/mindoc/models"
 	"github.com/lifei6671/mindoc/utils"
-	"github.com/lifei6671/mindoc/utils/wkhtmltopdf"
 	"github.com/russross/blackfriday"
 )
 
@@ -68,7 +67,7 @@ func isReadable(identify, token string, c *DocumentController) *models.BookResul
 		}
 	}
 
-	bookResult := book.ToBookResult()
+	bookResult := models.NewBookResult().ToBookResult(*book)
 
 	if c.Member != nil {
 		rel, err := models.NewRelationship().FindByBookIdAndMemberId(bookResult.BookId, c.Member.MemberId)
@@ -283,7 +282,7 @@ func (c *DocumentController) Edit() {
 			c.JsonResult(6002, "项目不存在或权限不足")
 		}
 
-		bookResult = book.ToBookResult()
+		bookResult = models.NewBookResult().ToBookResult(*book)
 	} else {
 		bookResult, err = models.NewBookResult().FindByIdentify(identify, c.Member.MemberId)
 
@@ -545,7 +544,7 @@ func (c *DocumentController) Upload() {
 	}
 
 	if attachment.HttpPath == "" {
-		attachment.HttpPath = beego.URLFor("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId)
+		attachment.HttpPath = c.BaseUrl() + beego.URLFor("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId)
 
 		if err := attachment.Update(); err != nil {
 			beego.Error("SaveToFile => ", err)
@@ -845,13 +844,6 @@ func (c *DocumentController) Content() {
 	c.JsonResult(0, "ok", doc)
 }
 
-func (c *DocumentController) ExportDoc() {
-	c.Export(true)
-}
-
-func (c *DocumentController) ExportBook() {
-	c.Export(false)
-}
 
 func (c *DocumentController) GetDocumentById(id string) (doc *models.Document, err error) {
 	doc = models.NewDocument()
@@ -871,7 +863,7 @@ func (c *DocumentController) GetDocumentById(id string) (doc *models.Document, e
 }
 
 // 导出
-func (c *DocumentController) Export(single_doc bool) {
+func (c *DocumentController) Export() {
 	c.Prepare()
 	c.TplName = "document/export.tpl"
 
@@ -897,7 +889,7 @@ func (c *DocumentController) Export(single_doc bool) {
 			c.Abort("500")
 		}
 
-		bookResult = book.ToBookResult()
+		bookResult = models.NewBookResult().ToBookResult(*book)
 	} else {
 		bookResult = isReadable(identify, token, c)
 	}
@@ -906,76 +898,50 @@ func (c *DocumentController) Export(single_doc bool) {
 		// TODO: 私有项目禁止导出
 	}
 
-	docs, err := models.NewDocument().FindListByBookId(bookResult.BookId)
+	if !strings.HasPrefix(bookResult.Cover,"http:://") && !strings.HasPrefix(bookResult.Cover,"https:://"){
+		bookResult.Cover = c.BaseUrl() + bookResult.Cover
+	}
+
+	eBookResult,err := bookResult.Converter(c.CruSession.SessionID())
+
 	if err != nil {
-		beego.Error(err)
+		beego.Error("转换文档失败:" + bookResult.BookName + " -> " + err.Error())
 		c.Abort("500")
 	}
 
+
 	if output == "pdf" {
-		exe := beego.AppConfig.String("wkhtmltopdf")
-		if exe == "" {
-			c.TplName = "errors/error.tpl"
-			c.Data["ErrorMessage"] = "没有配置PDF导出程序"
-			c.Data["ErrorCode"] = 50010
-			return
+		c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".pdf")
+
+		//如果没有开启缓存,则10分钟后删除
+		if !bookResult.IsCacheEBook {
+			defer func(pdfpath string) {
+				time.Sleep(time.Minute * 10)
+				os.Remove(filepath.Dir(pdfpath))
+			}(eBookResult.PDFPath)
 		}
-
-		dpath := "cache/" + bookResult.Identify
-		os.MkdirAll(dpath, 0766)
-
-		pathList := list.New()
-
-		// 增加对单页文档的导出,dandycheung, 2017-12-07
-		if single_doc {
-			id := c.Ctx.Input.Param(":id")
-			if doc, err := c.GetDocumentById(id); err == nil {
-				EachFun("", dpath, c, bookResult, doc, pathList)
-			}
-		} else {
-			RecursiveFun(0, "", dpath, c, bookResult, docs, pathList)
+		c.StopRun()
+	}else if output == "epub" {
+		c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub")
+
+		//如果没有开启缓存,则10分钟后删除
+		if !bookResult.IsCacheEBook {
+			defer func(pdfpath string) {
+				time.Sleep(time.Minute * 10)
+				os.Remove(filepath.Dir(pdfpath))
+			}(eBookResult.EpubPath)
 		}
-
-		defer os.RemoveAll(dpath)
-
-		// TODO: check if the pathList is empty
-
-		os.MkdirAll("./cache", 0766)
-		pdfpath := filepath.Join("cache", identify+"_"+c.CruSession.SessionID()+".pdf")
-
-		if _, err := os.Stat(pdfpath); os.IsNotExist(err) {
-			wkhtmltopdf.SetPath(beego.AppConfig.String("wkhtmltopdf"))
-
-			pdfg, err := wkhtmltopdf.NewPDFGenerator()
-			if err != nil {
-				beego.Error(err)
-				c.Abort("500")
-			}
-
-			pdfg.MarginBottom.Set(35)
-
-			for e := pathList.Front(); e != nil; e = e.Next() {
-				if page, ok := e.Value.(string); ok {
-					pdfg.AddPage(wkhtmltopdf.NewPage(page))
-				}
-			}
-
-			err = pdfg.Create()
-			if err != nil {
-				beego.Error(err)
-				c.Abort("500")
-			}
-
-			err = pdfg.WriteFile(pdfpath)
-			if err != nil {
-				beego.Error(err)
-			}
+		c.StopRun()
+	}else if output == "mobi" {
+		c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub")
+
+		//如果没有开启缓存,则10分钟后删除
+		if !bookResult.IsCacheEBook {
+			defer func(pdfpath string) {
+				time.Sleep(time.Minute * 10)
+				os.Remove(filepath.Dir(pdfpath))
+			}(eBookResult.MobiPath)
 		}
-
-		c.Ctx.Output.Download(pdfpath, identify+".pdf")
-
-		defer os.Remove(pdfpath)
-
 		c.StopRun()
 	}
 

+ 497 - 0
converter/converter.go

@@ -0,0 +1,497 @@
+//Author:TruthHun
+//Email:[email protected]
+//Date:2018-01-21
+package converter
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"time"
+
+	"os/exec"
+
+	"errors"
+
+	"github.com/TruthHun/gotil/cryptil"
+	"github.com/TruthHun/gotil/filetil"
+	"github.com/TruthHun/gotil/ziptil"
+)
+
+type Converter struct {
+	BasePath       string
+	Config         Config
+	Debug          bool
+	GeneratedCover string
+}
+
+//目录结构
+type Toc struct {
+	Id    int    `json:"id"`
+	Link  string `json:"link"`
+	Pid   int    `json:"pid"`
+	Title string `json:"title"`
+}
+
+//config.json文件解析结构
+type Config struct {
+	Charset      string   `json:"charset"`       //字符编码,默认utf-8编码
+	Cover        string   `json:"cover"`         //封面图片,或者封面html文件
+	Timestamp    string   `json:"date"`          //时间日期,如“2018-01-01 12:12:21”,其实是time.Time格式,但是直接用string就好
+	Description  string   `json:"description"`   //摘要
+	Footer       string   `json:"footer"`        //pdf的footer
+	Header       string   `json:"header"`        //pdf的header
+	Identifier   string   `json:"identifier"`    //即uuid,留空即可
+	Language     string   `json:"language"`      //语言,如zh、en、zh-CN、en-US等
+	Creator      string   `json:"creator"`       //作者,即author
+	Publisher    string   `json:"publisher"`     //出版单位
+	Contributor  string   `json:"contributor"`   //同Publisher
+	Title        string   `json:"title"`         //文档标题
+	Format       []string `json:"format"`        //导出格式,可选值:pdf、epub、mobi
+	FontSize     string   `json:"font_size"`     //默认的pdf导出字体大小
+	PaperSize    string   `json:"paper_size"`    //页面大小
+	MarginLeft   string   `json:"margin_left"`   //PDF文档左边距,写数字即可,默认72pt
+	MarginRight  string   `json:"margin_right"`  //PDF文档左边距,写数字即可,默认72pt
+	MarginTop    string   `json:"margin_top"`    //PDF文档左边距,写数字即可,默认72pt
+	MarginBottom string   `json:"margin_bottom"` //PDF文档左边距,写数字即可,默认72pt
+	More         []string `json:"more"`          //更多导出选项[PDF导出选项,具体参考:https://manual.calibre-ebook.com/generated/en/ebook-convert.html#pdf-output-options]
+	Toc          []Toc    `json:"toc"`           //目录
+	///////////////////////////////////////////
+	Order []string `json:"-"` //这个不需要赋值
+}
+
+var (
+	output       = "output" //文档导出文件夹
+	ebookConvert = "ebook-convert"
+)
+// 接口文档 https://manual.calibre-ebook.com/generated/en/ebook-convert.html#table-of-contents
+//根据json配置文件,创建文档转化对象
+func NewConverter(configFile string, debug ...bool) (converter *Converter, err error) {
+	var (
+		cfg      Config
+		basepath string
+		db       bool
+	)
+	if len(debug) > 0 {
+		db = debug[0]
+	}
+
+	if cfg, err = parseConfig(configFile); err == nil {
+		if basepath, err = filepath.Abs(filepath.Dir(configFile)); err == nil {
+			//设置默认值
+			if len(cfg.Timestamp) == 0 {
+				cfg.Timestamp = time.Now().Format("2006-01-02 15:04:05")
+			}
+			if len(cfg.Charset) == 0 {
+				cfg.Charset = "utf-8"
+			}
+			converter = &Converter{
+				Config:   cfg,
+				BasePath: basepath,
+				Debug:    db,
+			}
+		}
+	}
+	return
+}
+
+//执行文档转换
+func (this *Converter) Convert() (err error) {
+	if !this.Debug { //调试模式下不删除生成的文件
+		defer this.converterDefer() //最后移除创建的多余而文件
+	}
+
+	if err = this.generateMimeType(); err != nil {
+		return
+	}
+	if err = this.generateMetaInfo(); err != nil {
+		return
+	}
+	if err = this.generateTocNcx(); err != nil { //生成目录
+		return
+	}
+	if err = this.generateSummary(); err != nil { //生成文档内目录
+		return
+	}
+	if err = this.generateTitlePage(); err != nil { //生成封面
+		return
+	}
+	if err = this.generateContentOpf(); err != nil { //这个必须是generate*系列方法的最后一个调用
+		return
+	}
+
+	//将当前文件夹下的所有文件压缩成zip包,然后直接改名成content.epub
+	f := this.BasePath + "/content.epub"
+	os.Remove(f) //如果原文件存在了,则删除;
+	if err = ziptil.Zip(f, this.BasePath); err == nil {
+		//创建导出文件夹
+		os.Mkdir(this.BasePath+"/"+output, os.ModePerm)
+		if len(this.Config.Format) > 0 {
+			var errs []string
+			for _, v := range this.Config.Format {
+				fmt.Println("convert to " + v)
+				switch strings.ToLower(v) {
+				case "epub":
+					if err = this.convertToEpub(); err != nil {
+						errs = append(errs, err.Error())
+					}
+				case "mobi":
+					if err = this.convertToMobi(); err != nil {
+						errs = append(errs, err.Error())
+					}
+				case "pdf":
+					if err = this.convertToPdf(); err != nil {
+						errs = append(errs, err.Error())
+					}
+				}
+			}
+			if len(errs) > 0 {
+				err = errors.New(strings.Join(errs, "\n"))
+			}
+		} else {
+			err = this.convertToPdf()
+			if err != nil {
+				fmt.Println(err)
+			}
+		}
+	}
+	return
+}
+
+//删除生成导出文档而创建的文件
+func (this *Converter) converterDefer() {
+	//删除不必要的文件
+	os.RemoveAll(this.BasePath + "/META-INF")
+	os.RemoveAll(this.BasePath + "/content.epub")
+	os.RemoveAll(this.BasePath + "/mimetype")
+	os.RemoveAll(this.BasePath + "/toc.ncx")
+	os.RemoveAll(this.BasePath + "/content.opf")
+	os.RemoveAll(this.BasePath + "/titlepage.xhtml") //封面图片待优化
+	os.RemoveAll(this.BasePath + "/summary.html")    //文档目录
+}
+
+//生成metainfo
+func (this *Converter) generateMetaInfo() (err error) {
+	xml := `<?xml version="1.0"?>
+			<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
+			   <rootfiles>
+				  <rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
+			   </rootfiles>
+			</container>
+    `
+	folder := this.BasePath + "/META-INF"
+	os.MkdirAll(folder, os.ModePerm)
+	err = ioutil.WriteFile(folder+"/container.xml", []byte(xml), os.ModePerm)
+	return
+}
+
+//形成mimetyppe
+func (this *Converter) generateMimeType() (err error) {
+	return ioutil.WriteFile(this.BasePath+"/mimetype", []byte("application/epub+zip"), os.ModePerm)
+}
+
+//生成封面
+func (this *Converter) generateTitlePage() (err error) {
+	if ext := strings.ToLower(filepath.Ext(this.Config.Cover)); !(ext == ".html" || ext == ".xhtml") {
+		xml := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
+				<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="` + this.Config.Language + `">
+					<head>
+						<meta http-equiv="Content-Type" content="text/html; charset=` + this.Config.Charset + `"/>
+						<meta name="calibre:cover" content="true"/>
+						<title>Cover</title>
+						<style type="text/css" title="override_css">
+							@page {padding: 0pt; margin:0pt}
+							body { text-align: center; padding:0pt; margin: 0pt; }
+						</style>
+					</head>
+					<body>
+						<div>
+							<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="100%" height="100%" viewBox="0 0 800 1068" preserveAspectRatio="none">
+								<image width="800" height="1068" xlink:href="` + strings.TrimPrefix(this.Config.Cover, "./") + `"/>
+							</svg>
+						</div>
+					</body>
+				</html>
+		`
+		if err = ioutil.WriteFile(this.BasePath+"/titlepage.xhtml", []byte(xml), os.ModePerm); err == nil {
+			this.GeneratedCover = "titlepage.xhtml"
+		}
+	}
+	return
+}
+
+//生成文档目录
+func (this *Converter) generateTocNcx() (err error) {
+	ncx := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
+			<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="%v">
+			  <head>
+				<meta content="4" name="dtb:depth"/>
+				<meta content="calibre (2.85.1)" name="dtb:generator"/>
+				<meta content="0" name="dtb:totalPageCount"/>
+				<meta content="0" name="dtb:maxPageNumber"/>
+			  </head>
+			  <docTitle>
+				<text>%v</text>
+			  </docTitle>
+			  <navMap>%v</navMap>
+			</ncx>
+	`
+	codes, _ := this.tocToXml(0, 1)
+	ncx = fmt.Sprintf(ncx, this.Config.Language, this.Config.Title, strings.Join(codes, ""))
+	return ioutil.WriteFile(this.BasePath+"/toc.ncx", []byte(ncx), os.ModePerm)
+}
+
+//生成文档目录,即summary.html
+func (this *Converter) generateSummary() (err error) {
+	//目录
+	summary := `<!DOCTYPE html>
+				<html lang="` + this.Config.Language + `">
+				<head>
+				    <meta charset="` + this.Config.Charset + `">
+				    <title>目录</title>
+				    <style>
+				        body{margin: 0px;padding: 0px;}h1{text-align: center;padding: 0px;margin: 0px;}ul,li{list-style: none;}
+				        a{text-decoration: none;color: #4183c4;text-decoration: none;font-size: 16px;line-height: 28px;}
+				    </style>
+				</head>
+				<body>
+				    <h1>目&nbsp;&nbsp;&nbsp;&nbsp;录</h1>
+				    %v
+				</body>
+				</html>`
+	summary = fmt.Sprintf(summary, strings.Join(this.tocToSummary(0), ""))
+	return ioutil.WriteFile(this.BasePath+"/summary.html", []byte(summary), os.ModePerm)
+}
+
+//将toc转成toc.ncx文件
+func (this *Converter) tocToXml(pid, idx int) (codes []string, next_idx int) {
+	var code string
+	for _, toc := range this.Config.Toc {
+		if toc.Pid == pid {
+			code, idx = this.getNavPoint(toc, idx)
+			codes = append(codes, code)
+			for _, item := range this.Config.Toc {
+				if item.Pid == toc.Id {
+					code, idx = this.getNavPoint(item, idx)
+					codes = append(codes, code)
+					var code_arr []string
+					code_arr, idx = this.tocToXml(item.Id, idx)
+					codes = append(codes, code_arr...)
+					codes = append(codes, `</navPoint>`)
+				}
+			}
+			codes = append(codes, `</navPoint>`)
+		}
+	}
+	next_idx = idx
+	return
+}
+
+//将toc转成toc.ncx文件
+func (this *Converter) tocToSummary(pid int) (summarys []string) {
+	summarys = append(summarys, "<ul>")
+	for _, toc := range this.Config.Toc {
+		if toc.Pid == pid {
+			summarys = append(summarys, fmt.Sprintf(`<li><a href="%v">%v</a></li>`, toc.Link, toc.Title))
+			for _, item := range this.Config.Toc {
+
+				if item.Pid == toc.Id {
+					summarys = append(summarys, fmt.Sprintf(`<li><ul><li><a href="%v">%v</a></li>`, item.Link, item.Title))
+					summarys = append(summarys, "<li>")
+					summarys = append(summarys, this.tocToSummary(item.Id)...)
+					summarys = append(summarys, "</li></ul></li>")
+				}
+
+			}
+		}
+	}
+	summarys = append(summarys, "</ul>")
+	return
+}
+
+//生成navPoint
+func (this *Converter) getNavPoint(toc Toc, idx int) (navpoint string, nextidx int) {
+	navpoint = `
+	<navPoint id="id%v" playOrder="%v">
+		<navLabel>
+			<text>%v</text>
+		</navLabel>
+		<content src="%v"/>`
+	navpoint = fmt.Sprintf(navpoint, toc.Id, idx, toc.Title, toc.Link)
+	this.Config.Order = append(this.Config.Order, toc.Link)
+	nextidx = idx + 1
+	return
+}
+
+//生成content.opf文件
+//倒数第二步调用
+func (this *Converter) generateContentOpf() (err error) {
+	var (
+		guide       string
+		manifest    string
+		manifestArr []string
+		spine       string //注意:如果存在封面,则需要把封面放在第一个位置
+		spineArr    []string
+	)
+
+	meta := `<dc:title>%v</dc:title>
+			<dc:contributor opf:role="bkp">%v</dc:contributor>
+			<dc:publisher>%v</dc:publisher>
+			<dc:description>%v</dc:description>
+			<dc:language>%v</dc:language>
+			<dc:creator opf:file-as="Unknown" opf:role="aut">%v</dc:creator>
+			<meta name="calibre:timestamp" content="%v"/>
+	`
+	meta = fmt.Sprintf(meta, this.Config.Title, this.Config.Contributor, this.Config.Publisher, this.Config.Description, this.Config.Language, this.Config.Creator, this.Config.Timestamp)
+	if len(this.Config.Cover) > 0 {
+		meta = meta + `<meta name="cover" content="cover"/>`
+		guide = `<reference href="titlepage.xhtml" title="Cover" type="cover"/>`
+		manifest = fmt.Sprintf(`<item href="%v" id="cover" media-type="%v"/>`, this.Config.Cover, GetMediaType(filepath.Ext(this.Config.Cover)))
+		spineArr = append(spineArr, `<itemref idref="titlepage"/>`)
+	}
+
+	if _, err := os.Stat(this.BasePath + "/summary.html"); err == nil {
+		spineArr = append(spineArr, `<itemref idref="summary"/>`) //目录
+
+	}
+
+	//扫描所有文件
+	if files, err := filetil.ScanFiles(this.BasePath); err == nil {
+		basePath := strings.Replace(this.BasePath, "\\", "/", -1)
+		for _, file := range files {
+			if !file.IsDir {
+				ext := strings.ToLower(filepath.Ext(file.Path))
+				sourcefile := strings.TrimPrefix(file.Path, basePath+"/")
+				id := "ncx"
+				if ext != ".ncx" {
+					if file.Name == "titlepage.xhtml" { //封面
+						id = "titlepage"
+					} else if file.Name == "summary.html" { //目录
+						id = "summary"
+					} else {
+						id = cryptil.Md5Crypt(sourcefile)
+					}
+				}
+				if mt := GetMediaType(ext); mt != "" { //不是封面图片,且media-type不为空
+					if sourcefile != strings.TrimLeft(this.Config.Cover, "./") { //不是封面图片,则追加进来。封面图片前面已经追加进来了
+						manifestArr = append(manifestArr, fmt.Sprintf(`<item href="%v" id="%v" media-type="%v"/>`, sourcefile, id, mt))
+					}
+				}
+			}
+		}
+
+		items := make(map[string]string)
+		for _, link := range this.Config.Order {
+			id := cryptil.Md5Crypt(link)
+			if _, ok := items[id]; !ok { //去重
+				items[id] = id
+				spineArr = append(spineArr, fmt.Sprintf(`<itemref idref="%v"/>`, id))
+			}
+		}
+		manifest = manifest + strings.Join(manifestArr, "\n")
+		spine = strings.Join(spineArr, "\n")
+	} else {
+		return err
+	}
+
+	pkg := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
+		<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
+		  <metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata">
+			%v
+		  </metadata>
+		  <manifest>
+			%v
+		  </manifest>
+		  <spine toc="ncx">
+			%v
+		  </spine>
+			%v
+		</package>
+	`
+	if len(guide) > 0 {
+		guide = `<guide>` + guide + `</guide>`
+	}
+	pkg = fmt.Sprintf(pkg, meta, manifest, spine, guide)
+	return ioutil.WriteFile(this.BasePath+"/content.opf", []byte(pkg), os.ModePerm)
+}
+
+//转成epub
+func (this *Converter) convertToEpub() (err error) {
+	args := []string{
+		this.BasePath + "/content.epub",
+		this.BasePath + "/" + output + "/book.epub",
+	}
+	cmd := exec.Command(ebookConvert, args...)
+
+	if this.Debug {
+		fmt.Println(cmd.Args)
+	}
+	return cmd.Run()
+}
+
+//转成mobi
+func (this *Converter) convertToMobi() (err error) {
+	args := []string{
+		this.BasePath + "/content.epub",
+		this.BasePath + "/" + output + "/book.mobi",
+	}
+	cmd := exec.Command(ebookConvert, args...)
+	if this.Debug {
+		fmt.Println(cmd.Args)
+	}
+
+	return cmd.Run()
+}
+
+//转成pdf
+func (this *Converter) convertToPdf() (err error) {
+	args := []string{
+		this.BasePath + "/content.epub",
+		this.BasePath + "/" + output + "/book.pdf",
+	}
+	//页面大小
+	if len(this.Config.PaperSize) > 0 {
+		args = append(args, "--paper-size", this.Config.PaperSize)
+	}
+	//文字大小
+	if len(this.Config.FontSize) > 0 {
+		args = append(args, "--pdf-default-font-size", this.Config.FontSize)
+	}
+
+	//header template
+	if len(this.Config.Header) > 0 {
+		args = append(args, "--pdf-header-template", this.Config.Header)
+	}
+
+	//footer template
+	if len(this.Config.Footer) > 0 {
+		args = append(args, "--pdf-footer-template", this.Config.Footer)
+	}
+
+	if len(this.Config.MarginLeft) > 0 {
+		args = append(args, "--pdf-page-margin-left", this.Config.MarginLeft)
+	}
+	if len(this.Config.MarginTop) > 0 {
+		args = append(args, "--pdf-page-margin-top", this.Config.MarginTop)
+	}
+	if len(this.Config.MarginRight) > 0 {
+		args = append(args, "--pdf-page-margin-right", this.Config.MarginRight)
+	}
+	if len(this.Config.MarginBottom) > 0 {
+		args = append(args, "--pdf-page-margin-bottom", this.Config.MarginBottom)
+	}
+
+	//更多选项
+	if len(this.Config.More) > 0 {
+		args = append(args, this.Config.More...)
+	}
+
+	cmd := exec.Command(ebookConvert, args...)
+	if this.Debug {
+		fmt.Println(cmd.Args)
+	}
+	return cmd.Run()
+}

+ 47 - 0
converter/util.go

@@ -0,0 +1,47 @@
+//Author:TruthHun
+//Email:[email protected]
+//Date:2018-01-21
+package converter
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"strings"
+)
+
+//media-type
+var MediaType = map[string]string{
+	".jpeg":  "image/jpeg",
+	".png":   "image/png",
+	".jpg":   "image/jpeg",
+	".gif":   "image/gif",
+	".ico":   "image/x-icon",
+	".bmp":   "image/bmp",
+	".html":  "application/xhtml+xml",
+	".xhtml": "application/xhtml+xml",
+	".htm":   "application/xhtml+xml",
+	".otf":   "application/x-font-opentype",
+	".ttf":   "application/x-font-ttf",
+	".js":    "application/x-javascript",
+	".ncx":   "x-dtbncx+xml",
+	".txt":   "text/plain",
+	".xml":   "text/xml",
+	".css":   "text/css",
+}
+
+//根据文件扩展名,获取media-type
+func GetMediaType(ext string) string {
+	if mt, ok := MediaType[strings.ToLower(ext)]; ok {
+		return mt
+	}
+	return ""
+}
+
+//解析配置文件
+func parseConfig(configFile string) (cfg Config, err error) {
+	var b []byte
+	if b, err = ioutil.ReadFile(configFile); err == nil {
+		err = json.Unmarshal(b, &cfg)
+	}
+	return
+}

+ 4 - 34
models/book.go

@@ -3,8 +3,6 @@ package models
 import (
 	"time"
 
-	"strings"
-
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
@@ -24,6 +22,10 @@ type Book struct {
 	OrderIndex int    `orm:"column(order_index);type(int);default(0)" json:"order_index"`
 	// Description 项目描述.
 	Description string `orm:"column(description);size(2000)" json:"description"`
+	//发行公司
+	Publisher string 	`orm:"column(publisher);size(500)" json:"publisher"`
+	//是否缓存导出的电子书,如果缓存可能会出现导出的文件不是最新的。 0 为不缓存
+	IsCacheEBook int `orm:"column(is_cache_ebook);type(int);default(0)" json:"is_cache_ebook"`
 	Label       string `orm:"column(label);size(500)" json:"label"`
 	// PrivatelyOwned 项目私有: 0 公开/ 1 私有
 	PrivatelyOwned int `orm:"column(privately_owned);type(int);default(0)" json:"privately_owned"`
@@ -354,38 +356,6 @@ func (m *Book) FindForLabelToPager(keyword string, pageIndex, pageSize, member_i
 }
 
 
-func (book *Book) ToBookResult() *BookResult {
-
-	m := NewBookResult()
-
-	m.BookId = book.BookId
-	m.BookName = book.BookName
-	m.Identify = book.Identify
-	m.OrderIndex = book.OrderIndex
-	m.Description = strings.Replace(book.Description, "\r\n", "<br/>", -1)
-	m.PrivatelyOwned = book.PrivatelyOwned
-	m.PrivateToken = book.PrivateToken
-	m.DocCount = book.DocCount
-	m.CommentStatus = book.CommentStatus
-	m.CommentCount = book.CommentCount
-	m.CreateTime = book.CreateTime
-	m.ModifyTime = book.ModifyTime
-	m.Cover = book.Cover
-	m.Label = book.Label
-	m.Status = book.Status
-	m.Editor = book.Editor
-	m.Theme = book.Theme
-	m.AutoRelease = book.AutoRelease == 1
-
-	if book.Theme == "" {
-		m.Theme = "default"
-	}
-	if book.Editor == "" {
-		m.Editor = "markdown"
-	}
-	return m
-}
-
 //重置文档数量
 func (m *Book) ResetDocumentNumber(book_id int) {
 	o := orm.NewOrm()

+ 194 - 4
models/book_result.go

@@ -2,10 +2,20 @@ package models
 
 import (
 	"time"
+	"bytes"
 
 	"github.com/astaxie/beego/orm"
 	"github.com/astaxie/beego/logs"
 	"github.com/lifei6671/mindoc/conf"
+	"strings"
+	"github.com/lifei6671/mindoc/converter"
+	"strconv"
+	"github.com/russross/blackfriday"
+	"path/filepath"
+	"github.com/astaxie/beego"
+	"os"
+	"github.com/PuerkitoBio/goquery"
+	"github.com/lifei6671/mindoc/utils"
 )
 
 type BookResult struct {
@@ -14,17 +24,19 @@ type BookResult struct {
 	Identify string         `json:"identify"`
 	OrderIndex int          `json:"order_index"`
 	Description string      `json:"description"`
+	Publisher string 		`json:"publisher"`
+	IsCacheEBook bool		`json:"is_cache_ebook"`
 	PrivatelyOwned int      `json:"privately_owned"`
 	PrivateToken string     `json:"private_token"`
 	DocCount int            `json:"doc_count"`
 	CommentStatus string    `json:"comment_status"`
 	CommentCount int        `json:"comment_count"`
 	CreateTime time.Time    `json:"create_time"`
-	CreateName string 	`json:"create_name"`
+	CreateName string 		`json:"create_name"`
 	ModifyTime time.Time	`json:"modify_time"`
 	Cover string            `json:"cover"`
-	Theme string		`json:"theme"`
-	Label string		`json:"label"`
+	Theme string			`json:"theme"`
+	Label string			`json:"label"`
 	MemberId int            `json:"member_id"`
 	Editor string           `json:"editor"`
 	AutoRelease bool		`json:"auto_release"`
@@ -79,13 +91,14 @@ func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,
 		return m, err
 	}
 
-	m = book.ToBookResult()
+	m = NewBookResult().ToBookResult(*book)
 
 	m.CreateName 		= member.Account
 	m.MemberId 		= relationship.MemberId
 	m.RoleId		= relationship.RoleId
 	m.RelationshipId	= relationship.RelationshipId
 
+
 	if m.RoleId == conf.BookFounder {
 		m.RoleName = "创始人"
 	} else if m.RoleId == conf.BookAdmin {
@@ -134,6 +147,183 @@ func (m *BookResult) FindToPager(pageIndex, pageSize int) (books []*BookResult,t
 	return
 }
 
+//实体转换
+func (m *BookResult) ToBookResult(book Book) *BookResult {
+
+	m.BookId = book.BookId
+	m.BookName = book.BookName
+	m.Identify = book.Identify
+	m.OrderIndex = book.OrderIndex
+	m.Description = strings.Replace(book.Description, "\r\n", "<br/>", -1)
+	m.PrivatelyOwned = book.PrivatelyOwned
+	m.PrivateToken = book.PrivateToken
+	m.DocCount = book.DocCount
+	m.CommentStatus = book.CommentStatus
+	m.CommentCount = book.CommentCount
+	m.CreateTime = book.CreateTime
+	m.ModifyTime = book.ModifyTime
+	m.Cover = book.Cover
+	m.Label = book.Label
+	m.Status = book.Status
+	m.Editor = book.Editor
+	m.Theme = book.Theme
+	m.AutoRelease = book.AutoRelease == 1
+	m.Publisher = book.Publisher
+	m.IsCacheEBook = book.IsCacheEBook == 1
+
+	if book.Theme == "" {
+		m.Theme = "default"
+	}
+	if book.Editor == "" {
+		m.Editor = "markdown"
+	}
+	return m
+}
+
+func (m *BookResult) Converter(sessionId string) (ConvertBookResult,error) {
+
+	convertBookResult := ConvertBookResult{}
+	outputPath := filepath.Join(beego.AppConfig.DefaultString("book_output_path","cache"),sessionId,strconv.Itoa(m.BookId))
+
+	if m.IsCacheEBook {
+		outputPath = filepath.Join(beego.AppConfig.DefaultString("book_output_path","cache"),strconv.Itoa(m.BookId))
+	}
+
+	if m.IsCacheEBook {
+		pdfpath := filepath.Join(outputPath,"output","book.pdf")
+		epubpath := filepath.Join(outputPath,"output","book.epub")
+		mobipath := filepath.Join(outputPath,"output","book.mobi")
+
+		if utils.FileExists(pdfpath) && utils.FileExists(epubpath) && utils.FileExists(mobipath){
+			convertBookResult.EpubPath = epubpath
+			convertBookResult.MobiPath = mobipath
+			convertBookResult.PDFPath = pdfpath
+			return convertBookResult,nil
+		}
+	}
+	docs, err := NewDocument().FindListByBookId(m.BookId)
+	if err != nil {
+		return convertBookResult,err
+	}
+
+	tocList := make([]converter.Toc,0)
+
+	for _, item := range docs {
+		if item.ParentId == 0 {
+			toc := converter.Toc{
+				Id: item.DocumentId,
+				Link: strconv.Itoa(item.DocumentId) + ".html",
+				Pid: item.ParentId,
+				Title: item.DocumentName,
+			}
+
+			tocList = append(tocList,toc)
+		}
+	}
+	for _, item := range docs {
+		if item.ParentId != 0 {
+			toc := converter.Toc{
+				Id: item.DocumentId,
+				Link: strconv.Itoa(item.DocumentId) + ".html",
+				Pid: item.ParentId,
+				Title: item.DocumentName,
+			}
+			tocList = append(tocList,toc)
+		}
+	}
+
+	ebookConfig := converter.Config{
+		Charset :      "utf-8",
+		Cover :        m.Cover,
+		Timestamp :    time.Now().Format("2006-01-02 15:04:05"),
+		Description :  string(blackfriday.MarkdownBasic([]byte(m.Description))),
+		Footer :       "<p style='color:#8E8E8E;font-size:12px;'>本文档使用 <a href='https://www.iminho.me' style='text-decoration:none;color:#1abc9c;font-weight:bold;'>MinDoc</a> 构建 <span style='float:right'>- _PAGENUM_ -</span></p>",
+		Header :       "<p style='color:#8E8E8E;font-size:12px;'>_SECTION_</p>",
+		Identifier :   "",
+		Language :     "zh-CN",
+		Creator :      m.CreateName,
+		Publisher :    m.Publisher,
+		Contributor :  m.Publisher,
+		Title :        m.BookName,
+		Format:        []string{"epub", "mobi", "pdf"},
+		FontSize :     "14",
+		PaperSize :    "a4",
+		MarginLeft :   "72",
+		MarginRight :  "72",
+		MarginTop :    "72",
+		MarginBottom : "72",
+		Toc :          tocList,
+		More :         []string{},
+
+	}
+
+
+	os.MkdirAll(outputPath, 0766)
+	if outputPath, err = filepath.Abs(outputPath); err != nil {
+		beego.Error("导出目录配置错误:" + err.Error())
+		return convertBookResult,err
+	}
+
+	viewPath := beego.BConfig.WebConfig.ViewsPath
+	baseUrl := beego.AppConfig.DefaultString("baseurl","")
+
+	for _,item := range docs {
+		name := strconv.Itoa(item.DocumentId)
+		fpath := filepath.Join(outputPath,name + ".html")
+
+		f, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0777)
+		if err != nil {
+			return convertBookResult,err
+		}
+		var buf bytes.Buffer
+
+		if err := beego.ExecuteViewPathTemplate(&buf,"document/export.tpl",viewPath,map[string]interface{}{"Model": m, "Lists": item, "BaseUrl": baseUrl}); err != nil {
+			return convertBookResult,err
+		}
+		html := buf.String()
+
+
+		if err != nil {
+
+			f.Close()
+			return convertBookResult,err
+		}
+
+		bufio := bytes.NewReader(buf.Bytes())
+
+		doc, err := goquery.NewDocumentFromReader(bufio)
+		doc.Find("img").Each(func(i int, contentSelection *goquery.Selection) {
+			if src, ok := contentSelection.Attr("src"); ok && strings.HasPrefix(src, "/uploads/") {
+				contentSelection.SetAttr("src", baseUrl + src)
+			}
+		})
+
+		html, err = doc.Html()
+		if err != nil {
+			f.Close()
+			return convertBookResult,err
+		}
+
+		// html = strings.Replace(html, "<img src=\"/uploads", "<img src=\"" + c.BaseUrl() + "/uploads", -1)
+
+		f.WriteString(html)
+		f.Close()
+	}
+	eBookConverter := &converter.Converter{
+		BasePath : outputPath,
+		Config :   ebookConfig,
+		Debug :    false,
+	}
+
+	if err := eBookConverter.Convert();err != nil {
+		beego.Error("转换文件错误:" + m.BookName +" => "+ err.Error())
+		return convertBookResult,err
+	}
+	convertBookResult.MobiPath = filepath.Join(outputPath,"output","book.mobi")
+	convertBookResult.PDFPath = filepath.Join(outputPath,"output","book.pdf")
+	convertBookResult.EpubPath = filepath.Join(outputPath,"output","book.epub")
+	return  convertBookResult,nil
+}
 
 
 

+ 8 - 0
models/convert_book_result.go

@@ -0,0 +1,8 @@
+package models
+
+// 转换结果
+type ConvertBookResult struct {
+	PDFPath string
+	EpubPath string
+	MobiPath string
+}

+ 4 - 0
models/document.go

@@ -8,6 +8,7 @@ import (
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/orm"
 	"github.com/lifei6671/mindoc/conf"
+	"strings"
 )
 
 // Document struct.
@@ -137,6 +138,9 @@ func (m *Document) ReleaseContent(book_id int) {
 		if err == nil && len(attach_list) > 0 {
 			content := bytes.NewBufferString("<div class=\"attach-list\"><strong>附件</strong><ul>")
 			for _, attach := range attach_list {
+				if strings.HasPrefix(attach.HttpPath,"/"){
+					attach.HttpPath = strings.TrimSuffix(beego.AppConfig.DefaultString("baseurl",""),"/") + attach.HttpPath
+				}
 				li := fmt.Sprintf("<li><a href=\"%s\" target=\"_blank\" title=\"%s\">%s</a></li>", attach.HttpPath, attach.FileName, attach.FileName)
 
 				content.WriteString(li)

+ 1 - 2
routers/router.go

@@ -72,8 +72,7 @@ func init() {
 	beego.Router("/docs/:key", &controllers.DocumentController{}, "*:Index")
 	beego.Router("/docs/:key/:id", &controllers.DocumentController{}, "*:Read")
 	beego.Router("/docs/:key/search", &controllers.DocumentController{}, "post:Search")
-	beego.Router("/export/:key", &controllers.DocumentController{}, "*:ExportBook")
-	beego.Router("/export/:key/:id", &controllers.DocumentController{}, "*:ExportDoc")
+	beego.Router("/export/:key", &controllers.DocumentController{}, "*:Export")
 	beego.Router("/qrcode/:key.png", &controllers.DocumentController{}, "get:QrCode")
 
 	beego.Router("/attach_files/:key/:attach_id", &controllers.DocumentController{}, "get:DownloadAttachment")

+ 18 - 2
static/js/editor.js

@@ -88,7 +88,6 @@ function openDeleteDocumentDialog($node) {
             layer.close(index);
             if(res.errcode === 0){
                 window.treeCatalog.delete_node($node);
-                resetEditor($node);
             }else{
                 layer.msg("删除失败",{icon : 2})
             }
@@ -153,6 +152,24 @@ function pushVueLists($lists) {
     }
 }
 
+/**
+ * 发布项目
+ */
+function releaseBook() {
+    $.ajax({
+        url: window.releaseURL,
+        data: { "identify": window.book.identify },
+        type: "post",
+        dataType: "json",
+        success: function (res) {
+            if (res.errcode === 0) {
+                layer.msg("发布任务已推送到任务队列,稍后将在后台执行。");
+            } else {
+                layer.msg(res.message);
+            }
+        }
+    });
+}
 //实现小提示
 $("[data-toggle='tooltip']").hover(function () {
     var title = $(this).attr('data-title');
@@ -238,7 +255,6 @@ function uploadImage($id,$callback) {
 
                 var imageFile = clipboard.items[i].getAsFile();
 
-                console.log(imageFile)
                 var fileName = Date.parse(new Date());
 
                 switch (imageFile.type){

+ 1 - 19
static/js/markdown.js

@@ -227,25 +227,7 @@ $(function () {
             }
         });
     }
-
-    function releaseBook() {
-        $.ajax({
-            url: window.releaseURL,
-            data: { "identify": window.book.identify },
-            type: "post",
-            dataType: "json",
-            success: function (res) {
-                if (res.errcode === 0) {
-                    layer.msg("发布任务已推送到任务队列,稍后将在后台执行。");
-                } else {
-                    layer.msg(res.message);
-                }
-            }
-        });
-    }
-
-    function resetEditor($node) {
-    }
+    
 
     /**
      * 设置编辑器变更状态

+ 97 - 11
static/js/quill.js

@@ -8,14 +8,83 @@ $(function () {
             toolbar :"#editormd-tools"
         }
     });
-    window.editor.on("editor-change",function () {
+    window.editor.on("text-change",function () {
         resetEditorChanged(true);
     });
-    window.menu_save.on("click",function () {
-       if($(this).hasClass('change')){
-           saveDocument();
-       }
+    var $editorEle =  $("#editormd-tools");
+
+    $editorEle.find(".ql-undo").on("click",function () {
+        window.editor.history.undo();
+    });
+    $editorEle.find(".ql-redo").on("click",function () {
+        window.editor.history.redo();
     });
+
+    $("#btnRelease").on("click",function () {
+        if (Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0) {
+            if ($("#markdown-save").hasClass('change')) {
+                var comfirm_result = confirm("编辑内容未保存,需要保存吗?")
+                if (comfirm_result) {
+                    saveDocument(false, releaseBook);
+                    return;
+                }
+            }
+
+            releaseBook();
+        } else {
+            layer.msg("没有需要发布的文档")
+        }
+    });
+
+    /**
+     * 实现自定义图片上传
+     */
+    window.editor.getModule('toolbar').addHandler('image',function () {
+        var input = document.createElement('input');
+        input.setAttribute('type', 'file');
+        input.click();
+
+        // Listen upload local image and save to server
+        input.onchange = function () {
+            var file = input.files[0];
+
+            // file type is only image.
+            if (/^image\//.test(file.type)) {
+                var form = new FormData();
+                form.append('editormd-image-file', file, file.name);
+
+                var layerIndex = 0;
+
+                $.ajax({
+                    url: window.imageUploadURL,
+                    type: "POST",
+                    dataType: "json",
+                    data: form,
+                    processData: false,
+                    contentType: false,
+                    error: function() {
+                        layer.close(layerIndex);
+                        layer.msg("图片上传失败");
+                    },
+                    success: function(data) {
+                        layer.close(layerIndex);
+                        if(data.errcode !== 0){
+                            layer.msg(data.message);
+                        }else{
+                            var range = window.editor.getSelection();
+                            editor.insertEmbed(range.index, 'image', data.url);
+                        }
+                    }
+                });
+            } else {
+                console.warn('You could only upload images.');
+            }
+        };
+    });
+    /**
+     * 实现保存
+     */
+    window.menu_save.on("click",function () {if($(this).hasClass('change')){saveDocument();}});
     /**
      * 设置编辑器变更状态
      * @param $is_change
@@ -43,13 +112,14 @@ $(function () {
 
             if(res.errcode === 0){
                 window.isLoad = true;
-                window.editor.setContents([{ insert: res.data.content }]);
+                window.editor.root.innerHTML = res.data.content;
 
                 // 将原始内容备份
                 window.source = res.data.content;
                 var node = { "id" : res.data.doc_id,'parent' : res.data.parent_id === 0 ? '#' : res.data.parent_id ,"text" : res.data.doc_name,"identify" : res.data.identify,"version" : res.data.version};
                 pushDocumentCategory(node);
                 window.selectNode = node;
+                window.isLoad = true;
 
                 pushVueLists(res.data.attach);
 
@@ -70,8 +140,9 @@ $(function () {
         var index = null;
         var node = window.selectNode;
 
-        var html = window.editor.getContents();
+        var html = window.editor.root.innerHTML;
 
+        console.log(html)
         var content = "";
         if($.trim(html) !== ""){
             content = toMarkdown(html, { gfm: true });
@@ -111,10 +182,9 @@ $(function () {
                             break;
                         }
                     }
+                    resetEditorChanged(false);
                     // 更新内容备份
                     window.source = res.data.content;
-                    // 触发编辑器 onchange 回调函数
-                    window.editor.onchange();
                     if(typeof callback === "function"){
                         callback();
                     }
@@ -231,8 +301,20 @@ $(function () {
         }
     }).on('loaded.jstree', function () {
         window.treeCatalog = $(this).jstree();
+        var $select_node_id = window.treeCatalog.get_selected();
+        if ($select_node_id) {
+            var $select_node = window.treeCatalog.get_node($select_node_id[0])
+            if ($select_node) {
+                $select_node.node = {
+                    id: $select_node.id
+                };
+
+                loadDocument($select_node);
+            }
+        }
+
     }).on('select_node.jstree', function (node, selected, event) {
-        if(window.menu_save.hasClass('selected')) {
+        if(window.menu_save.hasClass('change')) {
             if(confirm("编辑内容未保存,需要保存吗?")){
                 saveDocument(false,function () {
                     loadDocument(selected);
@@ -242,7 +324,11 @@ $(function () {
         }
         loadDocument(selected);
 
-    }).on("move_node.jstree", jstree_save);
+    }).on("move_node.jstree", jstree_save)
+      .on("delete_node.jstree",function (node,parent) {
+          window.isLoad = true;
+          window.editor.root.innerHTML ='';
+      });
 
     window.saveDocument = saveDocument;
 

+ 1 - 1
static/quill/quill.icons.js

@@ -1,7 +1,7 @@
 (function () {
     var icons = Quill.import('ui/icons');
     icons.header[3] = '<svg viewBox="0 0 18 18">\n' +
-        '  <path class="ql-fill" d="M14.51758,9.64453a1.85627,1.85627,0,0,0-1.24316.38477H13.252a1.73532,1.73532,0,0,1,1.72754-1.4082,2.66491,2.66491,0,0,1,.5498.06641c.35254.05469.57227.01074.70508-.40723l.16406-.5166a.53393.53393,0,0,0-.373-.75977,4.83723,4.83723,0,0,0-1.17773-.14258c-2.43164,0-3.7627,2.17773-3.7627,4.43359,0,2.47559,1.60645,3.69629,3.19043,3.69629A2.70585,2.70585,0,0,0,16.96,12.19727,2.43861,2.43861,0,0,0,14.51758,9.64453Zm-.23047,3.58691c-.67187,0-1.22168-.81445-1.22168-1.45215,0-.47363.30762-.583.72559-.583.96875,0,1.27734.59375,1.27734,1.12207A.82182.82182,0,0,1,14.28711,13.23145ZM10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Z"/>\n' +
+        '  <path class="ql-fill" d="M16.65186,12.30664a2.6742,2.6742,0,0,1-2.915,2.68457,3.96592,3.96592,0,0,1-2.25537-.6709.56007.56007,0,0,1-.13232-.83594L11.64648,13c.209-.34082.48389-.36328.82471-.1543a2.32654,2.32654,0,0,0,1.12256.33008c.71484,0,1.12207-.35156,1.12207-.78125,0-.61523-.61621-.86816-1.46338-.86816H13.2085a.65159.65159,0,0,1-.68213-.41895l-.05518-.10937a.67114.67114,0,0,1,.14307-.78125l.71533-.86914a8.55289,8.55289,0,0,1,.68213-.7373V8.58887a3.93913,3.93913,0,0,1-.748.05469H11.9873a.54085.54085,0,0,1-.605-.60547V7.59863a.54085.54085,0,0,1,.605-.60547h3.75146a.53773.53773,0,0,1,.60547.59375v.17676a1.03723,1.03723,0,0,1-.27539.748L14.74854,10.0293A2.31132,2.31132,0,0,1,16.65186,12.30664ZM9,3A.99974.99974,0,0,0,8,4V8H3V4A1,1,0,0,0,1,4V14a1,1,0,0,0,2,0V10H8v4a1,1,0,0,0,2,0V4A.99974.99974,0,0,0,9,3Z"/>\n' +
         '</svg>';
     icons.header[4] = '<svg viewBox="0 0 18 18">\n' +
         '  <path class="ql-fill" d="M10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Zm7.05371,7.96582v.38477c0,.39648-.165.60547-.46191.60547h-.47314v1.29785a.54085.54085,0,0,1-.605.60547h-.69336a.54085.54085,0,0,1-.605-.60547V12.95605H11.333a.5412.5412,0,0,1-.60547-.60547v-.15332a1.199,1.199,0,0,1,.22021-.748l2.56348-4.05957a.7819.7819,0,0,1,.72607-.39648h1.27637a.54085.54085,0,0,1,.605.60547v3.7627h.33008A.54055.54055,0,0,1,17.05371,11.96582ZM14.28125,8.7207h-.022a4.18969,4.18969,0,0,1-.38525.81348l-1.188,1.80469v.02246h1.5293V9.60059A7.04058,7.04058,0,0,1,14.28125,8.7207Z"/>\n' +

+ 57 - 25
views/document/default_read.tpl

@@ -40,31 +40,34 @@
                 <span style="font-size: 12px;font-weight: 100;"></span>
             </div>
             <div class="navbar-header pull-right manual-menu">
-                <div class="dropdown">
-                    <button id="dLabel" class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                        项目
-                        <span class="caret"></span>
+                {{if gt .Member.MemberId 0}}
+                {{if gt .Model.RelationshipId 0}}
+                {{if eq .Model.RoleId 0 1 2}}
+                <div class="dropdown pull-right">
+                   <a href="{{urlfor "DocumentController.Edit" ":key" .Model.Identify ":id" ""}}" class="btn btn-default">编辑</a>
+                </div>
+                {{end}}
+                {{end}}
+                {{end}}
+                <div class="dropdown pull-right" style="margin-right: 10px;">
+                    <a href="{{urlfor "HomeController.Index"}}" class="btn btn-default"><i class="fa fa-home" aria-hidden="true"></i> 首页</a>
+                </div>
+                <div class="dropdown pull-right" style="margin-right: 10px;">
+                {{if eq .Model.PrivatelyOwned 0}}
+                    <button type="button" class="btn btn-success" data-toggle="modal" data-target="#shareProject"><i class="fa fa-share-alt" aria-hidden="true"></i> 分享</button>
+                {{end}}
+                </div>
+                <div class="dropdown pull-right" style="margin-right: 10px;">
+                    <button type="button" class="btn btn-primary" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                        下载 <span class="caret"></span>
                     </button>
-                    <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="dLabel">
-                        {{if gt .Member.MemberId 0}}
-                        {{if gt .Model.RelationshipId 0}}
-                        {{if eq .Model.RoleId 0 1 2}}
-                        <li><a href="{{urlfor "DocumentController.Edit" ":key" .Model.Identify ":id" ""}}">返回编辑</a> </li>
-                        {{end}}
-                        {{end}}
-                        <li><a href="{{urlfor "BookController.Index"}}">我的项目</a> </li>
-                        <li role="presentation" class="divider"></li>
-                        {{end}}
-                        {{if eq .Model.PrivatelyOwned 0}}
-                        <li><a href="javascript:" data-toggle="modal" data-target="#shareProject">项目分享</a> </li>
-                        <li role="presentation" class="divider"></li>
-                        <li><a href="javascript:void(0);" onclick="ExportPdfDoc()">文档导出为 PDF</a> </li>
-                        <li><a href="{{urlfor "DocumentController.ExportBook" ":key" .Model.Identify "output" "pdf"}}" target="_blank">项目导出为 PDF</a> </li>
-                        {{end}}
-
-                        <li><a href="{{urlfor "HomeController.Index"}}" title="返回首页">返回首页</a> </li>
+                    <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel" style="margin-top: -5px;">
+                        <li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "pdf"}}" target="_blank">PDF</a> </li>
+                        <li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "epub"}}" target="_blank">EPUB</a> </li>
+                        <li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "mobi"}}" target="_blank">MOBI</a> </li>
                     </ul>
                 </div>
+
             </div>
         </div>
     </header>
@@ -128,7 +131,7 @@
                             </div>
                             <div class="col-md-8 text-center">
                                 <h1 id="article-title">{{.Title}}</h1>
-                                <h3 id="article-info" class="article-info">{{.Info}}</h3>
+                                {{/*<h3 id="article-info" class="article-info">{{.Info}}</h3>*/}}
                             </div>
                             <div class="col-md-2">
                             </div>
@@ -198,7 +201,7 @@
     <div class="manual-mask"></div>
 </div>
 
-<!-- Share Modal -->
+<!-- 分享项目 -->
 <div class="modal fade" id="shareProject" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
     <div class="modal-dialog">
         <div class="modal-content">
@@ -226,10 +229,39 @@
         </div>
     </div>
 </div>
+<!-- 下载项目 -->
+<div class="modal fade" id="downloadBookModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <h4 class="modal-title" id="myModalLabel">项目分享</h4>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-sm-12 text-center" style="padding-bottom: 15px;">
+                        <img src="{{urlfor "DocumentController.QrCode" ":key" .Model.Identify}}" alt="扫一扫手机阅读" />
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label for="password" class="col-sm-2 control-label">项目地址</label>
+                    <div class="col-sm-10">
+                        <input type="text" value="{{.BaseUrl}}{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" class="form-control" onmouseover="this.select()" id="projectUrl" title="项目地址">
+                    </div>
+                    <div class="clearfix"></div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+
 <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
 <script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
-<script src="/static/layer/layer.js" type="text/javascript"></script>
+<script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/jstree/3.3.4/jstree.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/nprogress/nprogress.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/highlight/highlight.js"}}" type="text/javascript"></script>

+ 13 - 100
views/document/new_html_edit_template.tpl

@@ -48,7 +48,7 @@
             border-left: none;
             height: 100%;
             outline:none;
-            padding: 5px;
+            padding: 5px 5px 30px 5px;
         }
         .btn-info{background-color: #ffffff !important;}
         .btn-info>i{background-color: #cacbcd !important; color: #393939 !important; box-shadow: inset 0 0 0 1px transparent,inset 0 0 0 0 rgba(34,36,38,.15);}
@@ -182,12 +182,13 @@
 <body>
 
 <div class="m-manual manual-editor">
-    <div class="manual-head btn-toolbar" id="editormd-tools"  style="min-width: 1600px;" data-role="editor-toolbar" data-target="#editor">
+    <div class="manual-head btn-toolbar" id="editormd-tools"  style="min-width: 1360px;" data-role="editor-toolbar" data-target="#editor">
         <div class="editor-group">
             <a href="{{urlfor "BookController.Index"}}" data-toggle="tooltip" data-title="返回"><i class="fa fa-chevron-left" aria-hidden="true"></i></a>
         </div>
         <div class="editor-group">
-            <a href="javascript:;" id="markdown-save" data-toggle="tooltip" data-title="保存" class="disabled save"><i class="fa fa-save" aria-hidden="true" name="save"></i></a>
+            <a href="javascript:;" id="markdown-save" data-toggle="tooltip" data-title="保存" class="disabled save"><i class="fa fa-save first" aria-hidden="true" name="save"></i></a>
+            <a href="javascript:;" data-toggle="tooltip" data-title="发布" id="btnRelease"><i class="fa fa-cloud-upload last" name="release" aria-hidden="true"></i></a>
         </div>
         <div class="editor-group">
             <a href="javascript:;" data-toggle="tooltip" data-title="撤销 (Ctrl-Z)" class="ql-undo"><i class="fa fa-undo first" name="undo" unselectable="on"></i></a>
@@ -231,30 +232,13 @@
             <button data-toggle="tooltip" data-title="公式" class="ql-formula editor-item"><i class="fa fa-tasks item" name="tasks" aria-hidden="true"></i></button>
             <select data-toggle="tooltip" data-title="字体颜色" class="ql-color ql-picker ql-color-picker editor-item-select" ></select>
             <select data-toggle="tooltip" data-title="背景颜色" class="ql-background editor-item-select"></select>
-            <a href="javascript:;" data-toggle="tooltip" data-title="附件"><i class="fa fa-paperclip item" aria-hidden="true" name="attachment"></i></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title="模板"><i class="fa fa-tachometer last" name="template"></i></a>
-
-        </div>
-
-        <div class="editormd-group pull-right">
-            <a href="javascript:;" data-toggle="tooltip" data-title="关闭实时预览"><i class="fa fa-eye-slash first" name="watch" unselectable="on"></i></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title="修改历史"><i class="fa fa-history item" name="history" aria-hidden="true"></i></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title="边栏"><i class="fa fa-columns item" aria-hidden="true" name="sidebar"></i></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title="使用帮助"><i class="fa fa-question-circle-o last" aria-hidden="true" name="help"></i></a>
-        </div>
-
-        <div class="editormd-group pull-right">
-            <a href="javascript:;" data-toggle="tooltip" data-title="发布"><i class="fa fa-cloud-upload" name="release" aria-hidden="true"></i></a>
-        </div>
+            <a href="javascript:;" data-toggle="tooltip" data-title="附件" id="btnUploadFile"><i class="fa fa-paperclip last" aria-hidden="true" name="attachment"></i></a>
 
-        <div class="editor-group">
-            <a href="javascript:;" data-toggle="tooltip" data-title=""></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title=""></a>
         </div>
 
         <div class="clearfix"></div>
     </div>
-    <div class="manual-body" style="min-width: 1600px;right: inherit">
+    <div class="manual-body">
         <div class="manual-category" id="manualCategory" style=" border-right: 1px solid #DDDDDD;width: 281px;position: absolute;">
             <div class="manual-nav">
                 <div class="nav-item active"><i class="fa fa-bars" aria-hidden="true"></i> 文档</div>
@@ -263,26 +247,14 @@
             </div>
             <div class="manual-tree" id="sidebar"> </div>
         </div>
-        <div class="manual-editor-container" id="manualEditorContainer" style="min-width: 1319px;">
-            <div class="manual-editormd">
-                <div id="docEditor" class="manual-editormd-active ql-editor ql-blank">
-                    MinDoc 是一款针对IT团队开发的简单好用的文档管理系统。
-
-
-                    MinDoc 的前身是 SmartWiki 文档系统。SmartWiki 是基于 PHP 框架 laravel 开发的一款文档管理系统。因 PHP 的部署对普通用户来说太复杂,所以改用 Golang 开发。可以方便用户部署和实用。
-
-                    开发缘起是公司IT部门需要一款简单实用的项目接口文档管理和分享的系统。其功能和界面源于 kancloud 。
-
-                    可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,能够满足大部分中小团队的文档管理需求。
-                    <div contenteditable="false" class="editor-wrapper"><pre><code class="editor-code">f</code></pre></div>
-                    <div><br/></div>
+        <div class="manual-editor-container" id="manualEditorContainer" style="min-width: 1060px;">
+            <div class="manual-editormd" style="bottom: 0;">
+                <div id="docEditor" class="manual-editormd-active ql-editor ql-blank"></div>
+                <div class="manual-editor-status" style="border-top: 1px solid #DDDDDD;">
+                    <div id="attachInfo" class="item">0 个附件</div>
                 </div>
             </div>
-            <div class="manual-editor-status">
-                <div id="attachInfo" class="item">0 个附件</div>
-            </div>
         </div>
-
     </div>
 </div>
 <!-- 添加文档 -->
@@ -334,9 +306,7 @@
                 <div class="modal-body">
                     <div class="attach-drop-panel">
                         <div class="upload-container" id="filePicker">
-                            <div class="webuploader-pick">
                                 <i class="fa fa-upload" aria-hidden="true"></i>
-                            </div>
                         </div>
                     </div>
                     <div class="attach-list" id="attachList">
@@ -395,57 +365,6 @@
     </div>
 </div>
 
-<div class="modal fade" id="documentTemplateModal" tabindex="-1" role="dialog" aria-labelledby="请选择模板类型" aria-hidden="true">
-    <div class="modal-dialog">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h4 class="modal-title" id="modal-title">请选择模板类型</h4>
-            </div>
-            <div class="modal-body template-list">
-                <div class="container">
-                    <div class="section">
-                        <a data-type="normal" href="javascript:;"><i class="fa fa-file-o"></i></a>
-                        <h3><a data-type="normal" href="javascript:;">普通文档</a></h3>
-                        <ul>
-                            <li>默认类型</li>
-                            <li>简单的文本文档</li>
-                        </ul>
-                    </div>
-                    <div class="section">
-                        <a data-type="api" href="javascript:;"><i class="fa fa-file-code-o"></i></a>
-                        <h3><a data-type="api" href="javascript:;">API文档</a></h3>
-                        <ul>
-                            <li>用于API文档速写</li>
-                            <li>支持代码高亮</li>
-                        </ul>
-                    </div>
-                    <div class="section">
-                        <a data-type="code" href="javascript:;"><i class="fa fa-book"></i></a>
-
-                        <h3><a data-type="code" href="javascript:;">数据字典</a></h3>
-                        <ul>
-                            <li>用于数据字典显示</li>
-                            <li>表格支持</li>
-                        </ul>
-                    </div>
-                </div>
-
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
-            </div>
-        </div>
-    </div>
-</div>
-<template id="template-normal">
-{{template "document/template_normal.tpl"}}
-</template>
-<template id="template-api">
-{{template "document/template_api.tpl"}}
-</template>
-<template id="template-code">
-{{template "document/template_code.tpl"}}
-</template>
 <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
 <script src="{{cdnjs "/static/vuejs/vue.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
@@ -455,6 +374,7 @@
 {{/*<script src="/static/bootstrap/plugins/bootstrap-wysiwyg/bootstrap-wysiwyg.js" type="text/javascript"></script>*/}}
 {{/*<script src="/static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/prettify.js"></script>*/}}
 <script src="/static/katex/katex.min.js" type="text/javascript"></script>
+<script src="/static/to-markdown/dist/to-markdown.js" type="text/javascript"></script>
 <script src="/static/quill/quill.js" type="text/javascript"></script>
 <script src="/static/quill/quill.icons.js"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript" ></script>
@@ -465,14 +385,7 @@
 
 
     $(function () {
-        var $editorEle =  $("#editormd-tools");
 
-        $editorEle.find(".ql-undo").on("click",function () {
-           quill.history.undo();
-        });
-        $editorEle.find(".ql-redo").on("click",function () {
-            quill.history.redo();
-        });
 
         $(".editor-code").on("dblclick",function () {
             var code = $(this).html();
@@ -485,7 +398,7 @@
             $(this).parents(".editor-wrapper").addClass("editor-wrapper-selected");
         });
 
-        $("#attachInfo").on("click",function () {
+        $("#attachInfo,#btnUploadFile").on("click",function () {
             $("#uploadAttachModal").modal("show");
         });